一文带你了解jwt - 2022-08-19

哈喽,大家好,我是指北君。 今天带大家来认识一下什么是jwt。

jwt简介

JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于 Json 的开放标准。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,由服务器通过秘钥(serectkey)然后把用户id以及其他信息包装进行一系列的加密算法生成,用户登录的成功,顺带把这个token返回到客户端,用户以后拿着token来访问相关资源,服务器接收到token,通过serectkey然后通过特定算法,解析出这个token中的用户id,解析成功!那么这个人就是该用户,然后通过解析出的id进行该用户的相关操作。这就为应用的扩展提供了便利。

JWT的认证流程如下:

  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求
  2. 后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同{header}.{payload}.{signature}的字符串后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可.
  3. 前端在每次请求时将JWT Token放入HTTP请求头中,后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等
  4. 验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果

注:这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin:

JWT的构成

1
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJ0ZXN0OTUyNyIsImlhdCI6MTY1NjU1OTY2OCwic3ViIjoie1widXNlcklkXCI6XCI5NTI3XCIsXCJ1c2VyTmFtZVwiOlwidGVzdDk1MjdcIn0iLCJleHAiOjE2NTY1NjE0Njh9.7oot0glDxaL-g7pOJ2mZld2VLRhpo0h5y_BbVI4ZolA

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload,类似于飞机上承载的物品),第三部分是签证(signature)。

头部(header)

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法,通常直接使用 HMAC SHA256。这的加密算法也就是签名算法。

{ “alg”:”HS256” } 经过Base64编码之后 ewogICAgImFsZyI6IkhTMjU2Igp9

载荷(payload)

载荷就是存放有效信息的地方。这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明
    1
    2
    3
    4
    5
    6
    {
      "sub":{"userId":"9527","userName":"test9527"},
      "exp":1656561468,
      "iat":1656559668,
      "jti":"test9527"
    }
    
  • sub: 主体,一般是用户信息,最好不要放入敏感信息
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识

签证(signature)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。 signature顾名思义就是签名,签名一般就是用一些算法生成一个能够认证身份的字符串,secret配置在服务器端,不能泄露出去,就可以自己给自己签发token了。

JWT的使用(JAVA)


创建一个新项目,引入jwt包

1
2
3
4
5
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

JWT工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
     
 public class JwtUtil {
    //加密 解密时的密钥 用来生成key
    public static final String JWT_KEY = "secret";
     // 过期时间30分钟
    private static final long EXPIRE_TIME = 30 * 60 * 1000;
      //生成加密后的秘钥 secretKey
      
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    public static String createJWT(String id, String subject){
   
           //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
           SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
           //生成JWT的时间
           long nowMillis = System.currentTimeMillis();
           Date now = new Date(nowMillis);
           //生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
           SecretKey key = generalKey();
           //这里其实就是new一个JwtBuilder,设置jwt的body
           JwtBuilder builder = Jwts.builder()
           //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                   .setClaims(claims)
           //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                   .setId(id)
           //iat: jwt的签发时间
                   .setIssuedAt(now)
           //sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                   .setSubject(subject)
           //设置签名使用的签名算法和签名使用的秘钥
                   .signWith(signatureAlgorithm, key);
   
               long expMillis = nowMillis + EXPIRE_TIME;
               Date exp = new Date(expMillis);
               //设置过期时间
               builder.setExpiration(exp);
           //就开始压缩为xxxxxxxxxxxxxx.xxxxxxxxxxxxxxx.xxxxxxxxxxxxx这样的jwt
           return builder.compact();
       }
    /**
     * 获得token中的信息无需secret解密也能获得
     * token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT._decode_(token);
            return jwt.getClaim("userName").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

  • 创建controller类
  • 访问测试
  • 拿到的token可以使用Base64解码,可以看到,能看到用户信息,所以,token中不要带有敏感信息,防止泄露。
  • 使用该token调用getUser接口,可以正确解析
  • 下面我们不使用getToken获取token,而是伪造一个token,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
      "sub":{
          "userId":"001",
          "userName":"test001"
      },
      "exp":1656561468,
      "iat":1656559668,
      "jti":"test001"
    }
    

  • 再组合header,payload和signature,得到新的token访问getUser
    1
    eyJhbGciOiJIUzI1NiJ9.ewogICAgInN1YiI6ewogICAgICAgICJ1c2VySWQiOiIwMDEiLAogICAgICAgICJ1c2VyTmFtZSI6InRlc3QwMDEiCiAgICB9LAogICAgImV4cCI6MTY1NjU2MTQ2OCwKICAgICJpYXQiOjE2NTY1NTk2NjgsCiAgICAianRpIjoidGVzdDAwMSIKfQ==.7oot0glDxaL-g7pOJ2mZld2VLRhpo0h5y_BbVI4ZolA
    
  • 得到结果如下:无法正确解析token,说明此token无效 最后, JWT过期时间要设置适宜 ,过长,可能会被截取到,造成用户信息泄露安全等问题;过短用户体验不佳。
Java Geek Tech wechat
欢迎订阅 Java 技术指北,这里分享关于 Java 的一切。