Spring Boot 接口鉴权的常见误区是“能用就行”:把 token 校验写进拦截器,再手动复制到每个服务。业务一多,鉴权逻辑散落各处,改一处漏三处。本文按 JWT 无状态认证、过滤器链、注解权限控制、异常统一处理四层给出可落地的方案。
JWT无状态认证原理与Token结构
JWT(JSON Web Token)由 Header、Payload、Signature 三段 Base64 编码组成,服务端不保存会话状态,token 自带用户身份与过期时间,适合分布式部署的接口鉴权。
// Header: {"alg":"HS256","typ":"JWT"}
// Payload: {"sub":"1001","name":"matao","role":"admin","exp":1765238400}
// Signature: HMACSHA256(base64(header) + "." + base64(payload), secret)
签名密钥只留在服务端,token 由客户端持有,每次请求通过 Authorization 头携带。服务端校验签名与过期时间即可完成鉴权,无需查询数据库,天然支持水平扩容。
JWT工具类:生成、解析与过期校验
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire-hours:8}")
private long expireHours;
public String generate(String userId, String role) {
return Jwts.builder()
.setSubject(userId)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireHours * 3600_000L))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Claims parse(String token) {
return Jwts.parser().setSigningKey(secret)
.parseClaimsJws(token).getBody();
}
}
解析异常分两类处理:ExpiredJwtException 返回 401 提示登录过期,SignatureException 返回 401 提示令牌非法。不要把敏感日志堆在异常里,统一走全局异常处理器。
Spring Security过滤器链:OncePerRequestFilter注入鉴权
在 Spring Security 6 中基于 SecurityFilterChain 配置,把 JWT 校验放在 UsernamePasswordAuthenticationFilter 之前,通过 OncePerRequestFilter 实现一次性过滤:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtAuthFilter jwtFilter) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
过滤器核心逻辑:从请求头取 token,校验通过后把用户信息塞进 SecurityContext,后续业务代码通过 SecurityContextHolder 获取当前用户:
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
@Autowired private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)
throws ServletException, IOException {
String auth = req.getHeader("Authorization");
if (auth != null && auth.startsWith("Bearer ")) {
try {
Claims claims = jwtUtil.parse(auth.substring(7));
var authToken = new UsernamePasswordAuthenticationToken(
claims.getSubject(), null,
List.of(new SimpleGrantedAuthority("ROLE_" + claims.get("role"))));
SecurityContextHolder.getContext().setAuthentication(authToken);
} catch (Exception e) {
SecurityContextHolder.clearContext();
}
}
chain.doFilter(request, resp);
}
}
鉴权失败的两种路径:token 缺失时走匿名访问,被 authenticated() 规则拦截返回 401;token 非法时 SecurityContext 为空同样返回 401。权限细分用 hasRole 控制接口级别。
方法级鉴权:@PreAuthorize精确到业务方法
URL 级鉴权粒度不够时,在方法上用 @PreAuthorize 声明权限,需要先开启全局方法安全:
@EnableMethodSecurity
public class MethodSecurityConfig {}
@RestController
@RequestMapping("/api/order")
public class OrderController {
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.name")
public Order getOrder(@PathVariable Long id) {
return orderService.findById(id);
}
}
业务规则写在方法上,比 URL 规则更贴近数据边界。例如“订单只能本人或管理员查看”,用 SpEL 表达式直接表达,代码可读性明显提升。
登录接口与统一异常响应
登录接口校验用户名密码后签发 token,同时处理失败次数与锁定,保持响应结构统一:
@PostMapping("/login")
public ApiResponse<String> login(@RequestBody LoginRequest req) {
User user = userService.authenticate(req.getUsername(), req.getPassword());
String token = jwtUtil.generate(user.getUsername(), user.getRole());
return ApiResponse.ok(token);
}
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiResponse<Void>> badCredentials(BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(401, "用户名或密码错误"));
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<Void>> denied(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error(403, "无权访问"));
}
}
统一异常响应保证前端处理逻辑单一,401 与 403 语义清晰,前端拿到 401 直接跳登录页,403 提示无权限。
JWT鉴权的常见坑与规避
几个高频踩坑点:
- 令牌泄露:不要在 URL 参数传 token,统一用 Authorization 头;前端存内存或 httpOnly cookie,避免 XSS 窃取。
- 吊销问题:JWT 无状态意味着服务端无法主动吊销,紧急封号场景需要黑名单缓存(Redis 存被吊销 token 直到过期)。
- 密钥管理:secret 用环境变量注入,别写死在代码或配置仓库里,定期轮换。
- 时钟偏差:签发与校验方时钟不同步会误判过期,服务端集群统一走 NTP 校准。
这套方案覆盖登录签发、请求校验、权限控制、异常响应完整链路,单服务与网关转发场景都适用。微服务拆分后,把 JwtAuthFilter 下沉到公共依赖,各服务保持一致行为即可。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-jie-kou-jian-quan-shi-zhan-jwt-wu-zhuang-tai-ren/