概述
在现代微服务与前后端分离架构中,无状态的JWT(JSON Web Token)认证方式已成为主流选择。Spring Security 作为Java生态下强大的安全框架,通过自定义过滤器可以优雅地集成JWT认证。
本文将聚焦于如何构建一个自定义的 JwtFilter,负责从请求中提取并校验JWT,进而完成认证信息的设置,实现完全无状态的API安全保护。
纲要
- 核心概念:
JwtFilter、SecurityContextHolder、OncePerRequestFilter - 关键流程:检查请求头 → 提取Token → 解析Token → 构建认证对象 → 写入安全上下文
- 涉及组件:
JwtProperties:可配置的请求头名称与Token前缀JwtUtils:Token解析工具JwtFilter:认证过滤器实现SecurityConfig:将过滤器注册到过滤器链
- 补充内容:完整的代码实现、项目结构、流程序列图
背景与设计思路
在Spring Security体系中,认证信息最终会被封装为一个 Authentication 对象并存储在 SecurityContextHolder 中。对于JWT这种无状态认证,每个请求都必须携带一个有效Token。自定义过滤器需要拦截所有请求,完成以下工作:
- 从HTTP请求的指定头(默认为
Authorization)中取出Token; - 验证Token格式(是否以指定前缀开头,如
Bearer); - 解析Token并提取声明(claims),如用户名、角色等;
- 利用声明构造
UsernamePasswordAuthenticationToken并标记为已认证; - 将认证对象设置到
SecurityContext中,供后续过滤器及业务代码使用。
若Token非法或缺失,则应清空安全上下文,保证不会遗留错误的认证状态。整个过程无需数据库查询,因为JWT的自包含特性已经携带了所需的用户身份与权限信息。
项目结构
下面展示本文示例代码的包结构,所有类均放在 com.example.security.jwt 包下。
com.example.security.jwt
├── JwtProperties.java
├── JwtUtils.java
├── JwtFilter.java
└── SecurityConfig.java
核心代码实现
可配置的属性类 JwtProperties
为了让请求头的名称和Token前缀可灵活调整,我们将其抽取为配置属性。这样做的好处是,如果未来需要改用 X-Auth-Token 或自定义前缀,只需修改配置文件即可。
package com.example.security.jwt;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
/** 存放JWT的请求头名称,默认为 Authorization */
private String header = "Authorization";
/** Token的前缀,默认为 "Bearer " (注意末尾空格) */
private String tokenPrefix = "Bearer ";
/** JWT签名密钥,实际使用时应为高强度随机字符串并外部化 */
private String secret = "DefaultSecretKey";
// getters and setters...
public String getHeader() { return header; }
public void setHeader(String header) { this.header = header; }
public String getTokenPrefix() { return tokenPrefix; }
public void setTokenPrefix(String tokenPrefix) { this.tokenPrefix = tokenPrefix; }
public String getSecret() { return secret; }
public void setSecret(String secret) { this.secret = secret; }
}
JWT工具类 JwtUtils
提供解析JWT并返回声明(Claims)的能力。此处为简化展示,直接使用静态工具方法;实际项目可将密钥通过 JwtProperties 注入,避免静态持有密钥。
package com.example.security.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class JwtUtils {
private final JwtProperties jwtProperties;
public JwtUtils(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}
/**
* 解析JWT令牌,返回包含声明的Optional对象。
* 若令牌无效或签名不匹配,则返回空Optional。
*/
public Optional<Claims> parseToken(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(jwtProperties.getSecret().getBytes())
.build()
.parseClaimsJws(token)
.getBody();
return Optional.of(claims);
} catch (JwtException e) {
return Optional.empty();
}
}
}
类型转换辅助工具
JWT的声明中,“authorities”字段通常以List形式存储,但解析出来可能是 ArrayList 或JSON数组,需要统一处理为List后再转换为 SimpleGrantedAuthority。下面提供一个简单的转换工具方法,放在 JwtFilter 同级包下即可。
package com.example.security.jwt;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class CollectionUtil {
@SuppressWarnings("unchecked")
public static List<?> convertObjectToList(Object obj) {
if (obj instanceof List) {
return (List<?>) obj;
} else if (obj instanceof Collection) {
return new ArrayList<>((Collection<?>) obj);
} else if (obj != null && obj.getClass().isArray()) {
return new ArrayList<>(List.of((Object[]) obj));
}
return new ArrayList<>();
}
}
认证过滤器 JwtFilter
这是整个JWT认证的核心。过滤器继承自 OncePerRequestFilter,确保每个请求只会被该过滤器处理一次。
package com.example.security.jwt;
import io.jsonwebtoken.Claims;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class JwtFilter extends OncePerRequestFilter {
private final JwtProperties jwtProperties;
private final JwtUtils jwtUtils;
public JwtFilter(JwtProperties jwtProperties, JwtUtils jwtUtils) {
this.jwtProperties = jwtProperties;
this.jwtUtils = jwtUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 第一步:检查请求是否携带合法的JWT头
if (isJwtHeaderPresent(request)) {
// 第二、三步:解析Token并设置安全上下文
parseAndSetAuthentication(request);
} else {
// 未携带合法头部时,清空上下文,避免线程残留
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
/**
* 检查请求头中是否存在合法的JWT头部。
* 判断依据:头部值不为空,且以配置的前缀开头。
*/
private boolean isJwtHeaderPresent(HttpServletRequest request) {
String headerValue = request.getHeader(jwtProperties.getHeader());
return headerValue != null && headerValue.startsWith(jwtProperties.getTokenPrefix());
}
/**
* 从请求头中提取Token,解析并构建认证对象设置到安全上下文。
*/
private void parseAndSetAuthentication(HttpServletRequest request) {
String headerValue = request.getHeader(jwtProperties.getHeader());
// 去除前缀获得纯Token
String token = headerValue.replace(jwtProperties.getTokenPrefix(), "");
Optional<Claims> claimsOptional = jwtUtils.parseToken(token);
claimsOptional.ifPresentOrElse(
this::setupAuthentication, // 有值的情况
SecurityContextHolder::clearContext // 解析失败或无Claims,清空上下文
);
}
/**
* 使用解析出的 Claims 构造认证对象并写入 SecurityContext。
*/
private void setupAuthentication(Claims claims) {
// 提取角色列表,并转换为 GrantedAuthority 集合
List<?> rawAuthorities = CollectionUtil.convertObjectToList(claims.get("authorities"));
List<SimpleGrantedAuthority> authorities = rawAuthorities.stream()
.map(String::valueOf)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 构建已认证的 Authentication 对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
claims.getSubject(), // principal: 用户名
null, // credentials: 无需凭据
authorities // 权限集合
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
代码关键点:
isJwtHeaderPresent仅做格式校验,不涉及签名验证。- 提取Token后,利用
JwtUtils解析出Claims。 ifPresentOrElse方法将处理逻辑分为有值与无值两条路径,符合函数式风格,代码清晰。- 认证对象通过
UsernamePasswordAuthenticationToken的三参数构造器创建,该构造器会调用setAuthenticated(true),表示已认证。 - 若无有效Claims,则清空
SecurityContext,防止上下文污染。
配置类 SecurityConfig
必须将自定义的 JwtFilter 添加到Spring Security的过滤器链中,通常置于 UsernamePasswordAuthenticationFilter 之前,以便提前完成JWT认证。
package com.example.security.jwt;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig {
private final JwtProperties jwtProperties;
private final JwtUtils jwtUtils;
public SecurityConfig(JwtProperties jwtProperties, JwtUtils jwtUtils) {
this.jwtProperties = jwtProperties;
this.jwtUtils = jwtUtils;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态
.and()
.authorizeRequests()
.antMatchers("/auth/login").permitAll() // 假设登录接口公开
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter(jwtProperties, jwtUtils);
}
}
至此,JWT过滤器及其配套组件已经搭建完毕。当客户端访问受保护资源时,只需在请求头中携带 Authorization: Bearer <your_jwt> 即可通过认证。
流程可视化
以下序列图展示了 JwtFilter 工作的核心流程:
测试验证
你可以通过以下步骤验证过滤器的正确性:
- 启动应用,向
/auth/login发送登录请求,获得JWT Token(该端点及Token生成逻辑需自行实现)。 - 使用Postman或curl访问其他受保护接口,请求头设置:
Authorization: Bearer <your_token> - 若不携带或携带错误Token,应当收到
403响应,且日志中不会看到异常(过滤器已清空上下文,后续权限校验会拒绝访问)。 - 携带正确Token,可正常访问资源,并在业务代码中通过
SecurityContextHolder.getContext().getAuthentication()获取当前用户信息。
总结
本文详细介绍了在Spring Security框架下自定义JWT认证过滤器的实现方式。通过对请求头的检测、Token的解析以及安全上下文的设置,我们构建了一个无状态、轻量级的认证机制。
文中所有代码均可直接复制运行(需引入 jjwt 等依赖),并遵循了配置外部化、关注点分离的最佳实践。

1242

被折叠的 条评论
为什么被折叠?



