JWT
快来分享你的内容吧~
- 06-30 13:15·@编程小助手 微信: leikooo_一句话结论:JWT 不适合用来维持用户登录状态。 它本来就不是为持久会话设计的;真正干这件事的工具,是服务端 Session + Cookie,而且这套方案已经用了几十年。 先把概念对齐。很多人把「Cookie vs JWT」拿来对比,这本身就不对——查看全文leikooo:最近看到两篇文章感觉对又重新认识了 JWT😋,http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/、https://gist.github.com/samsch/0d1f3d3b4745d778f78b230cf60614529510分享
- 2025-03-15·Java后端
别用 JWT 管理用户会话
> 参考:[Stop using JWTs!](https://gist.github.com/samsch/0d1f3d3b4745d778f78b230cf6061452)(samsch) --- 一句话结论:**JWT 不适合用来维持用户登录状态。** 它本来就不是为持久会话设计的;真正干这件事的工具,是服务端 Session + Cookie,而且这套方案已经用了几十年。 先把概念对齐。很多人把「Cookie vs JWT」拿来对比,这本身就不对——Cookie 是**存储/传输机制**,JWT 是**带签名的令牌格式**,两者不是对立关系。真正该比的是: - **无状态 JWT**:会话数据直接编码进 Token 里 - **有状态 JWT**:Token 里只放一个 Session ID,数据仍在服务端 - **传统 Session Cookie**:一个(可选签名的)Session ID,数据同样在服务端 本文不是说 JWT 永远不该用,而是:**别把它当会话机制。** 那样做既不合适,还有安全隐患。 ## 常见误区 **「无状态更好扩展」**——只有无状态 JWT 在技术上才略沾边,但绝大多数应用根本到不了那个规模。单机多进程用 Redis 存 Session,多机再挂一台 Redis,现有框架都支持得很好。真到了要换方案的时候,全员登出一次就能迁移,不值得为了「未来可能」提前背上 JWT 的坑。 **「JWT 更简单、更灵活、更安全」**——恰恰相反。框架自带的 Session 开箱即用;灵活度上 Session 本来就能存任意数据;安全上,好的 Session 实现同样会用签名 Cookie,「用了密码学」不等于更安全。把 JWT 塞进 Local Storage 反而更危险——任何能执行 JS 的 XSS 都能把 Token 读走,而 HttpOnly Cookie 天生防这个。 **「JWT 自带过期,不用管 Session 清理」**——服务端过期同样好做,而且能在过期时清理数据库里的 Session 数据;无状态 JWT 过期前服务端毫无办法。 **「用 JWT 就不用 Cookie 同意弹窗」**——错。法规管的是**持久标识符**,跟你怎么存无关。功能性登录态本来就不需要额外同意;追踪分析才需要,换 JWT 也逃不掉。 **「放 Local Storage 能防 CSRF」**——CSRF 要靠 CSRF Token 防,跟 Session 机制无关。JWT 放 Cookie 一样有 CSRF;放 Local Storage 则强依赖 JS,还引入 XSS 风险。正确的 CSRF 缓解只有一种:**CSRF Token**。 **「移动端/禁 Cookie 用户更适合 JWT」**——移动浏览器和 HTTP 客户端都支持 Cookie;禁 Cookie 的用户通常也会禁 Local Storage。后端对移动端和 Web 端可以同一套 Session,Cookie 对 HTTP 客户端来说就是普通请求头,每次带上即可。 ## 真正的代价 **体积大。** 无状态 JWT 把用户信息全塞进 Token,很容易顶破 Cookie 4KB 上限,被迫改用 Local Storage——然后回到上面的安全问题。 **无法单独作废。** Session 可以随时在服务端踢掉;无状态 JWT 在过期前永远有效。用户改密码、发现账号被盗、撤销管理员权限——你都没法立刻让旧 Token 失效,只能等它自然过期,或者自己搭一套「黑名单」基础设施,那「无状态」也就名存实亡了。 **数据会过期。** Token 里的角色、权限、资料都是签发那一刻的快照。用户刚被降权,手里还攥着写着 `admin` 的旧 Token,而你又作废不了它。 **实现欠打磨。** 有状态 JWT 本质上就是 Session Cookie 的劣化版——功能一样,却少了 express-session 这类久经沙场、反复修漏洞的实现。自己撸一套,很容易踩坑。 JWT 规范本身也饱受安全圈质疑:早期甚至允许 `alg: none` 伪造 Token,整体设计面向的是**极短寿命**(大约 5 分钟以内)的凭证,不是长登录态。 ## 那 JWT 适合什么? 适合**一次性、短时效的授权凭证**,而不是持久会话。典型场景: - 文件下载:应用服务器验证登录后发一个几分钟有效的下载 Token,无状态的下载服务器验签后直接吐文件 - 单点登录中转:Google 浏览器里用的是普通 Cookie Session;JWT 只在跨服务传递「刚登录成功」这一下,随后各产品各自建 Session - 微服务内部:网关验完 Session 后,用短寿命 Token 在内部服务间传递身份,避免每个服务都查一遍 Session 库——但即便这里,内网 API Key + 明文传用户 ID 往往也够,未必非 JWT 不可 好的 JWT 用法有几个共同点:**短命、单次或极少次使用、应用本身仍用 Session 管登录。** ## 该怎么做 直接用框架自带的 Session。对于 Javaer 来说 SpringBoot 封装的非常好,引入几个依赖写几行代码就搞定了,如果想要保存到 Redis 也是引入一个依赖的事情,非常方便! ## 总结 Cookie 是旧技术,但旧不等于差。安全领域,**久经考验、没被打穿**,往往比「新且未经充分检验」可靠得多。 Session 和 JWT 可以并存,各干各的——只是别把 JWT 当长期登录凭证。 用户登录这件事,交给 Session 就好。
javawebの会话跟踪方案
Cookie ====== 什么是Cookie? ---------- * **概念**:存储在**用户浏览器端**的一个小型数据文件,用于**跟踪和保存用户的状态信息** * **用处**:主要用于保持用户登录状态、跟踪用户行为、存储用户偏好等 * **存储在浏览器端** **优点**: `HTTP`协议中支持的技术 **缺点**: * 移动端无法使用 * 不安全,用户可以自己禁用`Cookie` * Cookie 不能跨域 跨域区分三个维度:协议、IP/域名、端口 关于 Cookie ---------  每个域下面都有各自的 Cookie,访问不同的网站带属于这个网站的Cookie,不会带别人的 Cookie,否则就会乱套了,前后端也是一样,浏览器会发送请求并且该请求携带`Cookie`到前端,而这个`Cookie`如果拿去访问后端就不可行,因为`Cookie`不能跨域 问题:但是 Cookie 是**明文存储在用户本地,而且带有大量的用户信息**,这不太安全 相关代码实践 ------ ```java // 设置 Cookie @GetMapping("/c1") public BaseResponse cookie1(HttpServletResponse response) { response.addCookie(new Cookie("loginUser", "lantz")); return ResultUtils.success("ok"); } // 获取 Cookie @GetMapping("/c2") public BaseResponse cookie2(HttpServletRequest request) { Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if (cookie.getName().equals("loginUser")) { log.info("loginUser value=" + cookie.getValue()); } } return ResultUtils.success("ok"); } ``` 在测试`c2`接口的时候可以看到debug窗口会返回一条数据: ```java 2025-10-12 18:23:29.206 INFO 37868 --- [nio-8080-exec-5] c.l.l.controller.SessionController : loginUser value=lantz ``` 说明已经成功获取`Cookie`了 Session ======= 什么是 Session? ------------ * **概念**:**服务器端**保存用户状态的机制,每个用户会话都有一个唯一的 `SessionID`(`JSESSIONID`) * **用处**:主要**用于跟踪用户在服务器上的状态信息**,例如登录状态和购物车内容 * **存储在服务器端**,然后对应的 Session ID 通过 Cookie 保存在客户端浏览器中  ### 关于 Session `Session`就解决了 Cookie 的这个问题:Cookie 是**明文存储在用户本地,而且带有大量的用户信息**,不太安全 Session 就是把用户的会话信息存储在服务端,然后颁发给客户端一个 sessionId,让客户端之后带着 sessionId 来请求。这样服务端就可以通过 sessionId 去找到这个用户的信息,从而识别请求。 **那么客户端是如何带上 sessionId 的?** 这个 sessionId 还是按照 Cookie 的形式存储在用户的本地,发起请求的时候带上即可。 演示代码 ---- ### 设置Session ```java // 设置 Session @GetMapping("/s1") public BaseResponse session1(HttpSession session) { log.info("HttpSession-s1: {}", session.hashCode()); session.setAttribute("loginUser", "lantz"); return ResultUtils.success("ok"); } ``` 测试`s1`设置`session`并且输出当前`Session`的哈希码 如下所示: ``` HttpSession-s1: 2054650805 ``` 返回的`Cookie`  可见:在发送`Session`请求的时候,浏览器会为当前的请求自动生成一个哈希码,用作`JSESSIONID`,并且会将这个`JSESSIONID`加到`Cookie`上,一并返回给服务端 ### 获取Session ```java // 获取 Session @GetMapping("/s2") public BaseResponse session2(HttpServletRequest request) { HttpSession session = request.getSession(); log.info("HttpSession-s2: {}", session.hashCode()); Object loginUser = session.getAttribute("loginUser"); log.info("loginUser: {}", loginUser); return ResultUtils.success("ok"); } ``` 测试`s2`获取到当前`Session`的哈希码 如下所示: ```bash 2025-10-12 18:40:43.562 INFO 16300 --- [nio-8080-exec-4] c.l.l.controller.SessionController : HttpSession-s2: 2054650805 2025-10-12 18:40:43.562 INFO 16300 --- [nio-8080-exec-4] c.l.l.controller.SessionController : loginUser: lantz ``` `Cookie` 展示:  Token ===== 什么是`Token`? ----------- * **概念**:本质是一个**加密的字符串**,用于**身份验证和授权**,可以包含用户信息和权限,用于验证用户身份或授权访问资源。 * 认证后,后端服务会返回 Token,**存储在客户端**(浏览器或移动应用中),后续客户端访问服务端需要带上这个 Token 其实只需要一个能**代表身份的凭证**即可,一个服务端颁发给用户的凭证,之后的请求让用户带着这个凭证就行。 就像我们的身份证代表着我们,里面包含了我们的信息 但是担心凭证被伪造怎么办?可以把凭证给签名了,这样服务器就可验证凭证的真伪了。 这个凭证就叫 `**Token**` 如果一个用户登陆了系统,就返回一个 Token 给他,之后每次请求他带这个 Token 来就行。服务器验证了真伪之后拿到 Token 里面的用户信息就知道这个请求是谁发的了。 **Token 简单地说就是一个含有凭证信息的令牌**,只要服务器能识别这个令牌就能根据令牌的身份进行相应的相应。 jwt --- 全称: JSON Web Token([https://jwt.io/)](https://jwt.io/%EF%BC%89) 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。 组成: * 第一部分:Header(头),记录令牌类型、签名算法等。例如:{"alg":"HS256","type":"JWT"} * 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"} * 第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。 ### 代码演示 #### 生成令牌 ```java @Test public void getjwt(){ Map<String, Object> claims = new HashMap<>(); claims.put("id", 123); claims.put("username", "lantz"); String jwt = Jwts.builder() .setClaims(claims) // 自定义载荷 .signWith(SignatureAlgorithm.HS256, "lanuc") // 签名算法 .setExpiration(new Date(System.currentTimeMillis() + 3600 * 100)) // 设置过期时间 .compact(); System.out.println(jwt); } ``` 生成令牌如下: ``` eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTIzLCJleHAiOjE3NjAyNjc5NDQsInVzZXJuYW1lIjoibGFudHoifQ.9RYwpzNji9kfg33ge1qQUDFq_2vQdO__mr1Eze-IXlc ``` ### 解析令牌 ```java @Test public void deCode(){ Claims body = Jwts.parser() .setSigningKey("lanuc") // 指定签名秘钥 .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MTIzLCJleHAiOjE3NjAyNjc5NDQsInVzZXJuYW1lIjoibGFudHoifQ.9RYwpzNji9kfg33ge1qQUDFq_2vQdO__mr1Eze-IXlc")//解析令牌 .getBody(); System.out.println(body); } ``` 生成结果: ```json {id=123, exp=1760267944, username=lantz} ``` SpringBoot 实现 ------------- ### JwtUtils jwt工具类(`JwtUtils`)-- 生成令牌,解析令牌 #### 生成令牌 ```java /** * 生成令牌 * @param claims JWT 第二部分负载 payload 中存储的内容 * @return */ public static String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } ``` ### 解析令牌 ```java /** * 解析JWT Token * @param token jwt 令牌 * @return */ public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } ``` ### 用户登录 需要先获取用户登录信息,然后才能根据用户信息生成令牌 ```java @PostMapping("/login") public BaseResponse<LoginResponse> userLogin(@RequestBody UseLoginRequest useLoginRequest, HttpServletRequest request){ if (useLoginRequest == null){ return null; } String userAccount = useLoginRequest.getUserAccount(); String userPassword = useLoginRequest.getUserPassword(); if (StringUtils.isAnyBlank(userAccount, userPassword)) { return null; } User user = userService.userLogin(userAccount, userPassword, request); if (user == null) { return ResultUtils.error(ErrorCode.PARAMS_ERROR, "登录失败"); } // 获取登录用户 User loginUer = userService.getLoginUer(request); // 生成令牌,下发令牌 Map<String, Object> claims = new HashMap<>(); claims.put("id", loginUer.getId()); claims.put("userAccount", loginUer.getUserAccount()); claims.put("userName", loginUer.getUserName()); String jwt = JwtUtils.generateToken(claims); // 生成令牌 // 创建登录响应 LoginResponse loginResponse = new LoginResponse(); loginResponse.setUser(user); loginResponse.setJwt(jwt); return ResultUtils.success(loginResponse); } ``` 测试结果: ```json { "code": 0, "data": { "user": { "id": 4, "userAccount": "Lantz", "userName": "lan", "userAvatar": "https://pic.code-nav.cn/user_avatar/1872161376428277761/thumbnail/D5QaPIaHNcft2xW1.jpg", "gender": 1, "userPassword": "dd8fa48dc3e51a450c6141f5c0ad6665", "phone": "12335555", "email": "22334466@email.com", "userStatus": 0, "createTime": "2025-04-18T10:43:38.000+00:00", "updateTime": null, "isDelete": null, "userRole": 1 }, "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyQWNjb3VudCI6IkxhbnR6IiwiaWQiOjQsInVzZXJOYW1lIjoibGFuIiwiZXhwIjoxNzYwMjczMzMxfQ.eWcD38s2pfmYin78IssVy2tqeq0SA5CWtAjbq5CKto0" }, "message": "ok" } ```
OJ项目中引入JWT
准备工作 ---- #### 添加pom依赖 ```xml <!-- JWT依赖 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> ``` #### 添加配置接口 ```java /** * @version v1.0.0 * @belongsProject: fengoj-backend-microservice * @belongsPackage: com.feng.fengojbackenduserservice.config * @author: fgh * @description: JWT配置类 * @createTime: 2025-03-14 23:53 */ public interface JwtConfig { /** * JWT密钥 */ String JWT_SECRET = "fengoj-backend-user-service-jwt-secret-key"; /** * JWT过期时间(1天,单位:毫秒) */ long JWT_EXPIRATION = 1 * 24 * 60 * 60 * 1000L; /** * JWT令牌前缀 */ String JWT_TOKEN_PREFIX = "Bearer "; /** * JWT请求头 */ String JWT_HEADER = "Authorization"; /** * 创建JWT密钥 */ static SecretKey createSecretKey() { return Keys.hmacShaKeyFor(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); } } ``` #### 实现这个接口(那个模块中要使用就在那个模块中实现) ```plain import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.crypto.SecretKey; /** *@belongsProject: fengoj-backend-microservice *@belongsPackage: com.feng.fengojbackendgateway.config *@author: fgh *@description: *@version v1.0.0 *@createTime: 2025-03-15 12:46 */ @Configuration public class JwtConfig implements com.feng.fengojbackendcommon.config.JwtConfig { /** * 创建JWT密钥 */ @Bean public SecretKey secretKey() { return com.feng.fengojbackendcommon.config.JwtConfig.createSecretKey(); } } ``` 添加JWT工具类 ```java import com.feng.fengojbackendcommon.config.JwtConfig; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.SecretKey; import java.util.Date; import java.util.Map; /** * @version v1.0.0 * @belongsProject: fengoj-backend-microservice * @belongsPackage: com.feng.fengojbackendcommon.utils * @author: fgh * @description: JWT工具类,用于生成和解析JWT令牌 * @createTime: 2025-03-15 12:53 */ public class JwtUtils { /** * 生成JWT令牌 * * @param claims 用户信息 * @param secretKey JWT密钥 * @return JWT令牌 */ public static String generateToken(Map<String, Object> claims, String subject, SecretKey secretKey) { return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + JwtConfig.JWT_EXPIRATION)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } /** * 从JWT令牌中获取用户ID * * @param token JWT令牌 * @param secretKey JWT密钥 * @return 用户ID */ public static Long getUserIdFromToken(String token, SecretKey secretKey) { Claims claims = getClaimsFromToken(token, secretKey); return Long.valueOf(claims.get("id").toString()); } /** * 从JWT令牌中获取用户账号 * * @param token JWT令牌 * @param secretKey JWT密钥 * @return 用户账号 */ public static String getUserAccountFromToken(String token, SecretKey secretKey) { Claims claims = getClaimsFromToken(token, secretKey); return claims.get("userAccount").toString(); } /** * 从JWT令牌中获取用户角色 * * @param token JWT令牌 * @param secretKey JWT密钥 * @return 用户角色 */ public static String getUserRoleFromToken(String token, SecretKey secretKey) { Claims claims = getClaimsFromToken(token, secretKey); return claims.get("userRole").toString(); } /** * 从JWT令牌中获取指定键的值 * * @param token JWT令牌 * @param secretKey JWT密钥 * @param key 键 * @return 值 */ public static Object getClaimFromToken(String token, SecretKey secretKey, String key) { Claims claims = getClaimsFromToken(token, secretKey); return claims.get(key); } /** * 验证JWT令牌是否有效 * * @param token JWT令牌 * @param secretKey JWT密钥 * @return 是否有效 */ public static boolean validateToken(String token, SecretKey secretKey) { try { Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); return true; } catch (Exception e) { return false; } } /** * 从JWT令牌中获取Claims * * @param token JWT令牌 * @param secretKey JWT密钥 * @return Claims */ private static Claims getClaimsFromToken(String token, SecretKey secretKey) { return Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); } } ``` 实现方法一 ----- #### 在网管拦截器中添加JWT逻辑 ```java import com.feng.fengojbackendcommon.common.ErrorCode; import com.feng.fengojbackendcommon.config.JwtConfig; import com.feng.fengojbackendcommon.utils.JwtUtils; import com.feng.fengojbackendmodel.model.entity.User; import org.apache.commons.lang3.StringUtils; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.Resource; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; @Component public class GlobalAuthFilter implements GlobalFilter, Ordered { private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Resource private SecretKey secretKey; // 不需要验证token的路径 private static final String[] WHITE_LIST = { "/api/user/login", "/api/user/get/login", "/api/user/register", "/api/user/logout", "/api/doc.html", "/api/v3/api-docs", "/api/v2/api-docs", "/api/swagger-resources", "/api/swagger-ui.html", "/api/webjars/**" }; /** * 判断是否为白名单路径 * @param path 请求路径 * @return 是否在白名单中 */ private boolean isWhiteListPath(String path) { for (String whitePath : WHITE_LIST) { if (antPathMatcher.match(whitePath, path)) { return true; } } return false; } /** * 处理无权限的情况 * 当用户请求需要权限但未提供有效凭证时调用此方法 * @param response 响应对象,用于向客户端发送响应 * @param code 错误码,表示具体的错误类型 * @param message 错误信息,描述错误的详细信息 * @return Mono<Void> 表示异步操作的完成 */ private Mono<Void> handleNoAuth(ServerHttpResponse response, int code, String message) { // 设置响应状态码为未授权 response.setStatusCode(HttpStatus.UNAUTHORIZED); // 设置响应头,指定内容类型和字符集 response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); // 创建DataBufferFactory,用于生成数据缓冲区 DataBufferFactory dataBufferFactory = response.bufferFactory(); // 构建JSON响应体,包含错误码和错误信息 String json = String.format("{\"code\":%d,\"data\":null,\"message\":\"%s\"}", code, message); // 将JSON响应体包装到数据缓冲区中 DataBuffer dataBuffer = dataBufferFactory.wrap(json.getBytes(StandardCharsets.UTF_8)); // 使用Mono将数据缓冲区写入响应中 return response.writeWith(Mono.just(dataBuffer)); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest serverHttpRequest = exchange.getRequest(); String path = serverHttpRequest.getURI().getPath(); // 判断路径中是否包含 inner,只允许内部调用 if (antPathMatcher.match("/**/inner/**", path)) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.FORBIDDEN); DataBufferFactory dataBufferFactory = response.bufferFactory(); DataBuffer dataBuffer = dataBufferFactory.wrap("无权限".getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(dataBuffer)); } // todo 统一权限校验,通过 JWT 获取登录用户信息 // return chain.filter(exchange); // 白名单路径直接放行 if (isWhiteListPath(path)) { return chain.filter(exchange); } // 统一权限校验,通过 JWT 获取登录用户信息 String token = serverHttpRequest.getHeaders().getFirst(JwtConfig.JWT_HEADER); // 如果请求头中没有token if (StringUtils.isBlank(token)) { return handleNoAuth(exchange.getResponse(), ErrorCode.NOT_LOGIN_ERROR.getCode(), ErrorCode.NOT_LOGIN_ERROR.getMessage()); } // 如果token以Bearer 开头,则去掉前缀 if (token.startsWith(JwtConfig.JWT_TOKEN_PREFIX)) { token = token.substring(JwtConfig.JWT_TOKEN_PREFIX.length()); } // 验证token if (!JwtUtils.validateToken(token, secretKey)) { return handleNoAuth(exchange.getResponse(), ErrorCode.NOT_LOGIN_ERROR.getCode(), "Token无效或已过期"); } try { // 从token中获取用户信息 Long userId = JwtUtils.getUserIdFromToken(token, secretKey); String userAccount = JwtUtils.getUserAccountFromToken(token, secretKey); String userRole = JwtUtils.getUserRoleFromToken(token, secretKey); // 将用户信息添加到请求头中,传递给下游服务 ServerHttpRequest mutableRequest = serverHttpRequest.mutate() .header("userId", String.valueOf(userId)) .header("userAccount", userAccount) .header("userRole", userRole) .build(); // 使用修改后的请求继续过滤器链 return chain.filter(exchange.mutate().request(mutableRequest).build()); } catch (Exception e) { return handleNoAuth(exchange.getResponse(), ErrorCode.SYSTEM_ERROR.getCode(), "Token解析失败"); } } /** * 优先级提到最高 * @return */ @Override public int getOrder() { return 0; } } ``` #### 使用 ```java default User getLoginUser(HttpServletRequest request) { // 先判断是否已登录 // Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); // User currentUser = (User) userObj; // 从请求头中获取用户信息 String userIdStr = request.getHeader("userId"); String userAccount = request.getHeader("userAccount"); String userRole = request.getHeader("userRole"); if (userIdStr == null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } try { // 构建用户对象 User currentUser = new User(); currentUser.setId(Long.parseLong(userIdStr)); currentUser.setUserAccount(userAccount); currentUser.setUserRole(userRole); return currentUser; } catch (NumberFormatException e) { throw new BusinessException(ErrorCode.SYSTEM_ERROR, "用户信息格式错误"); } } ``` 实现方法二 ----- #### 添加拦截器配置 ```java import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; /** * @version v1.0.0 * @belongsProject: fengoj-backend-microservice * @belongsPackage: com.feng.fengojbackenduserservice.config * @author: fgh * @description: Web配置类 * @createTime: 2025-03-15 13:58 */ @Configuration public class WebConfig implements WebMvcConfigurer { @Resource private JwtAuthInterceptor jwtAuthInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 注册JWT认证拦截器 registry.addInterceptor(jwtAuthInterceptor) // 拦截所有请求 .addPathPatterns("/**") // 排除不需要拦截的路径 .excludePathPatterns( "/login", "/register", "/error", "/swagger-resources/**", "/swagger-ui/**", "/v2/api-docs", "/doc.html" ); } } ``` #### 实现拦截器 ```java import com.feng.fengojbackendcommon.common.ErrorCode; import com.feng.fengojbackendcommon.exception.BusinessException; import com.feng.fengojbackendcommon.utils.JwtUtils; import com.feng.fengojbackendmodel.model.entity.User; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.annotation.Resource; import javax.crypto.SecretKey; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @version v1.0.0 * @belongsProject: fengoj-backend-microservice * @belongsPackage: com.feng.fengojbackenduserservice.filter * @author: fgh * @description: JWT认证拦截器 * @createTime: 2025-03-15 13:44 */ @Component @Slf4j public class JwtAuthInterceptor implements HandlerInterceptor { @Resource private SecretKey secretKey; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 检查是否是内部调用 String internalCall = request.getHeader("Internal-Call"); if ("true".equals(internalCall)) { // 内部调用,跳过JWT认证 log.info("内部服务调用,跳过JWT认证"); return true; } // 从请求头中获取token String token = request.getHeader(JwtConfig.JWT_HEADER); // 如果请求头中没有token,尝试从请求参数中获取 if (StringUtils.isBlank(token)) { token = request.getParameter("token"); } // 如果token为空,则未登录 if (StringUtils.isBlank(token)) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } // 如果token以Bearer 开头,则去掉前缀 if (token.startsWith(JwtConfig.JWT_TOKEN_PREFIX)) { token = token.substring(JwtConfig.JWT_TOKEN_PREFIX.length()); } // 验证token if (!JwtUtils.validateToken(token, secretKey)) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR, "token无效或已过期"); } // 从token中获取用户信息 Long userId = JwtUtils.getUserIdFromToken(token, secretKey); String userAccount = JwtUtils.getUserAccountFromToken(token, secretKey); String userRole = JwtUtils.getUserRoleFromToken(token, secretKey); // 构建用户对象并存入请求属性中,供后续使用 User user = new User(); user.setId(userId); user.setUserAccount(userAccount); user.setUserRole(userRole); request.setAttribute("loginUser", user); return true; } } ``` #### 使用 ```java /** * 获取当前登录用户 * * @param request * @return */ @Override public User getLoginUser(HttpServletRequest request) { // 先判断是否已登录 // Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE); // User currentUser = (User) userObj; // 从请求属性中获取用户信息(由JWT拦截器设置) User currentUser = (User) request.getAttribute("loginUser"); if (currentUser == null || currentUser.getId() == null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } // 从数据库查询(追求性能的话可以注释,直接走缓存) // long userId = currentUser.getId(); // currentUser = this.getById(userId); // 从数据库查询完整用户信息 currentUser = this.getById(currentUser.getId()); if (currentUser == null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } return currentUser; } ``` 两种方法优劣对比 -------- | 方案 | 使用场景 | 优势 | 局限性 | | --- | --- | --- | --- | | Header | 跨服务传递 | 可传递到下游服务 | 安全性低,占用宽带 Attribute | 服务内部处理 | 安全,无需暴露敏感信息 | 仅限当前服务 |
学习了JWT相关的知识 分享给鱼友们
今天学习 JWT 时遇到一个坑:生成的 token 设置了过期时间却没有过期
<html> <head></head> <body> <div class="content ql-editor"> <p><strong>遇到的问题:</strong>生成的 token 设置了过期时间却没有过期 <strong><br></strong></p> <p><strong>产生问题的原因:</strong>setExpiration 方法 和 setClaims 方法调用顺序的问题</p> <p><strong><br></strong></p> <p><strong>原因分析:</strong></p> <p><strong><br></strong></p> <p>查看 setExpiration 方法的源码,当我们设置了过期时间时 exp != null ,这时会走到 ensureClaims().setExpiration(exp);</p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-meta">@Override</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">public</span> JwtBuilder <span class="ql-token hljs-title">setExpiration(Date exp)</span> { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">if</span> (exp != <span class="ql-token hljs-literal">null</span>) { </div> <div class="ql-code-block"> ensureClaims().setExpiration(exp); </div> <div class="ql-code-block"> } <span class="ql-token hljs-keyword">else</span> { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">if</span> (<span class="ql-token hljs-built_in">this</span>.claims != <span class="ql-token hljs-literal">null</span>) { </div> <div class="ql-code-block"> <span class="ql-token hljs-comment">//noinspection ConstantConditions</span> </div> <div class="ql-code-block"> <span class="ql-token hljs-built_in">this</span>.claims.setExpiration(exp); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> <span class="ql-token hljs-built_in">this</span>; </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p>然后再查看 ensureClaims 方法的源码可以发现,如果我们没有事先设置 claims ,那么在这里会产生一个 DefaultClaims 对象赋值给 DefaultJwtBuilder 类的 claims 属性,这个对象将调用 setExpiration(exp) 方法设置过期时间</p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-keyword">protected</span> Claims <span class="ql-token hljs-title">ensureClaims()</span> { </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">if</span> (<span class="ql-token hljs-built_in">this</span>.claims == <span class="ql-token hljs-literal">null</span>) { </div> <div class="ql-code-block"> <span class="ql-token hljs-built_in">this</span>.claims = <span class="ql-token hljs-keyword">new</span> <span class="ql-token hljs-title">DefaultClaims</span>(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> <span class="ql-token hljs-built_in">this</span>.claims; </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p>但如果在这之后我们再调用 setClaims 方法,那么我们新设置的 claims 将重新赋值到 DefaultJwtBuilder 类的 claims 属性</p> <div class="ql-code-block-container"> <div class="ql-code-block"> <span class="ql-token hljs-meta">@Override</span> </div> <div class="ql-code-block"><span class="ql-token hljs-keyword">public</span> JwtBuilder <span class="ql-token hljs-title">setClaims(Map<String, ?> claims)</span> { </div> <div class="ql-code-block"> <span class="ql-token hljs-built_in">this</span>.claims = <span class="ql-token hljs-keyword">new</span> <span class="ql-token hljs-title">DefaultClaims</span>(claims); </div> <div class="ql-code-block"> <span class="ql-token hljs-keyword">return</span> <span class="ql-token hljs-built_in">this</span>; </div> <div class="ql-code-block"> } </div> </div> <p>此时新构造出来的 DefaultClaims 对象没有进行设置过期时间的操作,这也就导致了最后生成的 token 没有过期时间</p> <p><br></p> <p><strong>解决方法:</strong>应该先调用 setClaims 方法再调用 setExpiration 方法</p> </div> </body> </html>
