JWT简述JWT(Jsonwebtoken)概述J

「这是我参与11月更文挑战的第22天,活动详情查看:2021最后一次更文挑战

JWT (Json web token)概述

JWT的由来

由于http 协议本身是无状态的协议 这是所有问题的起点和关键,所以使用了session

但是传统的Session存在两个问题

  1. 用户请求规模大之后增加服务器内存开销;
  2. 不利于服务端搭建集群。

所以为了解决这个原因,就使用了JWT

JWT 的认证流程

  1. 客户端发送用户名和密码至服务端进行认证。
  2. 服务端认证通过后,生成一个具有唯一性标识的字符串(Token)
  3. 服务端将 Token 发还给客户端,客户端后续的请求都需要带上这个 Token
  4. 服务端再次收到客户端请求时,认证这个 Token 是否是自己当初生成且未经篡改过的。如果没毛病,那么该干嘛干嘛。

Jwt组成

  • 头部(Header)

    作用:用于描述这个Jwt的基本组成部分,如类型,加密方式等

    {
      "typ": "JWT",
      "alg": "HS256"
    }
    真正加密后是这样的   eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
    复制代码
  • 荷载(Payload)

    作用:代表JWT的信息内容

    最起码有五个标准字段,剩下的都是可以自己添加的信息

    {
        "iss": "John Wu JWT",
        "iat": 1441593502,
        "exp": 1441594722,
        "aud": "www.example.com",
        "sub": "jrocket@example.com",
        "from_user": "B",
        "target_user": "A"
    }
    加密后是这样的
    eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
    复制代码
  • 签名(Signature)

    作用:这个一般是用作校验使用的,也就是为了解决jwt安全性使用的

    一般是使用头部+荷载+密钥转换成的

安全问题

头部和荷载是使用base64加密生成的,是对称加密

  1. 因为签名是有密钥生成的,所以客户端并不能仿照出一个对应的JWT字符串

  2. 所以,一旦客户端修改了荷载或者头部,发给服务端,服务端使用头部+荷载+密钥并不是对应的签名,就可以检测到客户端动了这个JWT

Java使用JWT

  1. 加上依赖

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    
    <!-- jjwt 依赖于 java-jwt -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.4.0</version>
    </dependency>
    复制代码
  2. 加上对应的工具类

    package com.xja.util;
    
    import java.util.*;
    import com.auth0.jwt.*;
    import com.auth0.jwt.algorithms.Algorithm;
    import io.jsonwebtoken.*;
    import org.apache.commons.codec.binary.Base64;
    
    import java.util.*;
    
    public class JwtUtil {
    
        public static final String defaultBase64EncodedSecretKey = "badbabe";
        public static final SignatureAlgorithm defaultSignatureAlgorithm = SignatureAlgorithm.HS256;
    
        // 生成签名是所使用的秘钥
        private final String base64EncodedSecretKey;
    
        // 生成签名的时候所使用的加密算法
        private final SignatureAlgorithm signatureAlgorithm;
    
        public JwtUtil() {
            this(defaultBase64EncodedSecretKey, defaultSignatureAlgorithm);
        }
    
        public JwtUtil(String secretKey, SignatureAlgorithm signatureAlgorithm) {
            this.base64EncodedSecretKey = Base64.encodeBase64String(secretKey.getBytes());
            this.signatureAlgorithm = signatureAlgorithm;
        }
    
        /**
         * 生成 JWT Token 字符串
         *
         * @param iss       签发人名称
         * @param ttlSeconds jwt 过期时间(秒)
         * @param claims    额外添加到荷部分的信息。
         *                  例如可以添加用户名、用户ID、用户(加密前的)密码等信息
         */
        public String encode(String iss, long ttlSeconds, Map<String, Object> claims) {
            if (claims == null) {
                claims = new HashMap<>();
            }
    
            // 签发时间(iat):荷载部分的标准字段之一
            long nowMillis = System.currentTimeMillis();
    
            // 下面就是在为payload添加各种标准声明和私有声明了
            JwtBuilder builder = Jwts.builder()
                    // 荷载部分的非标准字段/附加字段,一般写在标准的字段之前。
                    .setClaims(claims)
                    // JWT ID(jti):荷载部分的标准字段之一,JWT 的唯一性标识,虽不强求,但尽量确保其唯一性。
                    .setId(UUID.randomUUID().toString())
                    // 签发时间(iat):荷载部分的标准字段之一,代表这个 JWT 的生成时间。
                    .setIssuedAt(new Date(nowMillis))
                    // 签发人(iss):荷载部分的标准字段之一,代表这个 JWT 的所有者。通常是 username、userid 这样具有用户代表性的内容。
                    .setSubject(iss)
                    // 设置生成签名的算法和秘钥
                    .signWith(signatureAlgorithm, base64EncodedSecretKey);
    
            if (ttlSeconds>= 0) {
                long expMillis = nowMillis + ttlSeconds * 1000;
                // 过期时间(exp):荷载部分的标准字段之一,代表这个 JWT 的有效期。
                builder.setExpiration(new Date(expMillis));
            }
    
            return builder.compact();
        }
    
    
        /**
         * JWT Token 由 头部 荷载部 和 签名部 三部分组成。签名部分是由加密算法生成,无法反向解密。
         * 而 头部 和 荷载部分是由 Base64 编码算法生成,是可以反向反编码回原样的。
         * 这也是为什么不要在 JWT Token 中放敏感数据的原因。
         *
         * @param jwtToken 加密后的token
         * @return claims 返回荷载部分的键值对
         */
        public Claims decode(String jwtToken) {
    
            // 得到 DefaultJwtParser
            return Jwts.parser()
                    // 设置签名的秘钥
                    .setSigningKey(base64EncodedSecretKey)
                    // 设置需要解析的 jwt
                    .parseClaimsJws(jwtToken)
                    .getBody();
        }
    
    
        /**
         * 校验 token
         * 在这里可以使用官方的校验,或,
         * 自定义校验规则,例如在 token 中携带密码,进行加密处理后和数据库中的加密密码比较。
         *
         * @param jwtToken 被校验的 jwt Token
         */
        public boolean isVerify(String jwtToken) {
            Algorithm algorithm = null;
    
            switch (signatureAlgorithm) {
                case HS256:
                    algorithm = Algorithm.HMAC256(Base64.decodeBase64(base64EncodedSecretKey));
                    break;
                default:
                    throw new RuntimeException("不支持该算法");
            }
    
            JWTVerifier verifier = JWT.require(algorithm).build();
            verifier.verify(jwtToken);  // 校验不通过会抛出异常
                                        // 合法判断标准:
                                        // 1. JWT 的头部和荷载部分内容没有被篡改过。
                                        // 2. JWT 没有过期
            return true;
        }
    
        public static void main(String[] args) throws InterruptedException {
            JwtUtil util = new JwtUtil("tom", SignatureAlgorithm.HS256);
    
            Map<String, Object> map = new HashMap<>();
            map.put("username", "tom");
            map.put("password", "123456");
            map.put("age", 20);
    
            String jwtToken = util.encode("tom", 1, map);
    
            System.out.println(jwtToken);
            util.isVerify(jwtToken);
            System.out.println("合法");
    
            Thread.sleep(2000);
            util.isVerify(jwtToken);
    
            /*
            util.decode(jwtToken).entrySet().forEach((entry) -> {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            });
             */
        }
    }
    复制代码
  3. 修改登录功能

    1. 首先判断登录账号密码是否成功

      不成功就返回不成功

      成功就继续

    2. 创建一个JWT对象

      对象中传入几个参数

      1. 用户名

      2. 多久过期

      3. 可以放在荷载中的其他字段的一个Map类型对象

    3. 将对象返回给前台