Appearance
JWT实现
JSON Web Token (JWT) 是一种开放标准,它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象安全地传输信息。在Spring Security中,JWT可以用于身份验证和授权,特别适合用于RESTful API和微服务架构。
JWT 基本概念
JWT 的结构
JWT由三部分组成,用点(.)分隔:
Header(头部)
- 通常包含两部分信息:令牌类型(即JWT)和所使用的签名算法
- 例如:
{ "alg": "HS256", "typ": "JWT" }
Payload(负载)
- 包含声明(claims)
- 声明是关于实体(通常是用户)和其他数据的声明
- 三种类型的声明:注册声明、公共声明和私有声明
Signature(签名)
- 用于验证消息在传输过程中没有被更改
- 使用头部指定的算法,对编码后的头部、编码后的负载和一个秘钥进行签名
JWT 工作原理
- 用户登录后,服务器创建JWT令牌
- 服务器将JWT返回给客户端
- 客户端存储JWT(通常在localStorage或Cookie中)
- 客户端在后续请求中,将JWT放在Authorization头中
- 服务器验证JWT的有效性,并从中提取用户信息
Spring Security 集成 JWT
依赖配置
在Spring Boot项目中添加JWT相关依赖:
xml
<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>
JWT 工具类
创建一个工具类来处理JWT的创建、解析和验证:
java
@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<>();
return createToken(claims, userDetails.getUsername());
}
// 创建令牌
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
// 验证令牌
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 从令牌中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从令牌中获取过期日期
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 从令牌中获取声明
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 解析令牌
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}
// 检查令牌是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}
自定义JWT认证过滤器
创建一个过滤器来拦截请求并验证JWT:
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@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;
// JWT Token格式为 "Bearer token",去除Bearer字样,只获取token部分
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.error("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// 一旦获取到令牌,就验证它
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// 如果令牌有效,手动设置Spring Security认证
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);
}
}
配置认证入口点
自定义认证入口点来处理未认证的请求:
java
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("message", "Unauthorized access");
errorDetails.put("error", authException.getMessage());
PrintWriter writer = response.getWriter();
writer.println(mapper.writeValueAsString(errorDetails));
}
}
配置Spring Security
配置Spring Security以使用JWT:
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private JwtAuthenticationFilter jwtRequestFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 添加JWT过滤器
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
认证控制器
创建控制器处理用户认证和令牌生成:
java
@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认证请求和响应的模型类:
java
public class JwtRequest {
private String username;
private String password;
// 默认构造函数与getter和setter
public JwtRequest() {}
public JwtRequest(String username, String password) {
this.username = username;
this.password = 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 class JwtResponse {
private final String token;
public JwtResponse(String token) {
this.token = token;
}
public String getToken() {
return token;
}
}
配置属性
在application.properties
或application.yml
中添加JWT相关配置:
properties
# JWT配置
jwt.secret=your-secret-key-should-be-very-long-and-secure
jwt.expiration=86400
JWT 最佳实践与安全考虑
安全最佳实践
使用HTTPS:始终通过HTTPS传输JWT,以防止中间人攻击。
适当的令牌过期时间:设置较短的令牌有效期,减少被盗用的风险。
实施刷新令牌:使用刷新令牌机制,避免用户频繁登录。
安全存储密钥:JWT签名密钥应安全存储,避免硬编码在源代码中。
不在JWT中存储敏感信息:JWT的payload部分是Base64编码,不是加密,不应存储敏感数据。
刷新令牌机制
实现刷新令牌机制,延长用户会话:
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
// ...现有代码
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody JwtRefreshRequest refreshRequest) {
// 验证刷新令牌
String refreshToken = refreshRequest.getRefreshToken();
String username = jwtTokenUtil.getUsernameFromToken(refreshToken);
if (username != null && jwtTokenUtil.validateRefreshToken(refreshToken)) {
final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
final String accessToken = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(accessToken));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
}
}
令牌黑名单
实现令牌黑名单,处理失效的令牌:
java
@Service
public class TokenBlacklistService {
private Set<String> blacklist = new ConcurrentHashSet<>();
public void blacklistToken(String token) {
blacklist.add(token);
}
public boolean isBlacklisted(String token) {
return blacklist.contains(token);
}
// 定时任务,清理过期的令牌
@Scheduled(fixedRate = 3600000) // 每小时执行一次
public void purgeExpiredTokens() {
// 实现过期令牌清理逻辑
}
}
在JWT过滤器中检查黑名单:
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenBlacklistService blacklistService;
// ...现有代码
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// ...现有代码
if (jwtToken != null && blacklistService.isBlacklisted(jwtToken)) {
logger.error("JWT Token is blacklisted");
// 如果令牌在黑名单中,不进行验证
} else if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 继续处理有效令牌
}
// ...现有代码
}
}
JWT 与微服务架构
在微服务中使用JWT
集中式认证服务:专门的认证服务负责用户认证和JWT签发。
令牌传播:微服务之间传递JWT,验证用户身份与权限。
共享密钥管理:所有微服务共享JWT验证密钥或使用非对称密钥(RSA)。
将JWT与API网关集成
在API网关层验证JWT,减轻后端微服务的认证负担:
java
// API网关中的JWT过滤器示例
@Component
public class GatewayJwtFilter extends AbstractGatewayFilterFactory<GatewayJwtFilter.Config> {
private final JwtTokenUtil jwtTokenUtil;
public GatewayJwtFilter(JwtTokenUtil jwtTokenUtil) {
super(Config.class);
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 从请求中获取JWT令牌
String token = extractToken(request);
if (token != null && jwtTokenUtil.validateToken(token)) {
// 令牌有效,将用户信息添加到请求头
String username = jwtTokenUtil.getUsernameFromToken(token);
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", username)
.build();
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
// 令牌无效,返回未授权响应
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
};
}
private String extractToken(ServerHttpRequest request) {
List<String> authHeaders = request.getHeaders().get("Authorization");
if (authHeaders != null && !authHeaders.isEmpty()) {
String authHeader = authHeaders.get(0);
if (authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
}
return null;
}
public static class Config {
// 配置属性
}
}
JWT 常见问题与解决方案
问题1:令牌过期处理
问题:用户令牌过期但会话仍然活跃时,如何平滑处理?
解决方案:
- 实现无感刷新机制,通过刷新令牌自动获取新令牌
- 在令牌即将过期前主动刷新
- 使用滑动过期时间
问题2:令牌撤销
问题:JWT是无状态的,如何在必要时撤销已签发的令牌?
解决方案:
- 使用令牌黑名单
- 实现短期令牌策略
- 使用Redis存储令牌有效状态
问题3:令牌大小
问题:JWT包含所有用户信息,可能变得很大。
解决方案:
- 仅在令牌中存储必要信息(如用户ID)
- 使用压缩技术
- 考虑使用引用令牌(Reference Token)而非自包含令牌
总结
JWT提供了一种轻量级、无状态的认证机制,非常适合现代Web应用和微服务架构。
在Spring Security中集成JWT相对简单,主要包括:
- 创建JWT工具类处理令牌生成和验证
- 实现JWT过滤器拦截和处理请求
- 配置Spring Security使用JWT认证
使用JWT时需注意安全实践:
- 保护签名密钥
- 设置合理的过期时间
- 不在JWT中存储敏感信息
- 实现令牌刷新和撤销机制
微服务架构中,JWT通常与API网关结合使用,集中处理认证逻辑。
通过合理使用JWT,可以实现安全、可扩展的认证系统,简化分布式应用的用户认证与授权管理。