Spring Boot + JWT 实现无状态认证
·
1. JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用环境间安全地将信息作为 JSON 对象传输。JWT 是目前最流行的跨域认证解决方案,特别适合前后端分离的架构。
1.1 JWT 的结构
JWT 由三部分组成,用点号(.)分隔:
xxxxx.yyyyy.zzzzz
│ │ │
│ │ └── Signature(签名)
│ └───────── Payload(负载)
└──────────────── Header(头部)
Header:包含令牌类型和签名算法
{
"alg": "HS256",
"typ": "JWT"
}
Payload:包含声明(claims),如用户信息、过期时间等
{
"username": "zhangsan",
"iat": 1516239022,
"exp": 1516242622
}
Signature:使用密钥对前两部分进行签名,确保令牌不被篡改
1.2 JWT 的优势
| 优势 | 说明 |
|---|---|
| 无状态 | 服务器不需要保存会话信息,令牌自包含用户信息 |
| 跨域支持 | 天然支持 CORS,适合前后端分离架构 |
| 扩展性强 | 令牌中可以携带自定义信息 |
| 安全性高 | 使用签名防止篡改,支持过期时间 |
2. 项目架构
┌─────────────────────────────────────────────────────────────┐
│ 客户端 (Vue 3) │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ 登录请求 │ │ 存储 Token │ │ 请求携带 Token │ │
│ └──────────────┘ └──────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ HTTP/REST
▼
┌─────────────────────────────────────────────────────────────┐
│ 服务端 (Spring Boot) │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │
│ │ 生成 Token │ │ 验证 Token │ │ 授权访问资源 │ │
│ │ (JwtUtil) │ │ (JwtFilter) │ │ (SecurityConfig) │ │
│ └──────────────┘ └──────────────┘ └────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3. 核心组件实现
3.1 JwtUtil - JWT 工具类
负责生成、解析和验证 JWT 令牌。
@Component
public class JwtUtil {
// 使用 HS256 算法生成密钥
private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
@Autowired
private AppConfig appConfig;
/**
* 生成 JWT 令牌
* @param username 用户名
* @return JWT 令牌字符串
*/
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
return Jwts.builder()
.setClaims(claims) // 设置自定义声明
.setSubject(username) // 设置主题(用户名)
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + getExpirationTime())) // 设置过期时间
.signWith(key) // 使用密钥签名
.compact(); // 生成令牌
}
/**
* 从令牌中提取用户名
* @param token JWT 令牌
* @return 用户名
*/
public String extractUsername(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key) // 设置验证密钥
.build()
.parseClaimsJws(token) // 解析令牌
.getBody(); // 获取声明体
return claims.getSubject();
}
/**
* 验证令牌是否有效
* @param token JWT 令牌
* @return 是否有效
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true; // 解析成功,令牌有效
} catch (Exception e) {
return false; // 解析失败,令牌无效或过期
}
}
}
关键点说明:
- 使用
Keys.secretKeyFor(SignatureAlgorithm.HS256)生成安全的密钥 setExpiration()设置令牌过期时间,增强安全性validateToken()捕获所有异常,确保令牌验证的健壮性
3.2 JwtFilter - JWT 过滤器
拦截请求,验证 JWT 令牌的有效性。
@Component
public class JwtFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// 从请求头中获取令牌
String authorizationHeader = request.getHeader("Authorization");
String token = null;
String username = null;
// 检查授权头,提取 Bearer Token
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7); // 去掉 "Bearer " 前缀
try {
username = jwtUtil.extractUsername(token);
} catch (Exception e) {
logger.error("Error extracting username from token: {}", e.getMessage());
}
}
// 验证令牌并设置认证上下文
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtUtil.validateToken(token)) {
// 创建认证令牌
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
username, null, null);
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置安全上下文
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
关键点说明:
- 继承
OncePerRequestFilter确保每个请求只过滤一次 - 从
Authorization头中提取Bearer Token - 使用
SecurityContextHolder设置认证信息,供后续业务使用
3.3 SecurityConfig - 安全配置
配置 Spring Security,定义安全规则和认证方式。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtFilter jwtFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置 CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 禁用 CSRF(前后端分离不需要)
.csrf(csrf -> csrf.disable())
// 配置请求授权
.authorizeHttpRequests(authorize -> authorize
// 允许匿名访问的路径
.requestMatchers("/api/v1/users/register",
"/api/v1/users/login",
"/api/v1/users/security-question",
"/api/v1/users/reset-password",
"/api/v1/captcha/**").permitAll()
// AI 相关接口允许匿名访问
.requestMatchers("/api/v1/ai-analysis/**",
"/api/v1/ai-assistant/**").permitAll()
// Swagger 文档允许访问
.requestMatchers("/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**").permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
// 添加 JWT 过滤器
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:5173"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
关键点说明:
- 禁用 CSRF 保护(前后端分离架构不需要)
- 配置白名单路径,允许匿名访问登录、注册等接口
- 使用
addFilterBefore()将 JWT 过滤器添加到 Spring Security 过滤器链中
4. 认证流程
4.1 登录流程
1. 用户发送登录请求 POST /api/v1/users/login
{
"username": "zhangsan",
"password": "password123",
"captcha": "a3f8"
}
2. UserController 接收请求,验证用户名密码
3. 验证成功,调用 JwtUtil.generateToken(username) 生成 JWT
4. 返回响应,包含 JWT 令牌
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"username": "zhangsan"
}
}
5. 前端存储 Token(localStorage 或 sessionStorage)
4.2 请求认证流程
1. 用户访问受保护资源,请求头携带 Token
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
2. JwtFilter 拦截请求,从 Header 提取 Token
3. 验证 Token 有效性(签名、过期时间)
4. 验证通过,设置 SecurityContext 认证信息
5. 请求到达 Controller,执行业务逻辑
6. 返回业务数据
5. 代码示例
5.1 登录接口实现
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@PostMapping("/login")
public UserResponse login(@Valid @RequestBody UserLoginRequest request,
HttpServletRequest httpRequest) {
// 1. 验证验证码
String captcha = request.getCaptcha();
if (!validateCaptcha(captcha, httpRequest.getSession())) {
throw new RuntimeException("验证码错误");
}
// 2. 验证用户名密码
User user = userService.login(request.getUsername(), request.getPassword());
// 3. 生成 JWT Token
String token = jwtUtil.generateToken(user.getUsername());
// 4. 构建响应
UserResponse response = new UserResponse();
response.setToken(token);
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
// ... 其他用户信息
return response;
}
}
5.2 受保护接口示例
@RestController
@RequestMapping("/api/v1/food-records")
public class FoodRecordController {
@GetMapping
public List<FoodRecord> getRecords(@RequestParam String username) {
// 由于 JwtFilter 已经验证过 Token
// 这里可以直接获取当前认证用户
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String currentUser = auth.getName();
// 验证只能查询自己的记录
if (!currentUser.equals(username)) {
throw new RuntimeException("无权访问其他用户的数据");
}
return foodRecordService.findByUsername(username);
}
}
6. 前端集成
6.1 登录后存储 Token
// 登录成功,保存 Token
const login = async (credentials) => {
const response = await api.post('/api/v1/users/login', credentials);
const { token, username } = response.data;
// 存储到 localStorage
localStorage.setItem('token', token);
localStorage.setItem('username', username);
return response.data;
};
6.2 请求拦截器添加 Token
// Axios 请求拦截器
api.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
error => Promise.reject(error)
);
6.3 响应拦截器处理 401
// Axios 响应拦截器
api.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
// Token 过期或无效,清除存储并跳转登录页
localStorage.removeItem('token');
window.location.href = '/';
}
return Promise.reject(error);
}
);
7. 安全注意事项
7.1 密钥管理
- 生产环境:密钥应存储在环境变量或配置中心,不要硬编码
- 密钥轮换:定期更换密钥,旧令牌会自然失效
- 密钥强度:使用足够强度的密钥(HS256 自动生成)
7.2 令牌安全
| 风险 | 防护措施 |
|---|---|
| XSS 攻击 | 使用 httpOnly Cookie 存储令牌,或做好 XSS 防护 |
| CSRF 攻击 | 前后端分离天然免疫 CSRF,但仍需验证 Origin |
| 令牌泄露 | 设置合理的过期时间(通常 1-24 小时) |
| 重放攻击 | 结合时间戳和随机数,或使用短期令牌 |
7.3 传输安全
- 生产环境必须使用 HTTPS
- 敏感接口增加额外的验证(如密码修改需要旧密码)
更多推荐




所有评论(0)