Spring Boot接口鉴权实战:JWT无状态认证与过滤器链设计

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/

(0)
小编小编
上一篇 2小时前
下一篇 2小时前

相关推荐