前后端分离-JSON Web Token(JWT)

3,447 阅读6分钟

JSON Web Token(JWT)


1 . 简介

JSON Web Token(缩写JWT)是目前最流行的跨域认证解决方案。 对于传统的用户认证方案为:

  1. 用户向服务器发送用户名和密码。
  2. 服务器验证通过后,在当前**对话(session)**里面保存相关数据,比如用户角色、登录时间等等。
  3. 服务器向用户返回一个 session_id,写入用户的 Cookie
  4. 用户随后的每一次请求,都会通过 Cookie将 session_id 传回服务器
  5. 服务器收到 session_id,找到对应的session并获取前期保存的数据,由此得知用户的身份。

这种传统的通过session的方式适用于前后端不分离的情况,因为session是保存在服务器端,因此对于跨域或服务器集群的情况很不友好。

为了解决传统用户认证的问题,就出现了JWT这一种方案。可以简单的理解为session变为了保存在客户端而不是保存在服务器端了,用户每次请求都会携其内容。因此也就解决了跨域问题。

2. 原理

  1. 客户端初次访问,服务器端在进行认证之会生成一个包含用户数据的JSON对象。为防止该JSON对对象被篡改,服务器会在生成该对象的时候对其进行签名,这个加密后的数据就是token。
  2. 服务器将token发送给客户端。
  3. 客户端将token存放在本地。
  4. 在以后的访问中,客户端每次通信,都会在请求头中携带该JSON对象,服务器端通过该JSON认定用户身份
  5. 因此,服务器端不再保存session数据,变成了无状态的情况。从而比较容易实现扩展。

3. 特点

  1. jwt基于json,数据处理方便。

  2. 可以在令牌(token)中自定义内容,容易扩展。

  3. 使用非对称加密和签名技术,安全性高。

  4. 资源服务使用JWT,可不依赖认证服务即可完成授权。

JWT令牌较长,占用存储空间较大。

3.2 缺点

4. 组成结构

JWT由3部分组成,分别头部header荷载payload签名signature。如下图: 。

4.1 Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据(签名算法、token类型)。如上图中的红色部分。

  • alg:(algorithm)表示签名的算法。
  • typ:(type)表示这个token(令牌)的类型,JWT 令牌统一写为JWT。 最后,将头部进行base64加密(该加密是可对称解密的),就构成了第一部分

4.2 PayLoad

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

  • **标准中注册的声明,该部分建议但不强制使用
    • iss: jwt签发者
    • sub: jwt所面向的用户
    • aud: 接收jwt的一方
    • exp: jwt的过期时间,这个过期时间必须要大于签发时间
    • nbf: 定义在什么时间之前,该jwt都是不可用的.
    • iat: jwt的签发时间
    • jti: jwt的唯一id身份标识,主要用来作为一次性token,从而回避重放攻击。
  • 公共的声明: 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
  • 私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

4.3 Signature

Jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

5. 使用Java操作JWT

JWT Maven坐标:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

5.1 生成JWT

JwtBuilder jwtBuilder = Jwts.builder()
    // 设置jti
    .setId("123")
    // 设置用户sub
    .setSubject("andy")
    // 设置签发时间ita
    .setIssuedAt(new Date())
    // 设置token过期时间 1分钟
    .setExpiration(new Date(60*1000))
    // 设置签名算法和盐
    .signWith(SignatureAlgorithm.HS256,"xxxx");

// 获取jwt token
String token = jwtBuilder.compact();
System.out.println("token:"+token);

// 解密
String[] strings = token.split("\\.");
String header = Base64Codec.BASE64.decodeToString(strings[0]);
String payload = Base64Codec.BASE64.decodeToString(strings[1]);
String signature = Base64Codec.BASE64.decodeToString(strings[2]);
System.out.println("header:"+header);
System.out.println("payload:"+payload);
System.out.println("signature:"+signature);

通过Jwts创建创建jwtBuilder对象。通过jwtsbuilder配置token的相关信息,然后通过compact方法生成token

  • setId():设置荷载中的jti,即jwt中的唯一身份标识。

  • setSubject():设置荷载中的sub,即jwt所面向的用户。

  • setIssuedAt():设置j荷载中的jat,即jwt的签发时间。

  • setExpration():设置荷载中的exp,即jwt的过期时间。jwt过期后无法进行解析

  • cliams():自定义声明,参数以key-value即可。也可以使用.addClaims(Map)的方式自定义声明。

  • signWith():第一个参数为设置header中的签名算法,第二个参数为singature中的secret。

运行程序,查看输出结果如下。其中signature部分输出为乱码:

token:eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMjMiLCJzdWIiOiJhbmR5IiwiaWF0IjoxNjQ5OTMwMTY5fQ.sg15LmBWd6g_TAeUxU9s5tKs6L77UakgSBK_4fFpHkI

header:{"alg":"HS256"}
payload:{"jti":"123","sub":"andy","iat":1649930169
y.`Vw��1S9��:/��jH���

5.2 解析JWT

通过Jwts的parsr()方法可以解析token。通过getXxx()来获取对应的荷载属性。

String token = "eyJhbGcNiJ9.eyJqOioxfQ.sg15LK_4fFpHkI";

//解析token ,需要指定解析的“盐”
Jwt parse = Jwts.parser()
    .setSigningKey("xxxx") 
    .parse(token);
// 获取token的荷载部分
Claims body = (Claims) parse.getBody();

System.out.println("id:"+body.getId());
System.out.println("sub:"+body.getSubject());
System.out.println("iat:"+body.getIssuedAt());

立即执行,输出结果为:

id:123
sub:andy
iat:Thu Apr 14 17:56:09 CST 2022

等待一分钟后再次运行该部分代码,会抛出ExpiredJwtException异常,表示该token已经过期,不能再进行解析:

io.jsonwebtoken.ExpiredJwtException:
JWT expired at 2022-04-14T20:54:56Z. Current time: 2022-04-14T20:55:09Z, 
a difference of 13355 milliseconds.  Allowed clock skew: 0 milliseconds.

JWT 工具类

通常会将常用的jwt的方法封装到类中,减少不必要的代码重复,以下为结合spring security构成的工具类,同样适用于非security框架。

package top.xcyxiaoxiang.server.utils;  
  
import io.jsonwebtoken.Claims;  
import io.jsonwebtoken.Jwts;  
import io.jsonwebtoken.SignatureAlgorithm;  
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: xcy.小相  
 * @date: 2022/3/3  15:02  
 * @Description: Jwt 的工具类。  
 * 该工具类具有以下功能:  
 * 1. 根据用户信息生成token  
 * 2. 从token中获取用户名  
 * 3. 查看token是否有效  
 * 4. token是否到期  
 * 5. 刷新token时间  
 */  
  
@Component  
public class JwtTokenUtil {  
  
    /**  
     * 自定义的荷载的key  
     */    // jwt的面向的用户名  
    private static final String ClAIM_KEY_USERNAME = "sub";  
  
    /**  
     * JWT的加密密钥  
     */  
    @Value("${jwt.secret}")  
    private String secret;  
  
    /**  
     * jwt失效时间增量  
     */  
    @Value("${jwt.expiration}")  
    private long expiration;  
  
  
    /**  
     * 通过userDetails 自定义荷载,并通过荷载生成 token  
     *     * @param userDetails  
     * @return  
     */  
    public String generateTokenByUserDeatials(UserDetails userDetails) {  
        // 定义一个荷载,用于存放 生成token的相关信息  
        HashMap<String, Object> claims = new HashMap<>();  
        // 用户名称  
        claims.put(ClAIM_KEY_USERNAME, userDetails.getUsername());  
  
        return generateTokenWithClaims(claims);  
    }  
  
  
    /**  
     * 从token中获取用户名  
     *  
     * @param token  
     * @return  
     */  
    public String getUserNameFromToken(String token) {  
        String userName;  
        try {  
            Claims claims = this.getClaimsFromToken(token);  
            // 从荷载中获取用户名  
            userName = claims.getSubject();  
        } catch (Exception e) {  
            userName = null;  
        }  
        return userName;  
    }  
  
  
    /**  
     * 验证当前用户的token是否有效(username和exp均有效)  
     * - 用户名不匹配  
     * - 过期  
     *  
     * @param token  
     * @param userDetails  
     * @return 有效 true,无效 false  
     */    public boolean validateToken(String token, UserDetails userDetails) {  
        String userName = this.getUserNameFromToken(token);  
        return userName.equals(userDetails.getUsername()) && !this.isTokenExpired(token);  
    }  
  
  
    /**  
     * 刷新token时间  
     *  
     * @param token  
     * @return  
     */  
    public String refreshToken(String token) {  
        Claims claims = this.getClaimsFromToken(token);  
        return this.generateTokenWithClaims(claims);  
    }  
  
    /**  
     * 判断token是否过期失效  
     *  
     * @param token  
     * @return 过期 false 未过期 true  
     */    private boolean isTokenExpired(String token) {  
        Claims claims = this.getClaimsFromToken(token);  
        Date expireDate = claims.getExpiration();  
  
        return expireDate.before(new Date());  
    }  
  
  
    /**  
     * 从token中获取荷载  
     *  
     * @param token  
     * @return  
     */  
    private Claims getClaimsFromToken(String token) {  
  
        Claims claims;  
        try {  
            claims = Jwts.parser()  
                    .setSigningKey(secret)  // 指定加密时候的secret  
                    .parseClaimsJws(token)  // 指定解密的token  
                    .getBody();  // 获取荷载(payload)部分  
        } catch (Exception e) {  
            claims = null;  
            e.printStackTrace();  
        }  
        return claims;  
    }  
  
  
    /**  
     * 根据 荷载生成 token  
     *     * @param claims map形式的自定义的荷载  
     * @return jwt对象  
     */  
    private String generateTokenWithClaims(Map<String, Object> claims) {  
        return Jwts.builder()  
                // 放入荷载  
                .setClaims(claims)  
                //设置签发时间  
                .setIssuedAt(new Date())  
                // 设置荷载的失效时间  
                .setExpiration(new Date(System.currentTimeMillis() + this.expiration))  
                // 设置签名的加密方法  
                .signWith(SignatureAlgorithm.HS512, this.secret)  
                .compact();  
  
    }  
}