JWT详解

0. 由来

了解一门技术,首先从为什么产生开始说起是最好的。JWT 主要用于用户登录鉴权,所以我们从最传统的 session 认证开始说起。

📌session认证:
在这里插入图片描述

HTTP 协议本身是无状态的协议,每次用户进行用户名和密码认证之后,HTTP 不会留下记录,下一次请求还需要进行认证。

session 认证 就是在用户登录后把将此用户的登录状态存储到服务器的内存中。下一次请求携带 session 信息,服务器通过 session 信息 判断用户登录状态,进行业务处理。

session 认证 的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现 session 不能共享的问题,很难扩展。为解决该问题,推出 token认证 方案。

📌token 认证:
在这里插入图片描述

token认证 的过程就是在用户第一次登录的时候根据秘钥(一般秘钥中会包括此用户的唯一标志,比如账号)生成此次会话的 token,然后之后客户端每次访问服务器都携带 token,服务端再根据秘钥解析,如果解析成功就说明 token 有效,进而可以信任此次请求进行接下来的操作。

​基于 token 的认证方式是一种服务端无状态的认证方式,服务端不存储 token 数据,适合分布式系统。

无状态有状态 是指对于服务器而言的两种不同的处理方式:

  • 无状态:在无状态的认证机制中,服务器不需要保存任何关于客户端的状态信息。每次客户端发送请求时,服务器只需要对请求进行处理,而无需考虑之前的请求状态。这意味着服务器可以更容易地进行水平扩展,因为不需要担心请求会被路由到特定的服务器上。
  • 有状态:相比之下,在有状态的认证机制中,服务器需要保存客户端的状态信息,通常通过会话对象或其他方式来记录客户端的状态。这意味着服务器需要在多个请求之间共享状态信息,可能需要使用特定的机制来保证状态的一致性和可靠性。

💖​基于 token 的认证方式相比传统的 session 认证方式的优点如下:

  • 跨域支持:cookie 是无法跨域的,而 token 由于没有用到 cookie(前提是将 token 放到请求头中),所以跨域后不会存在信息丢失问题。
  • 无状态:token 机制在服务端不需要存储 session 信息,因为 token 自身包含了所有登录用户的信息,所以可以减轻服务端压力,节约服务器资源,并且可以很容易地分布式横向扩展应用
  • 更适用CDN:可以通过内容分发网络请求服务端的所有资料。
  • 更适用于移动端:当客户端是非浏览器平台时,cookie 是不被支持的,此时采用 token 认证方式会简单很多。
  • 无需考虑CSRF:由于不再依赖 cookie,所以采用 token 认证方式不会发生 CSRF,所以也就无需考虑 CSRF 防御。

1. 什么是JWT

JWTtoken认证 的一种具体实现方式,其全称是 JSON Web Token。它采用了 JSON 格式来对 Token 进行编码和解码,并携带了更多的信息,例如用户ID、角色、权限等。

2. JWT结构

JWT 由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将 JWT 的3部分分别进行 Base64 编码后用 . 进行连接形成最终传输的字符串。

JWTString = Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+“.”+base64UrlEncode(payload),secret)

2.1 Header

标头(Header)是一个描述 JWT 元数据的 JSON 对象,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为HS256);typ 属性表示令牌的类型,JWT 令牌统一写为 “JWT”。最后,使用 Base64 算法将上述 JSON 对象转换为字符串。

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

2.2 ✨Payload

有效载荷(Payload)部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT 指定七个默认字段供选择:

  • iss:发行人
  • exp:到期时间
  • sub:主题
  • aud:用户
  • nbf:在此之前不可用
  • iat:发布时间
  • jti:JWT ID用于标识该JWT

除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到 Payload中,如下例:

{
  "name": "Helen"
}

✨注意:默认情况下 JWT 是未加密的,因为只是采用 base64 算法,拿到 JWT 字符串后可以转换回原本的 JSON 数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到 JWT 中,以防止信息泄露。请注意,JWT 只是适合在网络中传输一些非敏感的信息。

2.3 Signature

签名(Signature)是对上面两部分数据签名,具体公式:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret),以确保数据不会被篡改。需要指定一个密钥(secret),该密钥仅仅为保存在服务器中,并且不能向用户公开。

最终,标头(Header)、有效载荷(Payload)、签名(Signature)三个部分组合成一个字符串,每个部分用 . 分隔,就构成整个 JWT 对象。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5iZWpzb24uY29tIiwic3ViIjoiZGVtbyIsImlhdCI6MTczMDMwNTEwNSwibmJmIjoxNzMwMzA1MTA1LCJleHAiOjE3MzAzOTE1MDV9.cf00vgo79p1HYW9V14tQXSJ3Jg_dZOZ0QsiO1NE69TU

在这里插入图片描述

3. JWT的种类

JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用 JWT 在两个组织之间传递安全可靠的信息,JWT 的具体实现可以分为以下几种:

  • nonsecure JWT:未经过签名,不安全的JWT。
  • JWS:经过签名的JWT。常用
  • JWE:payload 部分经过加密的JWT。

4. Java中使用JWT

官网推荐了6个 Java 使用 JWT 的开源库,其中比较推荐使用的是 java-jwtjjwt-root

4.1 引入依赖

  • 📃java-jwt:
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>
  • 📜jjwt-root:
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- 或者使用 jjwt-gson 如果你更喜欢 Gson -->
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

4.2 生成token

  • 📃java-jwt:
public void testGenerateToken(String secretKey){
    // 指定token过期时间为10秒
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.SECOND, 10);

    String token = JWT.create()
            .withHeader(new HashMap<>())  // Header
            .withClaim("userId", 21)  // Payload
            .withClaim("userName", "John")
            .withExpiresAt(calendar.getTime())  // 过期时间
            .sign(Algorithm.HMAC256(secretKey));  // 签名用的secret

    System.out.println(token);
}
  • 📜jjwt-root:
public void testGenerateToken(String secretKey){
    // 定义密钥
    String secretKey = "your-256-bit-secret";
    Key key = Keys.hmacShaKeyFor(secretKey.getBytes());
    
    String token = Jwts.builder()
            .setHeaderParam("typ", "JWT")
            .setHeaderParam("alg", "HS256")
            .setSubject("user123")  // 设置主题
            .claim("userId", 21)    // 添加自定义声明
            .claim("name", "John")
            .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))  // 设置过期时间为一小时后
            .signWith(key, SignatureAlgorithm.HS256)  // 使用 HS256 算法签名
            .compact();
    System.out.println(token);
}
  • setIssuer(issuer):设置 JWT 的发行人。
  • setSubject(subject):设置 JWT 的主题。
  • claim("key", value):添加自定义的键值对到 JWT 的负载中。
  • setExpiration():设置 JWT 的过期时间。
  • signWith():使用 HMAC SHA-256 算法和密钥对 JWT 进行签名。
  • compact():将 JWT 构建为字符串形式。

4.3 解析token

  • 📃java-jwt:
public void testResolveToken(String jwtToken){
    // 创建解析对象,使用的算法和secret要与创建token时保持一致
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!34ADAS")).build();
    // 解析指定的token
    DecodedJWT decodedJWT = jwtVerifier.verify(jwtToken);
    // 获取解析后的token中的payload信息
    Claim userId = decodedJWT.getClaim("userId");
    Claim userName = decodedJWT.getClaim("userName");
    System.out.println(userId.asInt());
    System.out.println(userName.asString());
    // 输出超时时间
    System.out.println(decodedJWT.getExpiresAt());
}
  • 📜jjwt-root:
public void testResolveToken(Key key, String jwtToken){
   try {
       Claims claims = Jwts.parserBuilder()
               .setSigningKey(key)
               .build()
               .parseClaimsJws(jwtToken)
               .getBody();
   
       // 提取和打印载荷信息
       System.out.println("Subject: " + claims.getSubject());
       System.out.println("User ID: " + claims.get("userId"));
       System.out.println("Name: " + claims.get("name"));
       System.out.println("Issued At: " + claims.getIssuedAt());
       System.out.println("Expiration: " + claims.getExpiration());
   } catch (Exception e) {
       System.out.println("Error parsing JWT: " + e.getMessage());
   }
}
  • Jwts.parserBuilder():创建一个 JwtParserBuilder,用来配置 JWT 的解析和验证。
  • setSigningKey(secretKey):设置签名密钥。
  • build():创建 JwtParser。
  • parseClaimsJws(jwt):解析 JWT 并获取 Claims 对象。
  • claims.get(key):从 Claims 对象中提取并输出相关信息。

4.4 ✨JWT工具类

  • 📃java-jwt:
@Configuration
public class JwtUtil {
    @Value("${shiro.jwt.secret}")
    private static String secret;
	
    @Value("${shiro.jwt.expire}")
    private static Long expire;
	
    @Value("${shiro.jwt.header.alg}")
    private static String headerAlg;
	
    @Value("${shiro.jwt.header.typ}")
    private static String headerTyp;

    /**
     * 生成token
     */
    public static String getToken(String account) {
        // 设置秘钥
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(account).append(secret);

        // 设置jwt头header
        Map<String, Object> headerClaims = new HashMap<>();
        headerClaims.put("alg", headerAlg); // 签名算法
        headerClaims.put("typ", headerTyp); // token 类型
        // 设置jwt的header,负载paload以及加密算法
        String token = JWT
                .create()
                .withHeader(headerClaims)
                .withClaim("account" ,account)
                .withClaim("expire", System.currentTimeMillis() + expire)
                .sign(Algorithm.HMAC256(stringBuilder.toString()));
        return token;
    }

    /**
     * 无需秘钥就能获取其中的信息
     * 解析token.
     * {
     * "account": "account",
     * "timeStamp": "134143214"
     * }
     */
    public static Map<String, String> parseToken(String token) {
        HashMap<String, String> map = new HashMap<String, String>();
        // 解码 JWT
        DecodedJWT decodedJwt = JWT.decode(token);
        Claim account = decodedJwt.getClaim("account");
        Claim expire = decodedJwt.getClaim("expire");
        map.put("account", account.asString());
        map.put("expire", expire.asLong().toString());
        return map;
    }

    /**
     * 解析token获取账号.
     */
    public static String getAccount(String token) {
        HashMap<String, String> map = new HashMap<String, String>();
        DecodedJWT decodedJwt = JWT.decode(token);
        Claim account = decodedJwt.getClaim("account");
        return account.asString();
    }

    /**
     * 校验token是否正确
     * @param token Token
     * @return boolean 是否正确
     */
    public static boolean verify(String token) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(getAccount(token)).append(secret);
        // 帐号加JWT私钥解密
        Algorithm algorithm = Algorithm.HMAC256(stringBuilder.toString());
        JWTVerifier verifier = JWT.require(algorithm).build();
        try {
            verifier.verify(token);
            return true; // 验证成功
        } catch (JWTVerificationException e) {
            return false; // 验证失败
        }
    }

}
  • 📜jjwt-root:
public class JwtUtils {
    @Value("${shiro.jwt.secret}")
    private static String secret;
	
    @Value("${shiro.jwt.expire}")
    private static long expire;

    // 创建 JWT
    public static String createToken(String issuer, String subject, Long userId, String username, String role) {
        return Jwts.builder()
                .setIssuer(issuer)
                .setSubject(subject)
                .claim("userId", userId)
                .claim("username", username)
                .claim("role", role)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
                .compact();
    }
	
	// 验证 JWT 是否有效
    public static boolean validateToken(String jwt) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                    .build()
                    .parseClaimsJws(jwt);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    // 解析 JWT 并返回 Claims 对象
    public static Claims parseClaims(String jwt) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                    .build()
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (Exception e) {
            throw new RuntimeException("JWT 解析失败: " + e.getMessage());
        }
    }

    // 从 JWT 中获取某个声明
    public static <T> T getClaimFromToken(String jwt, String claimName, Class<T> clazz) {
        Claims claims = parseClaims(jwt);
        return claims.get(claimName, clazz);
    }

    // 从 JWT 中获取用户 ID
    public static Long getUserIdFromToken(String jwt) {
        return getClaimFromToken(jwt, "userId", Long.class);
    }

    // 从 JWT 中获取用户名
    public static String getUsernameFromToken(String jwt) {
        return getClaimFromToken(jwt, "username", String.class);
    }

    // 从 JWT 中获取角色
    public static String getRoleFromToken(String jwt) {
        return getClaimFromToken(jwt, "role", String.class);
    }

    public static void main(String[] args) {
        // 创建 JWT
        String jwt = createToken("your-issuer", "your-subject", 123L, "exampleUser", "admin");
        System.out.println("Generated JWT: " + jwt);

        // 验证 JWT
        if (validateToken(jwt)) {
            System.out.println("JWT is valid.");
        } else {
            System.out.println("JWT is invalid.");
        }

        // 解析 JWT
        Claims claims = parseClaims(jwt);
        System.out.println("Issuer: " + claims.getIssuer());
        System.out.println("Subject: " + claims.getSubject());
        System.out.println("User ID: " + claims.get("userId"));
        System.out.println("Username: " + claims.get("username"));
        System.out.println("Role: " + claims.get("role"));
        System.out.println("Issued At: " + claims.getIssuedAt());
        System.out.println("Expiration: " + claims.getExpiration());

        // 从 JWT 中获取特定信息
        Long userId = getUserIdFromToken(jwt);
        String username = getUsernameFromToken(jwt);
        String role = getRoleFromToken(jwt);

        System.out.println("User ID from token: " + userId);
        System.out.println("Username from token: " + username);
        System.out.println("Role from token: " + role);
    }
}

5. Springboot使用JWT实现登录认证以及请求拦截

在这里插入图片描述

📊 主要流程如下:

  1. 登录验证通过后,给请求方生成一个随机不重复字符串(一般使用 uuid 等算法生成),然后将该字符串作为 key 的一部分,用户信息作为 value 存入 Redis,并设置过期时间,这个过期时间就是登录失效的时间
  2. 将第一步生成的随机字符串作为 JWTpayload,生成 JWT 字符串返回给请求方。
  3. 请求方在之后的每次请求,都在请求头中的 Authorization 字段携带 JWT 字符串。
  4. 服务端定义一个 拦截器,每次收到请求方请求时,先从请求头中的 Authorization 字段中取出 JWT 字符串并进行验证。验证通过后解析出 payload 中的随机字符串,然后再用这个随机字符串得到 key,从 Redis 中获取用户信息,如果能获取到就说明用户已经登录。

📌服务端主要配置拦截器、配置要拦截哪些接口,伪代码如下,后续的文件 📖《Shiro实战》会详细介绍:

涉及文件:

  • 拦截器 JWTInterceptor
  • 拦截器配置 InterceptorConfig
public class JWTInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token= request.getHeader("Authorization");
        if(StringUtils.isEmpty(token)){
            throw new Exception("token不能为空");
        }
        try {
            // 1.校验JWT字符串
            Claims claims = parseClaims(token);
            // 2.取出JWT字符串载荷中的随机字符串,从Redis中获取用户信息
            ...
            return true;
        }catch (SignatureVerificationException e){
            System.out.println("无效签名");
            e.printStackTrace();
        }catch (TokenExpiredException e){
            System.out.println("token已经过期");
            e.printStackTrace();
        }catch (AlgorithmMismatchException e){
            System.out.println("算法不一致");
            e.printStackTrace();
        }catch (Exception e){
            System.out.println("token无效");
            e.printStackTrace();
        }
        return false;
    }
}
// 配置要拦截哪些接口
@Configuration
public class InterceptorConfig  implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptor())
                //拦截的路径
                .addPathPatterns("/**")
                //排除登录接口 /test/login 表示你给控制器起的名称/控制器下的方法,如login
                .excludePathPatterns("/test/login");
    }
}

✨注意,在实际开发中需要用下列手段来增加 JWT 的安全性

  • JWT 是在请求头中传递的,所以为了避免网络劫持,推荐使用 HTTPS 来传输,更加安全。
  • JWT 签名密钥是存放在服务端的,所以只要服务器不被攻破,理论上 JWT 是安全的,因此要保证服务器的安全。
  • JWT 可以使用暴力穷举来破解,所以为了应对这种破解方式,可以定期更换服务端的签名密钥(盐值)。

6. Q/A

6.1 用户登录时候,生成uuid传入redis,并且设置过期时间,如果客户一直使用到了过期时间,需要客户重新登录,怎么让过期时间在用户操作的时候自动延长?

答:可以在用户每次请求都刷新 token 的过期时间

6.2 在实际开发中的应用部分,为什么要把 uuid 放入 payload 中?直接返回 uuid ,也能从 redis 里面读取出用户信息,还节省了 JWT 加密、解密的时间。

答:直接用 uuid + redis 也可以,结合 JWT 是为了防篡改校验

6.3 为什么引用Redis呢?

  1. JWT 本身不能续期,结合 redis,可以实现续期主动登出
  2. redis 可以缓存用户信息,减少数据库压力。
  3. 可以实现分布式环境下的会话共享。

6.4 是不是意味着如果JWT暴露,别人就可以拿到你的权限。在你的有效期内进行所有权限的操作?

答:是的。所以实际开发中要使用 HTTPS 来传输,避免被他人截获。

6.5 对于登录认证功能,JWT结合Redis就可以实现,引入shiro的作用是什么?

答:shiro用于权限控制。shiro 和 springsecurity 这些框架的意义是帮助封装了认证和授权相关的功能,可以在接口方法上通过注解形式设置访问接口需要的权限,当用户访问接口时会获取到用户拥有的权限和接口注解标注的权限进行比对,判定是否有权限访问接口。这一系列逻辑如果不引入 shiro 这样的框架,就需要我们自己手动来实现。

参考文章:
📖 JWT详解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会叫的狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值