目录
六、实战案例:Spring Security + JWT完整实现
一、JWT令牌方案详解
1. JWT基本概念
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),用于在网络应用环境间安全地传递声明。JWT被设计为紧凑且自包含的方式,特别适用于分布式站点的单点登录(SSO)场景。
JWT的组成结构
JWT由三部分组成,用点(.)分隔:
- Header (头部)
- Payload (负载)
- Signature (签名)
格式为:xxxxx.yyyyy.zzzzz
1.1 Header (头部) 通常由两部分组成:
- 令牌类型 (typ):JWT
- 签名算法 (alg):如HMAC SHA256或RSA
示例:
{
"alg": "HS256",
"typ": "JWT"
}
1.2 Payload (负载) 包含声明(claims),声明是关于实体(通常是用户)和附加数据的语句。有三种类型的声明:
- 注册声明 (Registered claims):预定义的声明,如iss(签发者), exp(过期时间), sub(主题), aud(受众)等
- 公开声明 (Public claims):可以自定义,但为避免冲突应在IANA JSON Web Token Registry中定义
- 私有声明 (Private claims):自定义声明,用于在同意使用它们的各方之间共享信息
示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
1.3 Signature (签名) 签名用于验证消息在传递过程中没有被篡改。创建签名需要:
- 编码后的header
- 编码后的payload
- 一个密钥
- header中指定的算法
例如使用HMAC SHA256算法:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
2. JWT工作流程
- 用户使用凭据登录系统
- 服务器验证凭据并创建JWT返回给客户端
- 客户端存储JWT(通常在localStorage或cookie中)
- 客户端在后续请求的Authorization头中携带JWT
- 服务器验证JWT并处理请求
- 客户端收到响应
3. JWT的优势与劣势
优势:
- 无状态:服务器不需要存储会话信息
- 跨域支持:适合单点登录和分布式系统
- 安全性:基于签名,防止篡改
- 灵活性:可以包含自定义信息
- 标准化:遵循RFC标准,多种语言支持
劣势:
- 令牌一旦签发,在有效期内无法撤销
- 负载大小受限(通常不超过8KB)
- 需要自行处理令牌刷新逻辑
- 安全问题:如果泄露,攻击者可以在有效期内使用
4. JWT实现细节
4.1 令牌生成
// Java示例使用jjwt库
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(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
4.2 令牌验证
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
4.3 刷新令牌机制
通常实现两种令牌:
- 访问令牌(Access Token):短期有效(如30分钟)
- 刷新令牌(Refresh Token):长期有效(如7天)
刷新流程:
- 客户端使用过期访问令牌请求资源
- 服务器返回401未授权
- 客户端使用刷新令牌请求新的访问令牌
- 服务器验证刷新令牌并返回新访问令牌
- 客户端使用新访问令牌重试原始请求
5. JWT安全最佳实践
- 使用HTTPS传输JWT
- 设置合理的过期时间
- 不要存储敏感信息在payload中
- 使用强密钥(至少256位)
- 实现令牌黑名单机制(用于注销场景)
- 防范CSRF攻击(使用SameSite cookie属性)
- 防范XSS攻击(避免localStorage存储敏感令牌)
- 实现速率限制防止暴力破解
二、拦截器(Interceptor)详解
1. 拦截器基本概念
拦截器是一种AOP(面向切面编程)实现,用于在请求处理前后执行特定逻辑。在Web应用中,拦截器通常用于:
- 权限验证
- 日志记录
- 性能监控
- 通用行为注入
2. Spring拦截器实现
2.1 创建拦截器类
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 在控制器方法执行前调用
String token = request.getHeader("Authorization");
try {
// 验证令牌
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody();
// 将用户信息存入请求属性
request.setAttribute("userId", claims.getSubject());
return true;
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return false;
}
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// 控制器方法执行后,视图渲染前调用
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
// 请求完成后调用,用于资源清理
}
}
2.2 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor())
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/auth/login"); // 排除路径
}
}
3. 拦截器与过滤器的区别
特性 | 拦截器(Interceptor) | 过滤器(Filter) |
---|---|---|
所属规范 | Spring MVC特有 | Java Servlet规范 |
执行位置 | Controller方法前后 | Servlet处理前和后 |
依赖 | 依赖Spring容器 | 不依赖任何框架 |
获取上下文 | 可以获取Spring上下文 | 无法获取Spring上下文 |
异常处理 | 可以访问Controller抛出的异常 | 无法访问Controller抛出的异常 |
实现方式 | 实现HandlerInterceptor接口 | 实现javax.servlet.Filter接口 |
配置方式 | 通过WebMvcConfigurer配置 | 通过web.xml或@WebFilter注解配置 |
4. 拦截器应用场景
- 认证与授权:验证JWT令牌,检查用户权限
- 日志记录:记录请求信息、执行时间等
- 性能监控:统计方法执行时间
- 通用数据处理:如设置本地化信息、主题等
- 防重复提交:基于令牌的防重复提交机制
- 参数预处理:统一处理请求参数
5. 拦截器高级用法
5.1 多拦截器顺序控制
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()).order(1);
registry.addInterceptor(new AuthInterceptor()).order(2);
registry.addInterceptor(new PerformanceInterceptor()).order(3);
}
5.2 基于注解的拦截
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
String[] value();
}
// 拦截器中使用
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequiresPermission annotation = handlerMethod.getMethodAnnotation(RequiresPermission.class);
if (annotation != null) {
// 检查权限
}
}
三、过滤器(Filter)详解
1. 过滤器基本概念
过滤器是Java Servlet规范的一部分,用于在请求到达Servlet之前或响应发送到客户端之前对请求和响应进行预处理和后处理。
2. 过滤器实现
2.1 创建过滤器类
@WebFilter("/*")
public class JwtFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化方法
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String path = httpRequest.getRequestURI().substring(httpRequest.getContextPath().length());
// 排除登录等不需要验证的路径
if (path.startsWith("/api/auth/")) {
chain.doFilter(request, response);
return;
}
String token = httpRequest.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing or invalid token");
return;
}
try {
// 验证令牌
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token.substring(7))
.getBody();
// 将用户信息存入请求属性
httpRequest.setAttribute("userId", claims.getSubject());
chain.doFilter(request, response);
} catch (Exception e) {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
}
}
@Override
public void destroy() {
// 销毁方法
}
}
2.2 注册过滤器
在Spring Boot中,可以通过以下方式注册:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<JwtFilter> jwtFilter() {
FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/api/*");
registrationBean.setOrder(1); // 设置过滤器顺序
return registrationBean;
}
}
3. 过滤器应用场景
- 认证与授权:基础认证、JWT验证
- 跨域处理:设置CORS头
- 编码设置:统一请求和响应编码
- XSS防护:过滤请求参数中的恶意脚本
- 敏感词过滤:过滤请求和响应中的敏感词
- 请求/响应日志:记录完整的请求和响应
- 性能监控:统计请求处理时间
- 压缩处理:对响应进行GZIP压缩
4. 过滤器链机制
多个过滤器可以组成过滤器链,按照web.xml中定义的顺序或@Order注解的顺序执行:
请求 -> Filter1 -> Filter2 -> ... -> FilterN -> Servlet -> FilterN -> ... -> Filter2 -> Filter1 -> 响应
5. 过滤器高级用法
5.1 包装请求和响应
public class RequestWrapper extends HttpServletRequestWrapper {
// 实现自定义逻辑,如参数过滤等
}
public class ResponseWrapper extends HttpServletResponseWrapper {
// 实现自定义逻辑,如响应内容修改等
}
// 在过滤器中
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
RequestWrapper requestWrapper = new RequestWrapper((HttpServletRequest) request);
ResponseWrapper responseWrapper = new ResponseWrapper((HttpServletResponse) response);
chain.doFilter(requestWrapper, responseWrapper);
}
5.2 异步请求处理
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
AsyncContext context = request.startAsync();
context.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
// 异步完成处理
}
// 实现其他方法...
});
chain.doFilter(request, response);
}
四、JWT与拦截器/过滤器的最佳实践组合
1. 完整认证流程设计
-
登录流程:
- 客户端提交凭据
- 服务器验证凭据并生成JWT(访问令牌+刷新令牌)
- 返回令牌给客户端
-
访问受保护资源:
- 客户端在Authorization头中携带访问令牌
- 过滤器/拦截器验证令牌有效性
- 验证通过后允许访问资源
-
令牌刷新流程:
- 客户端使用过期访问令牌请求资源
- 服务器返回401
- 客户端使用刷新令牌获取新访问令牌
- 服务器验证刷新令牌并返回新访问令牌
2. 安全增强方案
2.1 双令牌机制
// 生成令牌对
public TokenPair generateTokenPair(UserDetails userDetails) {
String accessToken = generateAccessToken(userDetails);
String refreshToken = generateRefreshToken(userDetails);
return new TokenPair(accessToken, refreshToken);
}
private String generateAccessToken(UserDetails userDetails) {
// 短期有效的访问令牌
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
private String generateRefreshToken(UserDetails userDetails) {
// 长期有效的刷新令牌
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
2.2 令牌黑名单
// 令牌注销服务
@Service
public class TokenBlacklistService {
private final Set<String> blacklist = Collections.synchronizedSet(new HashSet<>());
public void addToBlacklist(String token) {
blacklist.add(token);
}
public boolean isBlacklisted(String token) {
return blacklist.contains(token);
}
}
// 在过滤器中检查
if (tokenBlacklistService.isBlacklisted(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token revoked");
return;
}
3. 性能优化建议
- 使用高效的签名算法:HS256比RS256性能更好
- 减少payload大小:只存储必要信息
- 缓存公钥:如果使用RS256,缓存公钥避免重复解析
- 异步验证:对于高并发系统,考虑异步验证机制
- 分布式缓存:对于黑名单等,使用Redis等分布式缓存
4. 微服务架构中的JWT实践
在微服务架构中,JWT特别适合:
- API网关统一认证:在网关层验证JWT,然后转发请求到微服务
- 服务间认证:服务间调用携带JWT
- 声明传递:通过JWT payload传递用户上下文
// 网关过滤器示例
public class GatewayJwtFilter implements GatewayFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
try {
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token.substring(7))
.getBody();
// 添加用户信息到请求头
exchange.getRequest().mutate()
.header("X-User-Id", claims.getSubject())
.build();
return chain.filter(exchange);
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
}
五、常见问题与解决方案
1. JWT安全问题
问题1:令牌泄露
- 解决方案:设置短有效期,使用刷新令牌机制,实现令牌黑名单
问题2:XSS攻击窃取令牌
- 解决方案:使用HttpOnly的Secure Cookie存储令牌,而不是localStorage
问题3:CSRF攻击
- 解决方案:使用SameSite Cookie属性,添加CSRF令牌
2. 性能问题
问题1:频繁的JWT验证开销
- 解决方案:在验证后缓存验证结果,设置合理的缓存时间
问题2:大负载影响性能
- 解决方案:保持payload精简,只存储必要信息
3. 分布式系统问题
问题1:注销困难
- 解决方案:实现分布式令牌黑名单(如使用Redis)
问题2:时钟漂移导致验证失败
- 解决方案:允许一定的时间偏差(如jjwt的setAllowedClockSkewSeconds)
4. 移动端问题
问题1:令牌安全存储
- 解决方案:使用移动端安全存储机制(如Android的Keystore, iOS的Keychain)
问题2:网络不稳定导致令牌刷新失败
- 解决方案:实现令牌预刷新机制,在令牌即将过期前主动刷新
六、实战案例:Spring Security + JWT完整实现
1. 依赖配置
<dependencies>
<!-- Spring Boot Web -->
<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支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>
2. JWT工具类
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
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() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
public List<SimpleGrantedAuthority> getRolesFromToken(String token) {
List<String> roles = getClaimsFromToken(token).get("roles", List.class);
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}
3. JWT认证过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (Exception e) {
logger.error("Unable to get JWT Token");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
4. Spring Security配置
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
5. 认证控制器
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
}
七、总结与最佳实践建议
1. 技术选型建议
-
JWT适用场景:
- 无状态分布式系统
- 需要跨域认证的单页应用
- 微服务架构中的服务间认证
- 需要客户端存储认证信息的移动应用
-
传统Session适用场景:
- 需要即时撤销会话的场景
- 需要服务端严格控制会话的场景
- 对安全性要求极高的内部系统
2. 架构设计建议
-
分层安全设计:
- 网络层:HTTPS + 防火墙
- 应用层:JWT签名验证 + 黑名单
- 数据层:敏感数据加密
-
性能与安全平衡:
- 访问令牌有效期:15-30分钟
- 刷新令牌有效期:7天
- 签名算法:HS256(性能好)或RS256(可分离密钥)
-
监控与告警:
- 监控异常认证尝试
- 记录令牌使用情况
- 设置可疑活动告警
3. 未来演进方向
-
JWT扩展:
- 探索JWE(JSON Web Encryption)加密敏感数据
- 实现JWT的Proof of Possession (PoP)机制
-
新兴标准:
- OAuth 2.0 Token Exchange
- DPoP (Demonstrating Proof-of-Possession)
- WebAuthn 无密码认证
-
服务网格集成:
- 在Service Mesh层统一处理JWT验证
- 实现自动化的令牌传播和刷新
通过本文的全面介绍,您应该已经掌握了JWT令牌方案的核心知识,以及如何在Java Web应用中结合拦截器和过滤器实现安全、高效的认证授权机制。实际应用中,请根据具体业务需求和安全要求进行调整和优化。