Spring Boot 3实战:从零构建JWT认证系统(jjwt 0.12.3)
1. 环境准备与项目搭建在开始构建JWT认证系统之前我们需要先准备好开发环境。这里我使用的是Spring Boot 3.1.2和JDK 17这也是目前比较新的稳定版本组合。如果你还在用JDK 8建议升级一下因为Spring Boot 3.x最低要求就是JDK 17了。首先创建一个新的Spring Boot项目我习惯用Spring Initializr来生成项目骨架。选择以下依赖Spring WebLombok简化代码的神器创建完项目后我们需要手动添加jjwt的依赖。这里要注意jjwt在0.12.3版本做了比较大的改动API和之前版本不太一样。在pom.xml中添加dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.12.3/version /dependency你可能在其他教程里看到需要添加jjwt-api、jjwt-impl等多个依赖但在0.12.3版本只需要上面这一个就够了简化了不少。我在实际项目中测试过这个单一依赖已经包含了所有需要的功能。2. JWT工具类编写接下来我们创建一个JWT工具类这个类将负责生成和解析JWT令牌。我把这个类命名为JwtUtil放在utils包下。import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; import javax.crypto.SecretKey; import java.util.Date; import java.util.Map; Slf4j public class JwtUtil { // 生成JWT令牌 public static String createToken(String secretKey, long ttlMillis, MapString, Object claims) { // 指定签名算法 SecretKey key Keys.hmacShaKeyFor(secretKey.getBytes()); // 计算过期时间 long expMillis System.currentTimeMillis() ttlMillis; Date exp new Date(expMillis); return Jwts.builder() .claims(claims) // 自定义负载信息 .expiration(exp) // 过期时间 .signWith(key) // 签名密钥 .compact(); // 生成最终字符串 } // 解析JWT令牌 public static Claims parseToken(String token, String secretKey) { SecretKey key Keys.hmacShaKeyFor(secretKey.getBytes()); return Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload(); } }这个工具类有两个核心方法createToken生成JWT令牌接收三个参数密钥、过期时间(毫秒)和自定义负载信息parseToken解析JWT令牌验证签名并提取负载信息这里有几个关键点需要注意新版的jjwt使用了更安全的密钥生成方式不再直接使用字符串作为密钥签名算法默认使用HS256这也是最常用的对称加密算法负载信息(claims)可以包含任何你需要传递的数据比如用户ID、角色等3. 配置JWT参数为了让配置更灵活我建议把JWT相关的参数放在application.yml中jwt: secret-key: your-secret-key-at-least-32-bytes-long ttl: 7200000 # 2小时单位毫秒 token-name: Authorization # 前端放在header中的字段名然后创建一个配置类来加载这些参数import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; Data Component ConfigurationProperties(prefix jwt) public class JwtProperties { private String secret-key; private long ttl; private String token-name; }这里我用了Lombok的Data注解自动生成getter/setter减少样板代码。secret-key建议设置至少32个字符的长度可以使用在线工具生成随机字符串。4. 实现JWT拦截器为了让JWT认证生效我们需要创建一个拦截器来验证请求中的tokenimport jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; Slf4j Component public class JwtInterceptor implements HandlerInterceptor { private final JwtProperties jwtProperties; public JwtInterceptor(JwtProperties jwtProperties) { this.jwtProperties jwtProperties; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 如果不是Controller方法直接放行 if (!(handler instanceof HandlerMethod)) { return true; } // 从header中获取token String token request.getHeader(jwtProperties.getTokenName()); try { // 解析token Claims claims JwtUtil.parseToken(token, jwtProperties.getSecretKey()); // 可以把用户ID存入线程上下文方便后续使用 Long userId Long.valueOf(claims.get(userId).toString()); UserContext.setCurrentUserId(userId); return true; } catch (Exception e) { log.error(JWT验证失败: {}, e.getMessage()); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } } }拦截器的核心逻辑是从请求头中获取token使用JwtUtil验证token的有效性如果验证通过提取用户信息并存入上下文如果验证失败返回401未授权状态码5. 注册拦截器创建了拦截器后还需要把它注册到Spring MVC中import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class WebConfig implements WebMvcConfigurer { private final JwtInterceptor jwtInterceptor; public WebConfig(JwtInterceptor jwtInterceptor) { this.jwtInterceptor jwtInterceptor; } Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) // 拦截/api开头的路径 .excludePathPatterns(/api/auth/login); // 排除登录接口 } }这里我配置了拦截所有以/api开头的请求但排除了登录接口。你可以根据实际需求调整这些路径。6. 实现登录接口现在我们来创建一个登录接口成功登录后返回JWT令牌import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; RestController public class AuthController { private final UserService userService; private final JwtProperties jwtProperties; public AuthController(UserService userService, JwtProperties jwtProperties) { this.userService userService; this.jwtProperties jwtProperties; } PostMapping(/api/auth/login) public ResultLoginVO login(RequestBody LoginDTO loginDTO) { // 验证用户名密码 User user userService.verifyLogin(loginDTO); // 生成token String token JwtUtil.createToken( jwtProperties.getSecretKey(), jwtProperties.getTtl(), Map.of(userId, user.getId()) ); // 返回结果 return Result.success(new LoginVO(user.getId(), user.getUsername(), token)); } }登录流程很简单接收用户名密码验证用户信息验证通过后生成JWT令牌返回用户信息和token7. 测试JWT认证现在我们可以测试整个流程了。首先用Postman调用登录接口POST /api/auth/login Content-Type: application/json { username: admin, password: 123456 }应该会得到类似这样的响应{ code: 200, data: { userId: 1, username: admin, token: eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImV4cCI6MTY5MDAwMDAwMH0.abcdef1234567890 } }然后复制这个token在访问其他接口时加到请求头中GET /api/user/profile Authorization: eyJhbGciOiJIUzI1NiJ9...如果token有效就能正常访问接口如果token无效或过期会返回401错误。8. 常见问题与解决方案在实际使用中我遇到过几个典型问题这里分享一下解决方案问题1密钥长度不足jjwt 0.12.3对密钥长度有严格要求HS256算法至少需要256位(32字节)的密钥。如果密钥太短会报错InvalidKeyException: The signing keys size is 128 bits which is not secure enough for the HS256 algorithm解决方案就是使用足够长的密钥可以用这个命令生成openssl rand -base64 32问题2token过期处理前端应该在收到401响应后自动跳转到登录页面。也可以在拦截器中返回更友好的错误信息response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\token已过期请重新登录\});问题3多端登录如果系统需要支持多端同时登录可以考虑在token中加入设备类型字段MapString, Object claims new HashMap(); claims.put(userId, user.getId()); claims.put(device, web); // 或app然后在解析token时校验设备类型实现更精细的权限控制。9. 安全性增强建议基础的JWT实现已经完成了但为了更安全我建议考虑以下几点使用HTTPSJWT在传输过程中可能被截获HTTPS是必须的设置合理的过期时间根据业务需求通常2小时到7天不等实现token刷新机制当token快过期时可以用refresh token获取新token黑名单机制虽然JWT本身是无状态的但可以维护一个黑名单来强制失效某些token敏感操作二次验证比如修改密码时要求再次输入密码一个简单的refresh token实现示例// 登录时生成两个token String accessToken JwtUtil.createToken(secretKey, 2 * 60 * 60 * 1000, claims); // 2小时 String refreshToken JwtUtil.createToken(secretKey, 7 * 24 * 60 * 60 * 1000, claims); // 7天 // 刷新token的接口 PostMapping(/api/auth/refresh) public ResultString refreshToken(RequestHeader(Refresh-Token) String refreshToken) { Claims claims JwtUtil.parseToken(refreshToken, jwtProperties.getSecretKey()); String newAccessToken JwtUtil.createToken( jwtProperties.getSecretKey(), jwtProperties.getTtl(), claims ); return Result.success(newAccessToken); }10. 前后端联调注意事项最后说一下前后端联调时需要注意的点token存储前端通常把token存在localStorage或cookie中但要注意XSS攻击风险请求头设置确保前端在请求时正确设置了Authorization头跨域问题如果前后端分离开发要配置CORS错误处理统一错误码和错误信息格式一个典型的前端请求示例使用axiosaxios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization token; } return config; }); axios.interceptors.response.use( response response, error { if (error.response.status 401) { // 跳转到登录页 window.location.href /login; } return Promise.reject(error); } );