Spring Security自定义JWT过滤器实现令牌校验的完整流程
作者:知远漫谈

在现代 Web 应用开发中,身份认证与授权是保障系统安全的核心环节。随着微服务架构和前后端分离的流行,传统的基于 Session 的认证机制逐渐被无状态的 Token 认证方案所取代。其中,JWT(JSON Web Token)因其轻量、自包含、易于跨域等优势,成为主流的身份验证手段之一。
Spring Security 作为 Java 生态中最成熟、功能最强大的安全框架,提供了高度可扩展的认证与授权机制。虽然它原生支持多种认证方式(如表单登录、OAuth2、LDAP 等),但对 JWT 的支持并不直接内置,需要开发者通过自定义组件来集成。本文将深入探讨如何在 Spring Security 中实现一个自定义的 JWT 过滤器,完成完整的令牌校验流程,并确保安全性与可维护性。
💡 提示:本文假设你已具备 Spring Boot 和 Spring Security 的基础知识,了解基本的 REST API 开发流程。
什么是 JWT?
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用环境间安全地传递声明(claims)。它由三部分组成,以点号(.)分隔:
- Header(头部):包含令牌类型(通常是
JWT)和签名算法(如HS256或RS256)。 - Payload(载荷):包含用户信息、权限、过期时间等声明(claims)。
- Signature(签名):用于验证消息在传输过程中未被篡改。
一个典型的 JWT 看起来像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 的核心优势在于无状态:服务器无需存储会话信息,只需验证签名即可信任令牌内容。这使得它非常适合分布式系统和微服务架构。
🌐 想深入了解 JWT 结构?可以访问 jwt.io,这是一个官方提供的在线解码与调试工具,支持实时查看 Header、Payload 和 Signature。
为什么需要自定义 JWT 过滤器?
Spring Security 的认证流程基于过滤器链(Filter Chain)。默认情况下,它使用 UsernamePasswordAuthenticationFilter 处理表单登录,或 BearerTokenAuthenticationFilter(在 OAuth2 资源服务器中)处理 Bearer Token。
然而,对于自定义的 JWT 实现(例如使用 HS256 算法签名、自定义用户信息结构等),Spring Security 并不提供开箱即用的支持。因此,我们需要:
- 拦截携带 JWT 的请求(通常在
Authorization: Bearer <token>头部); - 解析并验证 JWT 的有效性(签名、过期时间等);
- 将认证信息注入 Spring Security 上下文,以便后续的授权决策使用。
这就需要我们编写一个自定义的 OncePerRequestFilter,并在 Spring Security 的过滤器链中注册它。
项目结构与依赖准备
首先,创建一个 Spring Boot 项目(推荐使用 Spring Initializr),并添加以下依赖:
<dependencies>
<!-- Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT 支持(使用 jjwt 库) -->
<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>
<!-- Lombok(可选,简化代码) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
🔍 注意:
jjwt是 Java 社区广泛使用的 JWT 库,由 Auth0 团队维护,文档完善且活跃。其 GitHub 仓库虽不能直接引用,但可通过 Maven Central 查看版本信息。
用户实体与 UserDetails 实现
为了与 Spring Security 集成,我们需要一个表示用户信息的实体类,并实现 UserDetails 接口。
1. 定义 User 实体
// com.example.demo.entity.User
package com.example.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String username;
private String password;
private String email;
private String role; // 如 "ROLE_USER", "ROLE_ADMIN"
}
2. 实现 UserDetails
// com.example.demo.security.UserPrincipal
package com.example.demo.security;
import com.example.demo.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
public class UserPrincipal implements UserDetails {
private final User user;
public UserPrincipal(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority(user.getRole()));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
// 其他方法可根据业务需求返回 true/false
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() { return true; }
public User getUser() {
return user;
}
}
UserPrincipal 封装了原始 User 对象,并实现了 Spring Security 所需的 UserDetails 接口,特别是 getAuthorities() 方法,用于返回用户的权限列表。
JWT 工具类:生成与解析令牌
接下来,我们创建一个 JwtUtil 工具类,负责 JWT 的生成、解析和验证。
// com.example.demo.security.JwtUtil
package com.example.demo.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
// 从字符串生成 SecretKey(推荐使用至少 256 位的密钥)
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
// 生成 JWT
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 从 token 中提取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 通用提取方法
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 解析并验证 token,返回 Claims
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 判断 token 是否过期
public Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 验证 token:检查用户名是否匹配且未过期
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
⚠️ 安全提示:
jwt.secret必须是一个强密钥(建议至少 32 字节),不要硬编码在代码中。应通过配置文件或环境变量注入。
在 application.yml 中配置:
jwt: secret: your-very-strong-secret-key-32-characters-minimum expiration: 86400 # 24 小时,单位秒
自定义 JWT 认证过滤器
这是本文的核心部分:实现一个自定义过滤器,用于拦截请求、提取 JWT、验证并设置认证上下文。
// com.example.demo.security.JwtAuthenticationFilter
package com.example.demo.security;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {
this.jwtUtil = jwtUtil;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 检查 Authorization 头部是否以 "Bearer " 开头
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7); // 去掉 "Bearer "
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
logger.error("JWT 解析失败: {}", e.getMessage());
// 不抛出异常,继续链式调用,让后续处理(如 401)
}
}
// 如果能提取到用户名,且当前 SecurityContext 未认证
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 验证 token 有效性
if (jwtUtil.validateToken(jwt, userDetails)) {
// 创建认证对象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置到 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
// 继续过滤器链
filterChain.doFilter(request, response);
}
}
关键点解析:
OncePerRequestFilter:确保每个请求只执行一次过滤逻辑,避免重复处理。- 提取 Token:从
Authorization头部提取 Bearer Token。 - 验证 Token:使用
JwtUtil验证签名和过期时间。 - 设置认证上下文:如果验证通过,创建
UsernamePasswordAuthenticationToken并放入SecurityContextHolder,这样后续的控制器或方法级安全注解(如@PreAuthorize)就能获取到当前用户信息。 - 异常处理:捕获 JWT 解析异常(如签名无效、格式错误),但不中断过滤器链,而是让请求继续,最终由 Spring Security 返回 401。
实现 UserDetailsService
为了让 JwtAuthenticationFilter 能加载用户信息,我们需要实现 UserDetailsService。
// com.example.demo.service.CustomUserDetailsService
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.security.UserPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class CustomUserDetailsService implements UserDetailsService {
// 模拟数据库(实际项目中应查询数据库)
private static final Map<String, User> USER_DB = new HashMap<>();
static {
USER_DB.put("alice", new User(1L, "alice", "password", "alice@example.com", "ROLE_USER"));
USER_DB.put("admin", new User(2L, "admin", "admin123", "admin@example.com", "ROLE_ADMIN"));
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = USER_DB.get(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
return new UserPrincipal(user);
}
}
🧩 在真实项目中,
USER_DB应替换为对数据库(如 JPA Repository)的查询。
配置 Spring Security
现在,我们将自定义的 JWT 过滤器集成到 Spring Security 的过滤器链中。
// com.example.demo.config.SecurityConfig
package com.example.demo.config;
import com.example.demo.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // JWT 无状态,禁用 CSRF
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无会话
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/auth/**").permitAll() // 登录接口放行
.requestMatchers("/public/**").permitAll() // 公共接口
.anyRequest().authenticated() // 其他请求需认证
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
配置说明:
csrf().disable():由于 JWT 是无状态的,CSRF 攻击不适用,可禁用。SessionCreationPolicy.STATELESS:明确告知 Spring Security 不创建 HTTP 会话。addFilterBefore:将JwtAuthenticationFilter插入到UsernamePasswordAuthenticationFilter之前,确保在表单登录处理前先尝试 JWT 认证。- 路径放行:
/auth/**用于登录接口,不应受 JWT 保护。
登录接口与 Token 分发
用户需要先通过用户名/密码登录,获取 JWT。
// com.example.demo.controller.AuthController
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.security.JwtUtil;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
private final AuthenticationManager authenticationManager;
private final CustomUserDetailsService userDetailsService;
private final JwtUtil jwtUtil;
public AuthController(AuthenticationManager authenticationManager,
CustomUserDetailsService userDetailsService,
JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtUtil = jwtUtil;
}
@PostMapping("/auth/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
// 触发 Spring Security 的认证流程
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(401).body("用户名或密码错误");
}
// 认证成功,生成 JWT
final UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(jwt));
}
public static class LoginRequest {
private String username;
private String password;
// getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
public static class JwtResponse {
private String token;
public JwtResponse(String token) { this.token = token; }
public String getToken() { return token; }
}
}
✅ 流程说明:
- 客户端发送用户名/密码到
/auth/login;- 使用
AuthenticationManager进行认证(底层调用UserDetailsService和密码编码器);- 认证成功后,调用
JwtUtil.generateToken()生成 JWT;- 返回
{"token": "xxx"}。
测试受保护的 API
创建一个简单的受保护接口:
// com.example.demo.controller.ProfileController
package com.example.demo.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProfileController {
@GetMapping("/api/profile")
public String profile(@AuthenticationPrincipal UserDetails userDetails) {
return "Hello, " + userDetails.getUsername() + "! Your role: " +
userDetails.getAuthorities();
}
}
测试步骤:
登录获取 Token:
curl -X POST http://localhost:8080/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"alice","password":"password"}'响应:
{"token":"eyJhbGciOiJIUzI1NiJ9..."}使用 Token 访问受保护资源:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \ http://localhost:8080/api/profile响应:
Hello, alice! Your role: [ROLE_USER]
无效 Token 测试:
- 修改 Token 任意字符 → 返回 401
- 使用过期 Token → 返回 401
- 不带 Token → 返回 401
安全增强建议
虽然上述实现已满足基本需求,但在生产环境中还需考虑以下安全措施:
1. 使用强密钥
确保 jwt.secret 是高熵随机字符串(至少 32 字节)。可使用以下命令生成:
openssl rand -base64 32
2. 添加 Token 黑名单(可选)
JWT 一旦签发,在过期前始终有效。若需支持“立即注销”,可引入 Redis 存储已注销的 Token 及其过期时间,在验证时检查黑名单。
3. 防止重放攻击
可在 JWT Payload 中加入 jti(JWT ID)和 iat(签发时间),并在服务端记录最近使用的 jti,拒绝重复使用。
4. HTTPS 强制启用
JWT 通过 HTTP 头部传输,必须使用 HTTPS 防止中间人窃取。
5. 限制 Token 有效期
短期 Token(如 15-30 分钟)配合 Refresh Token 机制更安全。本文未实现 Refresh Token,但可参考 Auth0 的最佳实践。
整体认证流程图
让我们用 Mermaid 图表直观展示整个 JWT 认证流程:

该图清晰展示了从登录到访问受保护资源的完整流程,以及各组件之间的协作关系。
常见问题与排查
Q1: 为什么过滤器没有生效?
- 检查是否在
SecurityConfig中正确调用了addFilterBefore; - 确保
JwtAuthenticationFilter被 Spring 扫描到(有@Component注解); - 查看日志,确认过滤器是否被调用。
Q2: Token 验证总是失败?
- 检查
jwt.secret是否一致(生成和验证使用同一密钥); - 确认 Token 是否过期;
- 使用 jwt.io 解码 Token,检查 Payload 内容是否符合预期。
Q3: 如何支持多角色授权?
在 UserPrincipal.getAuthorities() 中返回多个 GrantedAuthority,例如:
Arrays.asList(
new SimpleGrantedAuthority("ROLE_USER"),
new SimpleGrantedAuthority("SCOPE_READ")
);
然后在控制器使用:
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin")
public String adminOnly() { ... }
Q4: 能否在 JWT 中存储更多信息?
可以!在 JwtUtil.createToken() 的 claims 中添加自定义字段:
claims.put("userId", user.getId());
claims.put("email", user.getEmail());
解析时通过 extractClaim(token, claims -> claims.get("userId", Long.class)) 获取。
性能考量
- JWT 解析开销:每次请求都要解析和验证 JWT,但
jjwt库性能良好,通常不是瓶颈。 - 避免频繁数据库查询:在
JwtAuthenticationFilter中,每次请求都会调用UserDetailsService.loadUserByUsername()。若用户信息不常变,可考虑缓存(如 Caffeine 或 Redis)。 - 无状态优势:省去了 Session 存储和集群同步的开销,适合高并发场景。
扩展:Refresh Token 机制
虽然本文聚焦于基础 JWT 实现,但实际应用中常需 Refresh Token 来延长会话。其基本思路是:
- 登录时返回两个 Token:
access_token(短有效期)和refresh_token(长有效期); access_token过期后,客户端用refresh_token请求新的access_token;refresh_token应存储在服务端(如数据库),支持撤销。
📚 想了解 Refresh Token 的详细实现?推荐阅读 OAuth 2.0 RFC 6749 中的相关章节。
总结
通过本文,我们完整实现了一个基于 Spring Security 的自定义 JWT 认证方案。关键步骤包括:
- 理解 JWT 原理及其在无状态认证中的优势;
- 实现
UserDetails和UserDetailsService,桥接业务用户与 Spring Security; - 编写
JwtUtil,封装 JWT 的生成与验证逻辑; - 创建
JwtAuthenticationFilter,在请求进入时自动完成认证; - 配置 Spring Security,禁用 Session、CSRF,并注册自定义过滤器;
- 提供登录接口,分发 JWT;
- 保护 API 资源,利用 Spring Security 的授权机制。
这套方案结构清晰、扩展性强,适用于大多数中小型 Web 应用。当然,安全是一个持续演进的过程,开发者应根据实际需求不断加固系统。
🌟 最后提醒:永远不要在 JWT 中存储敏感信息(如密码、身份证号),因为 Payload 是 Base64 编码,可被轻易解码!
到此这篇关于Spring Security自定义JWT过滤器实现令牌校验的文章就介绍到这了,更多相关Spring Security实现令牌校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
