1. 为什么选择JWT+Spring Security组合
在构建现代Web应用时,认证授权是绕不开的核心模块。我经历过多个项目的身份认证方案迭代,从早期的Session-Cookie到OAuth2.0,最终JWT+Spring Security的组合成为了我的首选方案。这个组合之所以能胜出,关键在于它完美平衡了安全性、扩展性和开发效率。
JWT(JSON Web Token)的本质是一种紧凑的URL安全声明表示方式。与传统的Session ID不同,JWT将用户信息直接编码在Token中,服务端无需维护会话状态。我曾在一个分布式电商项目中实测,采用JWT后认证服务的QPS提升了近3倍,因为不再需要频繁查询Redis获取会话数据。
Spring Security则是Java生态中最成熟的认证授权框架。最新5.7版本对OAuth2和JWT的支持已经非常完善。它的强大之处在于:
- 内置CSRF、XSS等安全防护
- 细粒度的权限控制(方法级、URL级)
- 与Spring生态无缝集成
- 高度可扩展的过滤器链
实际踩坑经验:Spring Security的默认配置往往不能满足生产需求,必须根据业务特点进行定制。比如默认的PasswordEncoder在5.0后改为必须显式配置,这点容易忽略导致启动报错。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目环境搭建与基础配置
2.1 依赖引入与版本选择
在Spring Boot 2.7.x项目中,需要添加以下核心依赖:
xml复制<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 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>
版本选择上有个坑需要注意:jjwt 0.10.x与1.0.x的API有不兼容改动。建议使用0.11.x这个过渡版本,既修复了安全漏洞又保持API稳定。
2.2 安全配置类骨架
创建基础安全配置类时,我推荐继承WebSecurityConfigurerAdapter(Spring Security 5.7仍支持,但未来版本可能移除):
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
关键配置解析:
STATELESS会话策略:声明我们使用无状态JWT- CSRF禁用:REST API通常不需要CSRF保护
- 过滤器顺序:JWT过滤器要在用户名密码认证前执行
3. JWT核心实现细节
3.1 Token生成与校验
JWT工具类需要处理三个核心操作:
java复制public class JwtTokenProvider {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration-ms}")
private int jwtExpirationMs;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (SignatureException e) {
log.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
} // 其他异常捕获...
return false;
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
安全注意事项:
- 密钥长度:HS512算法要求密钥至少64字节
- Claim设置:不要存放敏感信息如密码
- 时钟偏移:服务器间时间同步很重要
3.2 Token续签方案
JWT的固定有效期是个双刃剑。通过实践我总结出两种续签策略:
方案A:双Token机制
- access_token:短有效期(如30分钟)
- refresh_token:长有效期(如7天)
- 当access_token过期时,用refresh_token获取新token
方案B:滑动过期窗口
- 每次请求检查token剩余有效期
- 若有效期小于阈值(如15分钟),则签发新token
- 响应头携带新token:
X-New-Token: xxx
实测发现方案B实现更简单,但要注意防止token滥用。我通常在Redis中记录最近签发的token,避免短时间内重复签发。
4. Spring Security深度集成
4.1 认证过滤器实现
JWT认证过滤器的核心职责是从请求头提取token并构建认证上下文:
java复制public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = parseJwt(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
4.2 方法级权限控制
除了URL级别的antMatchers,Spring Security还支持方法级注解:
java复制@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// ...
}
@PostAuthorize("returnObject.owner == authentication.name")
@GetMapping("/documents/{id}")
public Document getDocument(@PathVariable String id) {
// ...
}
这些SpEL表达式非常灵活,但要注意:
- 需要启用方法安全:
@EnableGlobalMethodSecurity(prePostEnabled = true) - 复杂表达式会影响性能
- 与缓存注解组合使用时要注意顺序
5. 生产环境进阶配置
5.1 权限缓存优化
频繁查询用户权限会影响性能。我的解决方案是:
java复制@Service
@RequiredArgsConstructor
public class CachingUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
private final CacheManager cacheManager;
@Override
@Cacheable(value = "userDetails", key = "#username")
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(...));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
mapRolesToAuthorities(user.getRoles()));
}
public void evictUserCache(String username) {
cacheManager.getCache("userDetails").evict(username);
}
}
配合Redis缓存,用户权限查询耗时从平均50ms降到了2ms。注意在用户权限变更时要及时清除缓存。
5.2 接口访问日志与审计
结合Spring AOP记录权限校验日志:
java复制@Aspect
@Component
@Slf4j
public class SecurityLoggingAspect {
@AfterReturning(
pointcut = "@annotation(org.springframework.security.access.prepost.PreAuthorize)",
returning = "result")
public void logAuthorizedAccess(JoinPoint jp, Object result) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
log.info("用户 {} 访问了 {},参数:{}",
auth.getName(),
jp.getSignature().toShortString(),
Arrays.toString(jp.getArgs()));
}
@AfterThrowing(
pointcut = "@annotation(org.springframework.security.access.prepost.PreAuthorize)",
throwing = "ex")
public void logAuthorizationFailure(JoinPoint jp, AccessDeniedException ex) {
// 记录未授权访问尝试
}
}
这个切面可以帮助我们:
- 监控异常访问行为
- 分析接口使用情况
- 审计敏感操作
6. 常见问题排查指南
6.1 过滤器顺序混乱
Spring Security的过滤器链有严格顺序。如果自定义过滤器位置不对,会导致各种奇怪问题。正确的调试方法是:
java复制@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> jwtFilterRegistration(
JwtAuthenticationFilter filter) {
FilterRegistrationBean<JwtAuthenticationFilter> registration =
new FilterRegistrationBean<>(filter);
registration.setEnabled(false); // 禁止重复注册
return registration;
}
这样能确保过滤器只通过addFilterBefore添加一次。
6.2 CORS与预检请求
当遇到OPTIONS请求被拦截时,需要调整CORS配置:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://example.com"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
return config;
});
// 其他配置...
}
6.3 权限校验不生效
如果@PreAuthorize不生效,检查以下方面:
- 主配置类是否添加
@EnableGlobalMethodSecurity - 方法是否在同一个Bean中被调用(自调用失效)
- 代理模式是否正确(CGLIB vs JDK动态代理)
我在实际项目中还遇到过Spring Cloud Feign客户端调用时权限上下文丢失的问题,解决方案是通过RequestInterceptor传递安全上下文:
java复制@Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return requestTemplate -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getDetails() instanceof Jwt) {
Jwt jwt = (Jwt) authentication.getDetails();
requestTemplate.header("Authorization", "Bearer " + jwt.getTokenValue());
}
};
}
7. 安全加固建议
7.1 JWT安全最佳实践
- 密钥管理:不要硬编码密钥,使用KMS或Vault动态获取
- 算法选择:优先使用RS256等非对称算法
- Token撤销:维护黑名单应对提前失效需求
- Claims校验:严格校验iss、aud等标准声明
7.2 Spring Security加固配置
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.contentSecurityPolicy("script-src 'self'")
.and()
.frameOptions().deny()
.and()
.sessionManagement()
.sessionFixation().migrateSession()
.and()
.requiresChannel()
.requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
.requiresSecure();
}
这些配置可以防御:
- XSS攻击(Content-Security-Policy)
- 点击劫持(X-Frame-Options)
- 会话固定攻击
- HTTP明文传输
7.3 监控与告警
建议监控以下指标:
- 认证失败频率
- Token签发速率
- 权限校验失败次数
- 异常IP访问模式
通过Prometheus+Grafana搭建的监控看板示例配置:
yaml复制# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
在微服务架构下,还需要考虑分布式追踪,将认证链路与业务调用关联分析。
