目录
一、JWT令牌方案详解
1. JWT基本概念
2. JWT工作流程
3. JWT的优势与劣势
4. JWT实现细节
5. JWT安全最佳实践
二、拦截器(Interceptor)详解
1. 拦截器基本概念
2. Spring拦截器实现
3. 拦截器与过滤器的区别
4. 拦截器应用场景
5. 拦截器高级用法
三、过滤器(Filter)详解
1. 过滤器基本概念
2. 过滤器实现
3. 过滤器应用场景
4. 过滤器链机制
5. 过滤器高级用法
四、JWT与拦截器/过滤器的最佳实践组合
1. 完整认证流程设计
2. 安全增强方案
3. 性能优化建议
4. 微服务架构中的JWT实践
五、常见问题与解决方案
1. JWT安全问题
2. 性能问题
3. 分布式系统问题
4. 移动端问题
六、实战案例:Spring Security + JWT完整实现
1. 依赖配置
2. JWT工具类
3. JWT认证过滤器
4. Spring Security配置
5. 认证控制器
七、总结与最佳实践建议
1. 技术选型建议
2. 架构设计建议
3. 未来演进方向
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),用于在网络应用环境间安全地传递声明。JWT被设计为紧凑且自包含的方式,特别适用于分布式站点的单点登录(SSO)场景。
JWT的组成结构
JWT由三部分组成,用点(.)分隔:
格式为:xxxxx.yyyyy.zzzzz
1.1 Header (头部) 通常由两部分组成:
示例:
{
"alg": "HS256",
"typ": "JWT"
}
1.2 Payload (负载) 包含声明(claims),声明是关于实体(通常是用户)和附加数据的语句。有三种类型的声明:
示例:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
1.3 Signature (签名) 签名用于验证消息在传递过程中没有被篡改。创建签名需要:
例如使用HMAC SHA256算法:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
优势:
劣势:
4.1 令牌生成
// Java示例使用jjwt库
public String generateToken(UserDetails userDetails) {
Map 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 刷新令牌机制
通常实现两种令牌:
刷新流程:
拦截器是一种AOP(面向切面编程)实现,用于在请求处理前后执行特定逻辑。在Web应用中,拦截器通常用于:
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"); // 排除路径
}
}
特性 | 拦截器(Interceptor) | 过滤器(Filter) |
---|---|---|
所属规范 | Spring MVC特有 | Java Servlet规范 |
执行位置 | Controller方法前后 | Servlet处理前和后 |
依赖 | 依赖Spring容器 | 不依赖任何框架 |
获取上下文 | 可以获取Spring上下文 | 无法获取Spring上下文 |
异常处理 | 可以访问Controller抛出的异常 | 无法访问Controller抛出的异常 |
实现方式 | 实现HandlerInterceptor接口 | 实现javax.servlet.Filter接口 |
配置方式 | 通过WebMvcConfigurer配置 | 通过web.xml或@WebFilter注解配置 |
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) {
// 检查权限
}
}
过滤器是Java Servlet规范的一部分,用于在请求到达Servlet之前或响应发送到客户端之前对请求和响应进行预处理和后处理。
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() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/api/*");
registrationBean.setOrder(1); // 设置过滤器顺序
return registrationBean;
}
}
多个过滤器可以组成过滤器链,按照web.xml中定义的顺序或@Order注解的顺序执行:
请求 -> Filter1 -> Filter2 -> ... -> FilterN -> Servlet -> FilterN -> ... -> Filter2 -> Filter1 -> 响应
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);
}
登录流程:
访问受保护资源:
令牌刷新流程:
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 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;
}
在微服务架构中,JWT特别适合:
// 网关过滤器示例
public class GatewayJwtFilter implements GatewayFilter {
@Override
public Mono 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:令牌泄露
问题2:XSS攻击窃取令牌
问题3:CSRF攻击
问题1:频繁的JWT验证开销
问题2:大负载影响性能
问题1:注销困难
问题2:时钟漂移导致验证失败
问题1:令牌安全存储
问题2:网络不稳定导致令牌刷新失败
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt-api
0.11.2
io.jsonwebtoken
jjwt-impl
0.11.2
runtime
io.jsonwebtoken
jjwt-jackson
0.11.2
runtime
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(UserDetails userDetails) {
Map 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 getRolesFromToken(String token) {
List 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());
}
}
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);
}
}
@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);
}
}
@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);
}
}
}
JWT适用场景:
传统Session适用场景:
分层安全设计:
性能与安全平衡:
监控与告警:
JWT扩展:
新兴标准:
服务网格集成:
通过本文的全面介绍,您应该已经掌握了JWT令牌方案的核心知识,以及如何在Java Web应用中结合拦截器和过滤器实现安全、高效的认证授权机制。实际应用中,请根据具体业务需求和安全要求进行调整和优化。