Spring Boot 中JWT登录授权无感刷新看这篇就够了一、引言在当今的分布式系统和前后端分离架构盛行的时代传统的基于 Session 的认证方式逐渐暴露出诸多弊端。想象一下在一个大型电商系统中用户的操作频繁涉及多个服务模块且前端可能是网页端、移动端等多种类型。若采用传统 Session 认证当用户从网页端切换到移动端继续操作时由于跨域问题Session 信息难以有效传递 导致用户需要重新登录。同时随着用户数量的急剧增加服务器需要存储大量的 Session 信息这无疑给服务器带来了沉重的存储压力就像一间小仓库要存放海量的货物空间迟早会被耗尽。而 JWTJSON Web Token的出现犹如一道曙光为这些问题提供了完美的解决方案。JWT 是一种轻量级的身份认证与授权方案具有无状态的特性这意味着服务器无需存储用户的会话信息大大减轻了服务器的负担如同给服务器卸下了沉重的包袱。它在跨域场景下表现出色能够轻松地在不同的前端应用和后端服务之间传递为前后端分离架构的发展提供了有力支持。并且JWT 易于扩展方便与各种系统集成无论是小型项目还是大型企业级应用都能发挥其优势。本文将详细地为大家讲解 Spring Boot 整合 JWT 实现登录认证与接口授权的全流程从最基础的环境搭建到核心功能的实现再到进阶优化每一步都有详细的代码示例和解释让你轻松掌握这一关键技术为你的项目开发保驾护航。二、JWT 基础扫盲2.1 JWT 是什么JWT即 JSON Web Token是一种基于 JSON 的开放标准RFC 7519 用于在网络应用间安全地传输声明。简单来说它是一种轻量级的身份认证和授权方案以 JSON 格式组织和传输信息。相较于传统的认证方式JWT 具有无状态、自包含的特性这意味着服务器无需存储用户的会话信息减轻了服务器的负载同时也方便在不同的服务和系统之间传递身份验证信息就像一个小巧且功能强大的通行证在分布式系统和前后端分离的架构中被广泛应用。2.2 JWT 结构剖析JWT 看起来是一个很长的字符串实际上它由三部分组成每部分之间用英文句点 “.” 分隔即 Header.Payload.Signature。Header头部主要存储两方面信息一是令牌的类型通常就是 “JWT”二是签名算法常见的如 HMAC SHA256、RSA 等 。例如{ alg: HS256, typ: JWT }将这个 JSON 对象进行 Base64Url 编码后就成为了 JWT 的第一部分。Payload载荷存放实际需要传递的声明claims信息。这些声明可以分为三类已注册声明如 iss 签发者、exp 过期时间、iat 签发时间等、公共声明开发者自定义的公开信息像用户 ID、角色等、私有声明应用内自定义的非公开信息。例如{ sub: 1234567890, name: John Doe, admin: true, iat: 1516239022, exp: 1516239022 3600 // 假设有效期1小时 }同样将这个 JSON 对象 Base64Url 编码后构成 JWT 的第二部分。需要注意的是Payload 默认不加密不要存放敏感信息如密码等。Signature签名用于验证 JWT 的完整性确保内容未被篡改。生成签名需要用到编码后的 Header、编码后的 Payload、一个只有服务器知道的密钥secret以及 Header 中指定的签名算法。以 HS256 算法为例签名生成公式为HMACSHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret )计算出的签名作为 JWT 的第三部分与前两部分共同组成完整的 JWT如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c。2.3 认证流程详解用户登录用户在客户端如浏览器、移动应用输入用户名和密码向服务器发起登录请求。服务器验证服务器接收到登录请求后验证用户名和密码是否正确。若验证通过根据用户信息生成 JWT。这个过程就像是服务器给用户颁发了一张通行证通行证里包含了用户的关键信息如用户 ID、角色等并使用密钥和特定算法进行了签名。返回 JWT服务器将生成的 JWT 返回给客户端。客户端接收到 JWT 后可以将其存储在本地常见的存储方式有 localStorage、sessionStorage 或者 HttpOnly Cookie 等 。后续请求在后续的每一次请求中客户端都会将 JWT 携带在 HTTP 请求头中一般是放在 Authorization 字段格式为Authorization: Bearer JWT。Bearer 表示认证方案告诉服务器使用 JWT 进行认证。服务器验证服务器接收到请求后从请求头中提取 JWT然后使用相同的密钥和签名算法对 JWT 进行验证。验证内容包括签名是否正确、JWT 是否过期等。如果验证通过服务器就认为该请求是合法的并且可以从 JWT 的 Payload 中获取用户相关信息从而进行相应的授权操作返回请求的资源若验证失败则返回 401 Unauthorized 错误拒绝访问 。三、Spring Boot 环境搭建3.1 核心依赖引入在 Spring Boot 项目中首先要引入关键依赖为后续使用 JWT 进行登录授权奠定基础。这些依赖就像是搭建房屋的基石缺一不可。我们需要添加 Spring Security它是 Spring 生态中提供强大认证授权框架的组件能为应用程序保驾护航确保只有合法用户才能访问特定资源就像小区门口严格的保安阻挡外来人员随意进入JJWT 是 Java 领域主流的 JWT 工具库专门用于生成、解析和验证 JWT为我们处理 JWT 相关操作提供了便捷的方法Spring Web 则用于编写接口方便我们测试整个认证流程让我们能直观地看到认证授权在实际接口调用中的效果。如果你的项目使用 Maven 构建在pom.xml文件中添加以下依赖配置dependencies !-- Spring Security依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- JJWT依赖 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.2/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.2/version scoperuntime/scope /dependency !-- Spring Web依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies3.2 配置文件设置配置文件在整个项目中起着至关重要的作用它就像是项目的 “指挥中心”通过设置各种参数我们可以灵活地调整项目的运行方式。在application.properties或application.yml文件中需要配置 JWT 的一些关键参数。比如设置 JWT 的密钥jwt.secret这个密钥是生成和验证 JWT 签名的关键务必妥善保管就像保管自己家门的钥匙一样重要一旦泄露整个认证体系将面临被攻破的风险设置访问令牌过期时间jwt.access-token-expiration根据业务需求合理设定访问令牌的有效时长比如可以设置为 30 分钟这样既能保证一定的安全性又不会频繁让用户重新登录还有刷新令牌过期时间jwt.refresh-token-expiration通常刷新令牌的过期时间会比访问令牌长很多比如设置为 7 天用于在访问令牌过期时无需用户重新输入用户名和密码就能无感刷新获取新的访问令牌。以application.yml为例配置如下jwt: secret: your-secret-key access-token-expiration: 1800000 # 30分钟单位毫秒 refresh-token-expiration: 604800000 # 7天单位毫秒通过上述环境搭建步骤我们的 Spring Boot 项目已经具备了使用 JWT 进行登录授权的基本条件接下来就可以着手实现核心的登录认证和接口授权功能了。四、核心功能实现4.1 JWT 工具类封装在 Spring Boot 项目中为了方便地处理 JWT 相关操作我们需要将常用的 JWT 操作封装成一个工具类就像把各种工具整理到一个工具箱里使用时随手可拿。这里创建一个JwtUtils类利用 Spring 的依赖注入机制将 JWT 的密钥和过期时间等配置信息注入到类中这样我们就能灵活地根据配置生成和验证 JWT而无需在代码中硬编码这些关键信息。首先在类中使用Component注解将JwtUtils标记为 Spring 组件这样 Spring 容器在启动时会自动扫描并实例化这个类使其成为容器管理的 Bean方便在其他组件中通过依赖注入的方式使用。接着通过Value注解从配置文件中读取 JWT 的密钥jwt.secret、访问令牌过期时间jwt.access-token-expiration和刷新令牌过期时间jwt.refresh-token-expiration 并将这些值赋给相应的成员变量。下面是JwtUtils类中几个关键方法的实现及解释import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.access-token-expiration}) private long accessTokenExpiration; Value(${jwt.refresh-token-expiration}) private long refreshTokenExpiration; /** * 生成访问令牌 * param username 用户名 * return 生成的访问令牌 */ public String generateAccessToken(String username) { MapString, Object claims new HashMap(); return generateToken(claims, username, accessTokenExpiration); } /** * 生成刷新令牌 * param username 用户名 * return 生成的刷新令牌 */ public String generateRefreshToken(String username) { MapString, Object claims new HashMap(); return generateToken(claims, username, refreshTokenExpiration); } /** * 生成JWT令牌 * param claims 负载信息 * param subject 主题一般为用户名 * param expireTime 过期时间毫秒 * return 生成的JWT令牌 */ private String generateToken(MapString, Object claims, String subject, long expireTime) { Key key Keys.hmacShaKeyFor(secret.getBytes()); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireTime)) .signWith(key, SignatureAlgorithm.HS256) .compact(); } /** * 从令牌中获取用户名 * param token JWT令牌 * return 用户名 */ public String getUsernameFromToken(String token) { Claims claims getClaimsFromToken(token); return claims.getSubject(); } /** * 验证令牌是否有效 * param token JWT令牌 * return 是否有效 */ public boolean validateToken(String token) { try { Key key Keys.hmacShaKeyFor(secret.getBytes()); Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (Exception e) { return false; } } /** * 判断令牌是否过期 * param token JWT令牌 * return 是否过期 */ public boolean isTokenExpired(String token) { Claims claims getClaimsFromToken(token); Date expiration claims.getExpiration(); return expiration.before(new Date()); } /** * 从令牌中获取负载信息 * param token JWT令牌 * return 负载信息 */ private Claims getClaimsFromToken(String token) { Key key Keys.hmacShaKeyFor(secret.getBytes()); return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); } }生成访问令牌generateAccessToken该方法接收用户名作为参数创建一个空的claims载荷Map然后调用generateToken方法传入claims、用户名和访问令牌过期时间生成访问令牌。这里空的claims可以在后续根据业务需求添加更多用户相关信息比如用户角色、用户 ID 等 使令牌携带更丰富的用户身份信息。生成刷新令牌generateRefreshToken与生成访问令牌类似只是传入的过期时间是刷新令牌的过期时间用于生成长期有效的刷新令牌以便在访问令牌过期时用户无需重新输入用户名和密码就能获取新的访问令牌提升用户体验。生成 JWT 令牌generateToken这是一个私有的核心方法用于实际生成 JWT 令牌。它接收claims载荷信息、subject主题通常是用户名和expireTime过期时间单位毫秒作为参数。首先根据密钥生成一个Key对象然后使用Jwts.builder构建 JWT。依次设置claims、subject、签发时间当前时间、过期时间并使用指定的SignatureAlgorithm.HS256算法和密钥进行签名最后调用compact方法生成紧凑的 JWT 字符串 。从令牌中获取用户名getUsernameFromToken该方法从 JWT 令牌中提取出用户名。首先调用getClaimsFromToken方法获取令牌的claims载荷然后通过claims.getSubject()获取主题即用户名这样在验证令牌后我们就能方便地获取到令牌对应的用户身份。验证令牌是否有效validateToken尝试使用密钥和签名算法解析 JWT 令牌如果解析成功说明令牌有效返回true如果在解析过程中出现异常如签名验证失败、令牌格式错误、令牌已过期等说明令牌无效返回false确保只有合法有效的令牌才能通过验证保障系统安全。判断令牌是否过期isTokenExpired从令牌中获取claims载荷提取其中的过期时间expiration然后与当前时间进行比较如果过期时间早于当前时间说明令牌已过期返回true否则返回false这在处理令牌过期逻辑时非常重要比如决定是否需要刷新令牌。从令牌中获取负载信息getClaimsFromToken使用密钥和签名算法解析 JWT 令牌返回解析后的claims载荷其中包含了用户相关的各种信息如用户名、角色、过期时间等为后续根据令牌获取用户信息提供了基础。通过以上封装JwtUtils类提供了一套完整且便捷的 JWT 操作方法在整个项目中无论是生成令牌、验证令牌还是从令牌中提取信息都可以通过调用这个工具类的方法轻松实现大大提高了代码的复用性和可维护性 。4.2 实现认证过滤器在 Spring Boot 项目中为了对每个请求进行 JWT 认证我们需要创建一个认证过滤器。这个过滤器就像是一个严格的保安站在请求进入系统的入口对每个请求进行检查只有持有合法 JWT 令牌的请求才能放行进入系统。这里创建一个JwtAuthenticationFilter类让它继承OncePerRequestFilterOncePerRequestFilter保证每个请求只会被过滤一次避免重复过滤带来的性能损耗。在这个类中注入JwtUtils工具类用于验证 JWT 令牌的有效性同时注入UserDetailsService以便在验证令牌通过后获取用户的详细信息进行后续的授权操作。下面是JwtAuthenticationFilter类的核心实现及逻辑解释import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtUtils jwtUtils; Autowired private UserDetailsService userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 跳过登录和刷新令牌的接口 String requestURI request.getRequestURI(); if (requestURI.equals(/api/auth/login) || requestURI.equals(/api/auth/refresh)) { chain.doFilter(request, response); return; } // 从请求头获取token String token getTokenFromRequest(request); if (token ! null jwtUtils.validateToken(token)) { // 从token中获取用户名 String username jwtUtils.getUsernameFromToken(token); // 从UserDetailsService中获取用户详细信息 UserDetails userDetails userDetailsService.loadUserByUsername(username); // 创建认证对象 UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 将认证对象存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }跳过特定接口在doFilterInternal方法中首先获取当前请求的 URI判断是否是登录接口/api/auth/login或刷新令牌接口/api/auth/refresh 。如果是这两个接口之一直接调用chain.doFilter(request, response)放行请求因为登录接口用于用户获取 JWT 令牌刷新令牌接口用于在访问令牌过期时获取新的访问令牌这两个接口在请求时不需要进行 JWT 认证避免循环认证问题。从请求头获取 token调用getTokenFromRequest方法从请求头中提取 JWT 令牌。该方法先获取请求头中的Authorization字段这是约定俗成用于传递认证信息的字段。如果Authorization字段存在且以Bearer开头Bearer 是一种常见的认证方案前缀表示使用令牌认证则截取Bearer后面的字符串即 JWT 令牌返回给调用者如果不符合条件返回null。验证 token 并设置认证信息当获取到 JWT 令牌后调用jwtUtils.validateToken(token)方法验证令牌的有效性。如果令牌有效通过jwtUtils.getUsernameFromToken(token)从令牌中提取用户名。接着利用注入的UserDetailsService调用loadUserByUsername(username)方法根据用户名从数据库或其他数据源中加载用户的详细信息包括用户名、密码在验证过程中可能用到、用户权限等 。然后创建一个UsernamePasswordAuthenticationToken认证对象将用户详细信息、null密码在验证令牌后不再需要传递这里设为null和用户权限传入构造函数。再调用setDetails方法设置认证对象的详细信息这里使用WebAuthenticationDetailsSource创建请求相关的详细信息。最后将这个认证对象存入SecurityContextHolderSecurityContextHolder是 Spring Security 用于存储当前认证信息的地方这样在后续的请求处理过程中其他组件就可以从这里获取到当前用户的认证信息进行相应的授权操作 。放行请求在完成上述处理后无论是否成功验证令牌都调用chain.doFilter(request, response)将请求传递给下一个过滤器或处理器继续处理请求。如果令牌验证成功后续组件可以基于已设置的认证信息进行授权操作如果令牌验证失败由于没有在SecurityContextHolder中设置有效的认证信息后续的授权操作会因为认证失败而拒绝请求 。通过JwtAuthenticationFilter的实现我们在 Spring Boot 项目中建立了一个有效的 JWT 认证机制对每个进入系统的请求进行严格的身份验证确保只有合法用户的请求才能访问受保护的资源大大提高了系统的安全性和可靠性 。五、无感刷新机制实现5.1 双 Token 机制原理在实际应用中为了提升用户体验同时保障系统安全性我们引入双 Token 机制即同时使用 Access Token访问令牌和 Refresh Token刷新令牌。Access Token 主要用于用户在正常操作过程中每次请求时携带进行身份验证它包含了用户的关键信息如用户名、用户 ID、角色等这些信息会在服务器验证令牌时被提取和使用以确认请求的合法性和用户的权限。但由于其在网络传输中频繁使用一旦泄露可能导致用户身份被冒用所以通常设置较短的有效期比如 30 分钟 这就像一把有效期很短的临时钥匙即使丢失被他人利用的时间也有限。而 Refresh Token 则是专门用于在 Access Token 过期时获取新的 Access Token它就像是一把备用钥匙有效期相对较长例如可以设置为 7 天。因为它不直接参与业务接口的访问认证使用频率较低所以泄露的风险相对较小一般会存储在相对安全的地方如 HttpOnly Cookie 中防止前端 JavaScript 代码直接访问避免被 XSS 攻击窃取 。当用户登录成功后服务器会同时生成 Access Token 和 Refresh Token 并返回给客户端。客户端在后续请求时会将 Access Token 携带在 HTTP 请求头中发送给服务器。服务器在接收到请求后首先验证 Access Token 的有效性。如果 Access Token 有效正常处理请求若 Access Token 过期但此时客户端还持有有效的 Refresh Token客户端就会携带 Refresh Token 向服务器发送获取新 Access Token 的请求。服务器验证 Refresh Token 通过后会生成新的 Access Token 返回给客户端客户端更新本地存储的 Access Token然后继续后续操作整个过程对用户来说是无感知的极大地提升了用户体验同时也保障了系统的安全性 。5.2 后端实现步骤在 JwtUtils 类中添加刷新令牌的方法在之前封装的JwtUtils类中新增一个用于刷新令牌的方法。这个方法的作用是根据传入的旧的 Refresh Token生成新的 Access Token。首先从旧的 Refresh Token 中提取出用户名这是因为用户名是生成新的 Access Token 的关键信息就像重新配钥匙需要知道原来钥匙对应的锁的相关信息一样。然后调用之前的generateAccessToken方法根据提取的用户名生成新的 Access Token 并返回。import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.access-token-expiration}) private long accessTokenExpiration; Value(${jwt.refresh-token-expiration}) private long refreshTokenExpiration; // 其他方法... /** * 刷新令牌根据Refresh Token生成新的Access Token * param refreshToken 旧的Refresh Token * return 新的Access Token */ public String refreshToken(String refreshToken) { if (validateToken(refreshToken)) { String username getUsernameFromToken(refreshToken); return generateAccessToken(username); } return null; } }在 JwtAuthenticationFilter 中添加检查 token 是否即将过期并刷新的逻辑在JwtAuthenticationFilter过滤器中增加对 Access Token 是否即将过期的检查逻辑。当服务器接收到请求时会先从请求头中获取 Access Token然后判断该 Token 是否有效且即将过期。这里设置一个阈值比如当 Token 剩余有效期小于 5 分钟时认为即将过期 。如果即将过期调用JwtUtils中的refreshToken方法生成新的 Access Token并将新的 Token 添加到响应头中返回给客户端这样客户端就能及时更新本地的 Access Token实现无感刷新。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtUtils jwtUtils; Autowired private UserDetailsService userDetailsService; private static final long REFRESH_THRESHOLD 5 * 60 * 1000; // 5分钟即将过期的阈值单位毫秒 Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 跳过登录和刷新令牌的接口 String requestURI request.getRequestURI(); if (requestURI.equals(/api/auth/login) || requestURI.equals(/api/auth/refresh)) { chain.doFilter(request, response); return; } // 从请求头获取token String token getTokenFromRequest(request); if (token ! null jwtUtils.validateToken(token)) { // 从token中获取用户名 String username jwtUtils.getUsernameFromToken(token); // 判断token是否即将过期 Date expiration jwtUtils.getClaimsFromToken(token).getExpiration(); long remainingTime expiration.getTime() - System.currentTimeMillis(); if (remainingTime REFRESH_THRESHOLD) { // 生成新的accessToken String newAccessToken jwtUtils.refreshToken(token); if (newAccessToken ! null) { response.setHeader(Authorization, Bearer newAccessToken); } } // 从UserDetailsService中获取用户详细信息 UserDetails userDetails userDetailsService.loadUserByUsername(username); // 创建认证对象 UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 将认证对象存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }5.3 前端配合实现前端在实现无感刷新机制中起着重要的配合作用主要涉及以下几个关键步骤保存 token 和过期时间当用户登录成功后前端会从后端返回的响应中获取 Access Token 和 Refresh Token并将它们存储在本地。通常可以使用localStorage或者sessionStorage来存储这些信息 。同时为了方便后续判断 Token 是否过期还需要从 Token 的 Payload 中解析出过期时间并保存。以localStorage为例假设后端返回的响应数据是一个包含accessToken和refreshToken的 JSON 对象// 假设后端返回的数据 const responseData { accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c, refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c, // 假设后端同时返回过期时间毫秒时间戳 accessTokenExpiration: 1616239022000 }; // 保存token和过期时间 localStorage.setItem(accessToken, responseData.accessToken); localStorage.setItem(refreshToken, responseData.refreshToken); localStorage.setItem(accessTokenExpiration, responseData.accessTokenExpiration);检查 token 过期时间在每次前端发送 HTTP 请求前都需要检查本地存储的 Access Token 是否即将过期。通过获取当前时间并与之前保存的 Access Token 过期时间进行比较判断是否需要刷新 Token。这里同样设置一个阈值比如当距离过期时间小于 1 分钟时触发刷新操作 。可以使用一个自定义的函数来实现这个检查逻辑function checkTokenExpiration() { const accessToken localStorage.getItem(accessToken); const accessTokenExpiration parseInt(localStorage.getItem(accessTokenExpiration)); const currentTime Date.now(); // 设置阈值距离过期时间小于1分钟时触发刷新 const threshold 60 * 1000; if (accessToken accessTokenExpiration (accessTokenExpiration - currentTime) threshold) { return true; } return false; }发送续约请求当检查发现 Access Token 即将过期时前端需要携带旧的 Refresh Token 向服务器发送续约请求以获取新的 Access Token。通常会有一个专门的后端接口用于处理这个续约请求比如/api/auth/refresh。在发送请求时将 Refresh Token 放在请求头或者请求体中传递给后端。这里以使用axios库发送请求为例import axios from axios; async function renewToken() { const refreshToken localStorage.getItem(refreshToken); try { const response await axios.post(/api/auth/refresh, { refreshToken }, { headers: { Content-Type: application/json } }); const newAccessToken response.data.accessToken; const newAccessTokenExpiration response.data.accessTokenExpiration; // 更新本地存储的token和过期时间 localStorage.setItem(accessToken, newAccessToken); localStorage.setItem(accessTokenExpiration, newAccessTokenExpiration); return newAccessToken; } catch (error) { console.error(Token续约失败, error); // 处理续约失败的情况比如跳转到登录页面 window.location.href /login; } }更新本地存储当后端成功返回新的 Access Token 和相关信息如过期时间后前端需要及时更新本地存储的 Token 和过期时间确保后续请求使用的是最新的有效令牌。在上述renewToken函数中已经包含了更新本地存储的操作通过localStorage.setItem方法将新的 Access Token 和过期时间重新保存 。在实际应用中为了使代码结构更清晰、逻辑更严谨可以将上述功能封装成一个独立的模块并结合前端框架如 Vue、React 等的特性将这些逻辑集成到请求拦截器中实现对所有请求的统一处理确保在用户无感知的情况下完成 Token 的刷新提升用户体验和系统的安全性 。例如在 Vue 项目中可以利用axios的拦截器机制在请求发送前自动检查 Token 并进行刷新操作
Spring Boot 中JWT登录授权+无感刷新,看这篇就够了!
Spring Boot 中JWT登录授权无感刷新看这篇就够了一、引言在当今的分布式系统和前后端分离架构盛行的时代传统的基于 Session 的认证方式逐渐暴露出诸多弊端。想象一下在一个大型电商系统中用户的操作频繁涉及多个服务模块且前端可能是网页端、移动端等多种类型。若采用传统 Session 认证当用户从网页端切换到移动端继续操作时由于跨域问题Session 信息难以有效传递 导致用户需要重新登录。同时随着用户数量的急剧增加服务器需要存储大量的 Session 信息这无疑给服务器带来了沉重的存储压力就像一间小仓库要存放海量的货物空间迟早会被耗尽。而 JWTJSON Web Token的出现犹如一道曙光为这些问题提供了完美的解决方案。JWT 是一种轻量级的身份认证与授权方案具有无状态的特性这意味着服务器无需存储用户的会话信息大大减轻了服务器的负担如同给服务器卸下了沉重的包袱。它在跨域场景下表现出色能够轻松地在不同的前端应用和后端服务之间传递为前后端分离架构的发展提供了有力支持。并且JWT 易于扩展方便与各种系统集成无论是小型项目还是大型企业级应用都能发挥其优势。本文将详细地为大家讲解 Spring Boot 整合 JWT 实现登录认证与接口授权的全流程从最基础的环境搭建到核心功能的实现再到进阶优化每一步都有详细的代码示例和解释让你轻松掌握这一关键技术为你的项目开发保驾护航。二、JWT 基础扫盲2.1 JWT 是什么JWT即 JSON Web Token是一种基于 JSON 的开放标准RFC 7519 用于在网络应用间安全地传输声明。简单来说它是一种轻量级的身份认证和授权方案以 JSON 格式组织和传输信息。相较于传统的认证方式JWT 具有无状态、自包含的特性这意味着服务器无需存储用户的会话信息减轻了服务器的负载同时也方便在不同的服务和系统之间传递身份验证信息就像一个小巧且功能强大的通行证在分布式系统和前后端分离的架构中被广泛应用。2.2 JWT 结构剖析JWT 看起来是一个很长的字符串实际上它由三部分组成每部分之间用英文句点 “.” 分隔即 Header.Payload.Signature。Header头部主要存储两方面信息一是令牌的类型通常就是 “JWT”二是签名算法常见的如 HMAC SHA256、RSA 等 。例如{ alg: HS256, typ: JWT }将这个 JSON 对象进行 Base64Url 编码后就成为了 JWT 的第一部分。Payload载荷存放实际需要传递的声明claims信息。这些声明可以分为三类已注册声明如 iss 签发者、exp 过期时间、iat 签发时间等、公共声明开发者自定义的公开信息像用户 ID、角色等、私有声明应用内自定义的非公开信息。例如{ sub: 1234567890, name: John Doe, admin: true, iat: 1516239022, exp: 1516239022 3600 // 假设有效期1小时 }同样将这个 JSON 对象 Base64Url 编码后构成 JWT 的第二部分。需要注意的是Payload 默认不加密不要存放敏感信息如密码等。Signature签名用于验证 JWT 的完整性确保内容未被篡改。生成签名需要用到编码后的 Header、编码后的 Payload、一个只有服务器知道的密钥secret以及 Header 中指定的签名算法。以 HS256 算法为例签名生成公式为HMACSHA256( base64UrlEncode(header) . base64UrlEncode(payload), secret )计算出的签名作为 JWT 的第三部分与前两部分共同组成完整的 JWT如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c。2.3 认证流程详解用户登录用户在客户端如浏览器、移动应用输入用户名和密码向服务器发起登录请求。服务器验证服务器接收到登录请求后验证用户名和密码是否正确。若验证通过根据用户信息生成 JWT。这个过程就像是服务器给用户颁发了一张通行证通行证里包含了用户的关键信息如用户 ID、角色等并使用密钥和特定算法进行了签名。返回 JWT服务器将生成的 JWT 返回给客户端。客户端接收到 JWT 后可以将其存储在本地常见的存储方式有 localStorage、sessionStorage 或者 HttpOnly Cookie 等 。后续请求在后续的每一次请求中客户端都会将 JWT 携带在 HTTP 请求头中一般是放在 Authorization 字段格式为Authorization: Bearer JWT。Bearer 表示认证方案告诉服务器使用 JWT 进行认证。服务器验证服务器接收到请求后从请求头中提取 JWT然后使用相同的密钥和签名算法对 JWT 进行验证。验证内容包括签名是否正确、JWT 是否过期等。如果验证通过服务器就认为该请求是合法的并且可以从 JWT 的 Payload 中获取用户相关信息从而进行相应的授权操作返回请求的资源若验证失败则返回 401 Unauthorized 错误拒绝访问 。三、Spring Boot 环境搭建3.1 核心依赖引入在 Spring Boot 项目中首先要引入关键依赖为后续使用 JWT 进行登录授权奠定基础。这些依赖就像是搭建房屋的基石缺一不可。我们需要添加 Spring Security它是 Spring 生态中提供强大认证授权框架的组件能为应用程序保驾护航确保只有合法用户才能访问特定资源就像小区门口严格的保安阻挡外来人员随意进入JJWT 是 Java 领域主流的 JWT 工具库专门用于生成、解析和验证 JWT为我们处理 JWT 相关操作提供了便捷的方法Spring Web 则用于编写接口方便我们测试整个认证流程让我们能直观地看到认证授权在实际接口调用中的效果。如果你的项目使用 Maven 构建在pom.xml文件中添加以下依赖配置dependencies !-- Spring Security依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- JJWT依赖 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.2/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.2/version scoperuntime/scope /dependency !-- Spring Web依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies3.2 配置文件设置配置文件在整个项目中起着至关重要的作用它就像是项目的 “指挥中心”通过设置各种参数我们可以灵活地调整项目的运行方式。在application.properties或application.yml文件中需要配置 JWT 的一些关键参数。比如设置 JWT 的密钥jwt.secret这个密钥是生成和验证 JWT 签名的关键务必妥善保管就像保管自己家门的钥匙一样重要一旦泄露整个认证体系将面临被攻破的风险设置访问令牌过期时间jwt.access-token-expiration根据业务需求合理设定访问令牌的有效时长比如可以设置为 30 分钟这样既能保证一定的安全性又不会频繁让用户重新登录还有刷新令牌过期时间jwt.refresh-token-expiration通常刷新令牌的过期时间会比访问令牌长很多比如设置为 7 天用于在访问令牌过期时无需用户重新输入用户名和密码就能无感刷新获取新的访问令牌。以application.yml为例配置如下jwt: secret: your-secret-key access-token-expiration: 1800000 # 30分钟单位毫秒 refresh-token-expiration: 604800000 # 7天单位毫秒通过上述环境搭建步骤我们的 Spring Boot 项目已经具备了使用 JWT 进行登录授权的基本条件接下来就可以着手实现核心的登录认证和接口授权功能了。四、核心功能实现4.1 JWT 工具类封装在 Spring Boot 项目中为了方便地处理 JWT 相关操作我们需要将常用的 JWT 操作封装成一个工具类就像把各种工具整理到一个工具箱里使用时随手可拿。这里创建一个JwtUtils类利用 Spring 的依赖注入机制将 JWT 的密钥和过期时间等配置信息注入到类中这样我们就能灵活地根据配置生成和验证 JWT而无需在代码中硬编码这些关键信息。首先在类中使用Component注解将JwtUtils标记为 Spring 组件这样 Spring 容器在启动时会自动扫描并实例化这个类使其成为容器管理的 Bean方便在其他组件中通过依赖注入的方式使用。接着通过Value注解从配置文件中读取 JWT 的密钥jwt.secret、访问令牌过期时间jwt.access-token-expiration和刷新令牌过期时间jwt.refresh-token-expiration 并将这些值赋给相应的成员变量。下面是JwtUtils类中几个关键方法的实现及解释import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.access-token-expiration}) private long accessTokenExpiration; Value(${jwt.refresh-token-expiration}) private long refreshTokenExpiration; /** * 生成访问令牌 * param username 用户名 * return 生成的访问令牌 */ public String generateAccessToken(String username) { MapString, Object claims new HashMap(); return generateToken(claims, username, accessTokenExpiration); } /** * 生成刷新令牌 * param username 用户名 * return 生成的刷新令牌 */ public String generateRefreshToken(String username) { MapString, Object claims new HashMap(); return generateToken(claims, username, refreshTokenExpiration); } /** * 生成JWT令牌 * param claims 负载信息 * param subject 主题一般为用户名 * param expireTime 过期时间毫秒 * return 生成的JWT令牌 */ private String generateToken(MapString, Object claims, String subject, long expireTime) { Key key Keys.hmacShaKeyFor(secret.getBytes()); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireTime)) .signWith(key, SignatureAlgorithm.HS256) .compact(); } /** * 从令牌中获取用户名 * param token JWT令牌 * return 用户名 */ public String getUsernameFromToken(String token) { Claims claims getClaimsFromToken(token); return claims.getSubject(); } /** * 验证令牌是否有效 * param token JWT令牌 * return 是否有效 */ public boolean validateToken(String token) { try { Key key Keys.hmacShaKeyFor(secret.getBytes()); Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (Exception e) { return false; } } /** * 判断令牌是否过期 * param token JWT令牌 * return 是否过期 */ public boolean isTokenExpired(String token) { Claims claims getClaimsFromToken(token); Date expiration claims.getExpiration(); return expiration.before(new Date()); } /** * 从令牌中获取负载信息 * param token JWT令牌 * return 负载信息 */ private Claims getClaimsFromToken(String token) { Key key Keys.hmacShaKeyFor(secret.getBytes()); return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody(); } }生成访问令牌generateAccessToken该方法接收用户名作为参数创建一个空的claims载荷Map然后调用generateToken方法传入claims、用户名和访问令牌过期时间生成访问令牌。这里空的claims可以在后续根据业务需求添加更多用户相关信息比如用户角色、用户 ID 等 使令牌携带更丰富的用户身份信息。生成刷新令牌generateRefreshToken与生成访问令牌类似只是传入的过期时间是刷新令牌的过期时间用于生成长期有效的刷新令牌以便在访问令牌过期时用户无需重新输入用户名和密码就能获取新的访问令牌提升用户体验。生成 JWT 令牌generateToken这是一个私有的核心方法用于实际生成 JWT 令牌。它接收claims载荷信息、subject主题通常是用户名和expireTime过期时间单位毫秒作为参数。首先根据密钥生成一个Key对象然后使用Jwts.builder构建 JWT。依次设置claims、subject、签发时间当前时间、过期时间并使用指定的SignatureAlgorithm.HS256算法和密钥进行签名最后调用compact方法生成紧凑的 JWT 字符串 。从令牌中获取用户名getUsernameFromToken该方法从 JWT 令牌中提取出用户名。首先调用getClaimsFromToken方法获取令牌的claims载荷然后通过claims.getSubject()获取主题即用户名这样在验证令牌后我们就能方便地获取到令牌对应的用户身份。验证令牌是否有效validateToken尝试使用密钥和签名算法解析 JWT 令牌如果解析成功说明令牌有效返回true如果在解析过程中出现异常如签名验证失败、令牌格式错误、令牌已过期等说明令牌无效返回false确保只有合法有效的令牌才能通过验证保障系统安全。判断令牌是否过期isTokenExpired从令牌中获取claims载荷提取其中的过期时间expiration然后与当前时间进行比较如果过期时间早于当前时间说明令牌已过期返回true否则返回false这在处理令牌过期逻辑时非常重要比如决定是否需要刷新令牌。从令牌中获取负载信息getClaimsFromToken使用密钥和签名算法解析 JWT 令牌返回解析后的claims载荷其中包含了用户相关的各种信息如用户名、角色、过期时间等为后续根据令牌获取用户信息提供了基础。通过以上封装JwtUtils类提供了一套完整且便捷的 JWT 操作方法在整个项目中无论是生成令牌、验证令牌还是从令牌中提取信息都可以通过调用这个工具类的方法轻松实现大大提高了代码的复用性和可维护性 。4.2 实现认证过滤器在 Spring Boot 项目中为了对每个请求进行 JWT 认证我们需要创建一个认证过滤器。这个过滤器就像是一个严格的保安站在请求进入系统的入口对每个请求进行检查只有持有合法 JWT 令牌的请求才能放行进入系统。这里创建一个JwtAuthenticationFilter类让它继承OncePerRequestFilterOncePerRequestFilter保证每个请求只会被过滤一次避免重复过滤带来的性能损耗。在这个类中注入JwtUtils工具类用于验证 JWT 令牌的有效性同时注入UserDetailsService以便在验证令牌通过后获取用户的详细信息进行后续的授权操作。下面是JwtAuthenticationFilter类的核心实现及逻辑解释import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtUtils jwtUtils; Autowired private UserDetailsService userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 跳过登录和刷新令牌的接口 String requestURI request.getRequestURI(); if (requestURI.equals(/api/auth/login) || requestURI.equals(/api/auth/refresh)) { chain.doFilter(request, response); return; } // 从请求头获取token String token getTokenFromRequest(request); if (token ! null jwtUtils.validateToken(token)) { // 从token中获取用户名 String username jwtUtils.getUsernameFromToken(token); // 从UserDetailsService中获取用户详细信息 UserDetails userDetails userDetailsService.loadUserByUsername(username); // 创建认证对象 UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 将认证对象存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }跳过特定接口在doFilterInternal方法中首先获取当前请求的 URI判断是否是登录接口/api/auth/login或刷新令牌接口/api/auth/refresh 。如果是这两个接口之一直接调用chain.doFilter(request, response)放行请求因为登录接口用于用户获取 JWT 令牌刷新令牌接口用于在访问令牌过期时获取新的访问令牌这两个接口在请求时不需要进行 JWT 认证避免循环认证问题。从请求头获取 token调用getTokenFromRequest方法从请求头中提取 JWT 令牌。该方法先获取请求头中的Authorization字段这是约定俗成用于传递认证信息的字段。如果Authorization字段存在且以Bearer开头Bearer 是一种常见的认证方案前缀表示使用令牌认证则截取Bearer后面的字符串即 JWT 令牌返回给调用者如果不符合条件返回null。验证 token 并设置认证信息当获取到 JWT 令牌后调用jwtUtils.validateToken(token)方法验证令牌的有效性。如果令牌有效通过jwtUtils.getUsernameFromToken(token)从令牌中提取用户名。接着利用注入的UserDetailsService调用loadUserByUsername(username)方法根据用户名从数据库或其他数据源中加载用户的详细信息包括用户名、密码在验证过程中可能用到、用户权限等 。然后创建一个UsernamePasswordAuthenticationToken认证对象将用户详细信息、null密码在验证令牌后不再需要传递这里设为null和用户权限传入构造函数。再调用setDetails方法设置认证对象的详细信息这里使用WebAuthenticationDetailsSource创建请求相关的详细信息。最后将这个认证对象存入SecurityContextHolderSecurityContextHolder是 Spring Security 用于存储当前认证信息的地方这样在后续的请求处理过程中其他组件就可以从这里获取到当前用户的认证信息进行相应的授权操作 。放行请求在完成上述处理后无论是否成功验证令牌都调用chain.doFilter(request, response)将请求传递给下一个过滤器或处理器继续处理请求。如果令牌验证成功后续组件可以基于已设置的认证信息进行授权操作如果令牌验证失败由于没有在SecurityContextHolder中设置有效的认证信息后续的授权操作会因为认证失败而拒绝请求 。通过JwtAuthenticationFilter的实现我们在 Spring Boot 项目中建立了一个有效的 JWT 认证机制对每个进入系统的请求进行严格的身份验证确保只有合法用户的请求才能访问受保护的资源大大提高了系统的安全性和可靠性 。五、无感刷新机制实现5.1 双 Token 机制原理在实际应用中为了提升用户体验同时保障系统安全性我们引入双 Token 机制即同时使用 Access Token访问令牌和 Refresh Token刷新令牌。Access Token 主要用于用户在正常操作过程中每次请求时携带进行身份验证它包含了用户的关键信息如用户名、用户 ID、角色等这些信息会在服务器验证令牌时被提取和使用以确认请求的合法性和用户的权限。但由于其在网络传输中频繁使用一旦泄露可能导致用户身份被冒用所以通常设置较短的有效期比如 30 分钟 这就像一把有效期很短的临时钥匙即使丢失被他人利用的时间也有限。而 Refresh Token 则是专门用于在 Access Token 过期时获取新的 Access Token它就像是一把备用钥匙有效期相对较长例如可以设置为 7 天。因为它不直接参与业务接口的访问认证使用频率较低所以泄露的风险相对较小一般会存储在相对安全的地方如 HttpOnly Cookie 中防止前端 JavaScript 代码直接访问避免被 XSS 攻击窃取 。当用户登录成功后服务器会同时生成 Access Token 和 Refresh Token 并返回给客户端。客户端在后续请求时会将 Access Token 携带在 HTTP 请求头中发送给服务器。服务器在接收到请求后首先验证 Access Token 的有效性。如果 Access Token 有效正常处理请求若 Access Token 过期但此时客户端还持有有效的 Refresh Token客户端就会携带 Refresh Token 向服务器发送获取新 Access Token 的请求。服务器验证 Refresh Token 通过后会生成新的 Access Token 返回给客户端客户端更新本地存储的 Access Token然后继续后续操作整个过程对用户来说是无感知的极大地提升了用户体验同时也保障了系统的安全性 。5.2 后端实现步骤在 JwtUtils 类中添加刷新令牌的方法在之前封装的JwtUtils类中新增一个用于刷新令牌的方法。这个方法的作用是根据传入的旧的 Refresh Token生成新的 Access Token。首先从旧的 Refresh Token 中提取出用户名这是因为用户名是生成新的 Access Token 的关键信息就像重新配钥匙需要知道原来钥匙对应的锁的相关信息一样。然后调用之前的generateAccessToken方法根据提取的用户名生成新的 Access Token 并返回。import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.access-token-expiration}) private long accessTokenExpiration; Value(${jwt.refresh-token-expiration}) private long refreshTokenExpiration; // 其他方法... /** * 刷新令牌根据Refresh Token生成新的Access Token * param refreshToken 旧的Refresh Token * return 新的Access Token */ public String refreshToken(String refreshToken) { if (validateToken(refreshToken)) { String username getUsernameFromToken(refreshToken); return generateAccessToken(username); } return null; } }在 JwtAuthenticationFilter 中添加检查 token 是否即将过期并刷新的逻辑在JwtAuthenticationFilter过滤器中增加对 Access Token 是否即将过期的检查逻辑。当服务器接收到请求时会先从请求头中获取 Access Token然后判断该 Token 是否有效且即将过期。这里设置一个阈值比如当 Token 剩余有效期小于 5 分钟时认为即将过期 。如果即将过期调用JwtUtils中的refreshToken方法生成新的 Access Token并将新的 Token 添加到响应头中返回给客户端这样客户端就能及时更新本地的 Access Token实现无感刷新。import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Autowired private JwtUtils jwtUtils; Autowired private UserDetailsService userDetailsService; private static final long REFRESH_THRESHOLD 5 * 60 * 1000; // 5分钟即将过期的阈值单位毫秒 Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // 跳过登录和刷新令牌的接口 String requestURI request.getRequestURI(); if (requestURI.equals(/api/auth/login) || requestURI.equals(/api/auth/refresh)) { chain.doFilter(request, response); return; } // 从请求头获取token String token getTokenFromRequest(request); if (token ! null jwtUtils.validateToken(token)) { // 从token中获取用户名 String username jwtUtils.getUsernameFromToken(token); // 判断token是否即将过期 Date expiration jwtUtils.getClaimsFromToken(token).getExpiration(); long remainingTime expiration.getTime() - System.currentTimeMillis(); if (remainingTime REFRESH_THRESHOLD) { // 生成新的accessToken String newAccessToken jwtUtils.refreshToken(token); if (newAccessToken ! null) { response.setHeader(Authorization, Bearer newAccessToken); } } // 从UserDetailsService中获取用户详细信息 UserDetails userDetails userDetailsService.loadUserByUsername(username); // 创建认证对象 UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 将认证对象存入SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authenticationToken); } chain.doFilter(request, response); } private String getTokenFromRequest(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (bearerToken ! null bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }5.3 前端配合实现前端在实现无感刷新机制中起着重要的配合作用主要涉及以下几个关键步骤保存 token 和过期时间当用户登录成功后前端会从后端返回的响应中获取 Access Token 和 Refresh Token并将它们存储在本地。通常可以使用localStorage或者sessionStorage来存储这些信息 。同时为了方便后续判断 Token 是否过期还需要从 Token 的 Payload 中解析出过期时间并保存。以localStorage为例假设后端返回的响应数据是一个包含accessToken和refreshToken的 JSON 对象// 假设后端返回的数据 const responseData { accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c, refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c, // 假设后端同时返回过期时间毫秒时间戳 accessTokenExpiration: 1616239022000 }; // 保存token和过期时间 localStorage.setItem(accessToken, responseData.accessToken); localStorage.setItem(refreshToken, responseData.refreshToken); localStorage.setItem(accessTokenExpiration, responseData.accessTokenExpiration);检查 token 过期时间在每次前端发送 HTTP 请求前都需要检查本地存储的 Access Token 是否即将过期。通过获取当前时间并与之前保存的 Access Token 过期时间进行比较判断是否需要刷新 Token。这里同样设置一个阈值比如当距离过期时间小于 1 分钟时触发刷新操作 。可以使用一个自定义的函数来实现这个检查逻辑function checkTokenExpiration() { const accessToken localStorage.getItem(accessToken); const accessTokenExpiration parseInt(localStorage.getItem(accessTokenExpiration)); const currentTime Date.now(); // 设置阈值距离过期时间小于1分钟时触发刷新 const threshold 60 * 1000; if (accessToken accessTokenExpiration (accessTokenExpiration - currentTime) threshold) { return true; } return false; }发送续约请求当检查发现 Access Token 即将过期时前端需要携带旧的 Refresh Token 向服务器发送续约请求以获取新的 Access Token。通常会有一个专门的后端接口用于处理这个续约请求比如/api/auth/refresh。在发送请求时将 Refresh Token 放在请求头或者请求体中传递给后端。这里以使用axios库发送请求为例import axios from axios; async function renewToken() { const refreshToken localStorage.getItem(refreshToken); try { const response await axios.post(/api/auth/refresh, { refreshToken }, { headers: { Content-Type: application/json } }); const newAccessToken response.data.accessToken; const newAccessTokenExpiration response.data.accessTokenExpiration; // 更新本地存储的token和过期时间 localStorage.setItem(accessToken, newAccessToken); localStorage.setItem(accessTokenExpiration, newAccessTokenExpiration); return newAccessToken; } catch (error) { console.error(Token续约失败, error); // 处理续约失败的情况比如跳转到登录页面 window.location.href /login; } }更新本地存储当后端成功返回新的 Access Token 和相关信息如过期时间后前端需要及时更新本地存储的 Token 和过期时间确保后续请求使用的是最新的有效令牌。在上述renewToken函数中已经包含了更新本地存储的操作通过localStorage.setItem方法将新的 Access Token 和过期时间重新保存 。在实际应用中为了使代码结构更清晰、逻辑更严谨可以将上述功能封装成一个独立的模块并结合前端框架如 Vue、React 等的特性将这些逻辑集成到请求拦截器中实现对所有请求的统一处理确保在用户无感知的情况下完成 Token 的刷新提升用户体验和系统的安全性 。例如在 Vue 项目中可以利用axios的拦截器机制在请求发送前自动检查 Token 并进行刷新操作