JSON Web Token

JWT

JWT是 JSON Web Token 的缩写,它是基于开源标准(RFC 7519)定义的一种可以安全传输的 JSON对象。

  • JWT之所以叫JSON Web Token,因为其 header 和 payload 在编码之前都是JSON格式
  • JWT规定以JSON格式传递信息,header 和 payload 通常使用 base64 编码成字符串
  • JWT是自包含的,Token本身携带了验证信息,不需要借助其他工具就能知道一个Token是否有效。但当需要高级功能如token刷新、黑名单等,还是需要借助缓存和数据库等

为什么要使用JWT?

跨域认证问题

互联网服务用户认证的一般流程如下所示:

  1. 用户向服务器发送用户名和密码
  2. 服务器验证通过后,在当前session保存相关数据,如用户角色,登录时间等
  3. 服务器向用户返回一个 session_id,写入用户的 Cookie
  4. 用户随后的每一个请求都会通过 Cookie 将 session_id 传回服务器
  5. 服务器收到 session_id 后,找到前期保存的数据,以此验证用户的身份

这种方式的问题在于,扩展性不好。如果服务器是一个集群或者是跨域的服务导向架构,就要求 session 数据共享,使每台服务器都能够读取 session 。

示例:A网站和B网站都是某公司下的服务,要求用户在登录其中一个网站后,再访问另外一个网站实现自动登录。

  • session数据持久化:将session数据写入持久层,各服务器收到请求后都向持久层拿数据。
  • JWT:服务器不保存数据,所有数据都保存在客户端,每次请求都发回服务器。

使用session数据持久化会造成工程量增大,且会因为持久层失效导致单点登录失败。

JWT的结构

Header(头部)

存放签名的生成算法和Token类型。

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload(载荷)

存放携带的用户数据。

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

Payload中的字段:

字段 全称 作用
iss Issuer 代表token的颁发者
sub Subject 代表token的主题
aud Audience 代表token的接收目标
exp Expiration Time 代表token的过期时间,时间戳格式
nbf Not Before 代表token在这个时间之前不能被处理,纠正服务器的时间偏差
iat Issued At 代表token的颁发时间
jti JWT ID 代表token的id

除了以上标准定义的字段外,用户可以自由添加需要的信息,如用户ID,用户名等。通常添加的是经常使用但安全性要求不高的信息。

Signature(签名)

以 header 和 payload 生成的签名,一旦 header 或 payload 被篡改,验证将失败。

// secret: 密钥, 只有服务器知道
String signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

使用Header中指定的签名算法按上述代码产生签名,然后把Header、Payload、Signature三部分拼车一个字符串,各部分之间使用 . 分隔,最后返回给用户。

下图为JSON Web Token官网首页的一个示例:

JSON Web Token

JWT认证流程

  1. 用户使用账号密码登录,调用服务器登录接口
  2. 服务器登录程序生成token,并返回给用户
  3. 用户后续请求携带token
  4. 服务器收到用户请求后,验证token的合法性、有效性,验证通过后处理请求
  5. 返回请求结果给用户

JWT使用方式

客户端收到服务器返回的JWT,可以存储在Cookie中,也可以存储在localStorage。此后客户端每次与服务器通信都要携带这个JWT,可以放在Cookie中自动发送,但不能跨域,所以更好的做法是放在HTTP请求头的 Authorization 字段中。

JWT特点

  • JWT最大的缺点是由于服务器不保存session状态,因此无法在使用过程中废除某个token或更改token权限,即token一旦签发,在到期之间会一直有效,除非服务器部署额外的逻辑
  • JWT本身包含认证信息,一旦泄漏,任何人都可以获得该令牌的所有权限。所以JWT的有效期应该设置的比较短,以防盗用
  • JWT应该使用HTTPS协议传输,为了减少盗用

Springboot集成JWT

1 添加pom依赖

<!--  SpringSecurity 依赖配置  -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--  JWT(Json Web Token)登录支持  -->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>

2 配置application.yml

# 自定义JWT
jwt:
  field: Authorization         # 请求头字段
  secret: 123456               # 密钥
  expiration: 604800           # 过期时间(60*60*24s)
  tokenHead: Bearer            # token开头

3 添加JWT工具类

package com.nudt.demo_02.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: Lzy
 * @Time: 2022/4/18
 * @Description: JwtToken生成的工具类
 */
@Component
public class JwtTokenUtil {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 生成 JWT Token
     * 构成: Header.Payload.Signature
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)    //设置payLoad
                .setExpiration(generateExpirationDate())    //设置过期时间
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 生成 token 过期时间
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);  // 一天后过期
    }

    /**
     * PayLoad
     * 从 token 中获取JWT中的payload
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }
        catch (Exception e) {
            LOGGER.info("JWT 格式验证失败 : {}", token);
        }

        return claims;
    }

    private Date getExpireDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();    //map.get("exp", Date.Class)
    }

    /**
     *
     * @param token
     * @return True —— 已过期; False —— 未过期
     */
    private boolean isTokenExpired(String token) {
        Date expireDate = getExpireDateFromToken(token);
        return expireDate.before(new Date());
    }

    public String getUserNameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();    //从JwtMap中获得主题
        }
        catch (Exception e) {
            username = null;
        }

        return username;
    }

    /**
     * 验证token是否有效
     * @param token 客户端传入的token
     * @param userDetails 数据库查询的用户信息
     * @return
     */
    public boolean isTokenValidate(String token, UserDetails userDetails) {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 根据用户信息生成 token
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    public boolean canRefresh(String token) {
        return !isTokenExpired(token);
    }

    public String refresh(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

}

4 JWT登录授权

package com.nudt.demo_02.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author: Lzy
 * @Time: 2022/4/21
 * @Description: JWT登录授权过滤器
 */
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.field}")
    private String field;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.field);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            LOGGER.info("Checking username: {}", username);

            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtil.isTokenValidate(authToken, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    LOGGER.info("Authenticated user: {}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }

        filterChain.doFilter(request, response);
    }
}

参考文章

[1] JSON Web Token 入门教程

[2] mall学习教程

[3] 深入浅出之JWT

posted @ 2022-06-25 21:06  ylyzty  阅读(52)  评论(0编辑  收藏  举报