✅ 引言
在现代 Web 开发中,传统的 Session 认证方式在分布式、微服务架构下面临挑战:
- Session 存储依赖服务器内存或 Redis
- 跨域、跨服务共享 Session 复杂
- 增加服务器负担
JWT(JSON Web Token) 提供了一种无状态的解决方案:用户登录后,服务器返回一个 Token,后续请求携带该 Token 即可完成身份验证,无需服务器存储会话信息。
本文将结合 Spring Boot 拦截器,手把手实现一个完整的 JWT 无状态登录系统。
📌 一、JWT 是什么?
JWT 是一个开放标准(RFC 7519),用于在各方之间安全地传输信息。
一个 JWT 通常由三部分组成,用 .
分隔:
xxxxx.yyyyy.zzzzz
- Header:令牌类型和签名算法
- Payload:存放用户信息(如用户 ID、角色、过期时间等)
- Signature:签名,用于验证 Token 是否被篡改
✅ 优点:自包含、可扩展、跨语言、无状态。
📌 二、技术选型
- Spring Boot 2.7+
- JWT 库:
io.jsonwebtoken:jjwt
- 加密算法:HMAC SHA256
- 拦截器:
HandlerInterceptor
📌 三、项目结构
src/
├── main/
│ ├── java/
│ │ └── com/example/jwtlogin/
│ │ ├── config/ → 配置类
│ │ ├── interceptor/ → 拦截器
│ │ ├── util/ → 工具类
│ │ ├── controller/ → 控制器
│ │ └── entity/ → 实体类
📌 四、核心代码实现
4.1 添加依赖(pom.xml)
<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>
⚠️ 注意:JJWT 0.11+ 版本拆分了模块,需引入三个依赖。
4.2 JWT 工具类
@Component
public class JwtUtil {
// 密钥(应放在配置文件中)
private static final String SECRET = "your-256-bit-secret-your-256-bit-secret";
// 过期时间:24小时
private static final long EXPIRATION = 1000 * 60 * 60 * 24;
/**
* 生成 Token
*/
public String generateToken(String username, Long userId, String role) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("role", role);
return Jwts.builder()
.setSubject(username)
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
/**
* 解析 Token
*/
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new RuntimeException("Token 已过期");
} catch (UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
throw new RuntimeException("Token 无效");
}
}
/**
* 验证 Token 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
4.3 登录控制器
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
/**
* 用户登录
*/
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(@RequestBody LoginRequest request) {
// 这里应调用 UserService 验证用户名密码
// 为简化,假设用户名密码正确
if ("admin".equals(request.getUsername()) && "123456".equals(request.getPassword())) {
String token = jwtUtil.generateToken(request.getUsername(), 1L, "ADMIN");
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("username", request.getUsername());
result.put("role", "ADMIN");
return ResponseEntity.ok(result);
} else {
return ResponseEntity.status(401).body(Map.of("msg", "用户名或密码错误"));
}
}
/**
* 用户登出(前端清空 Token 即可)
*/
@PostMapping("/logout")
public ResponseEntity<String> logout() {
return ResponseEntity.ok("登出成功");
}
}
// 登录请求 DTO
class LoginRequest {
private String username;
private String password;
// getter & setter
}
4.4 JWT 拦截器
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
private static final String AUTH_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Bearer ";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 放行 OPTIONS 请求(预检请求)
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return true;
}
// 2. 放行登录接口
String requestURI = request.getRequestURI();
if ("/auth/login".equals(requestURI) || "/auth/logout".equals(requestURI)) {
return true;
}
// 3. 获取并验证 Token
String token = request.getHeader(AUTH_HEADER);
if (token == null || !token.startsWith(TOKEN_PREFIX)) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"缺少 Token\"}");
return false;
}
token = token.substring(TOKEN_PREFIX.length());
try {
Claims claims = jwtUtil.parseToken(token);
// 将用户信息存入 request,供后续 Controller 使用
request.setAttribute("currentUser", claims.getSubject());
request.setAttribute("userId", claims.get("userId"));
request.setAttribute("role", claims.get("role"));
return true;
} catch (RuntimeException e) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"" + e.getMessage() + "\"}");
return false;
}
}
}
4.5 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**") // 保护所有 API 接口
.excludePathPatterns("/auth/**", "/public/**"); // 放行认证和公共接口
}
}
4.6 测试受保护的接口
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/profile")
public Map<String, Object> getProfile(HttpServletRequest request) {
Map<String, Object> profile = new HashMap<>();
profile.put("username", request.getAttribute("currentUser"));
profile.put("userId", request.getAttribute("userId"));
profile.put("role", request.getAttribute("role"));
profile.put("email", "admin@example.com");
return profile;
}
@GetMapping("/admin/data")
public String adminData(HttpServletRequest request) {
String role = (String) request.getAttribute("role");
if ("ADMIN".equals(role)) {
return "敏感数据:只有管理员可见";
} else {
return "权限不足";
}
}
}
📌 五、测试流程
1. 登录获取 Token
curl -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
响应:
{
"token": "eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy",
"username": "admin",
"role": "ADMIN"
}
2. 携带 Token 访问受保护接口
curl http://localhost:8080/api/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.xxxxx.yyyyy"
响应:
{
"username": "admin",
"userId": 1,
"role": "ADMIN",
"email": "admin@example.com"
}
3. 不带 Token 访问 → 401 未授权
📌 六、生产环境优化建议
优化点 | 说明 |
---|---|
🔐 密钥安全 | 将 SECRET 放在 application.yml 或环境变量中,不要硬编码 |
🔄 Token 刷新 | 实现 Refresh Token 机制,避免频繁登录 |
🛑 Token 黑名单 | 使用 Redis 记录已注销的 Token,防止被盗用 |
🧩 自定义注解 | 使用 @RequireAuth(role="ADMIN") 简化权限控制 |
📊 日志监控 | 记录 Token 解析失败日志,便于排查问题 |
✅ 总结
步骤 | 说明 |
---|---|
1. 用户登录 | 验证账号密码,生成 JWT |
2. 客户端存储 | 将 Token 存入 localStorage 或 Cookie |
3. 携带请求 | 每次请求在 Authorization 头中携带 Bearer Token |
4. 拦截器验证 | 解析 Token,校验签名和过期时间 |
5. 放行或拒绝 | 验证通过则放行,否则返回 401 |
💡 无状态登录的核心:服务器不保存会话状态,所有信息都封装在 Token 中,由客户端负责携带和管理。