Spring Security JWT 认证实战
1. JWT 概述
1.1 什么是 JWT
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全传输信息的紧凑、URL 安全的方式。
1.2 JWT 结构
JWT 由三部分组成,用点(.)分隔:
Header.Payload.Signature| 部分 | 说明 |
|---|---|
| Header | 令牌类型和签名算法 |
| Payload | 声明(claims)数据 |
| Signature | 签名,验证令牌完整性 |
1.3 JWT vs Session
| 特性 | JWT | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务端 |
| 扩展性 | 无状态,易扩展 | 需要共享 Session |
| 跨域 | 支持 | 需要额外处理 |
| 安全性 | 无法主动失效 | 可随时销毁 |
| 性能 | 无需查询存储 | 需要查询 Session |
2. 项目结构
src/main/java/com/example/
├── config/
│ ├── SecurityConfig.java
│ └── JwtConfig.java
├── controller/
│ └── AuthController.java
├── dto/
│ ├── LoginRequest.java
│ ├── RegisterRequest.java
│ └── JwtResponse.java
├── entity/
│ └── User.java
├── repository/
│ └── UserRepository.java
├── security/
│ ├── JwtTokenProvider.java
│ ├── JwtAuthenticationFilter.java
│ └── CustomUserDetailsService.java
└── service/
└── AuthService.java3. 添加依赖
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>4. JWT 工具类
4.1 配置类
java
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {
private String secret;
private long expiration;
private String header;
private String prefix;
// getters and setters
}yaml
jwt:
secret: your-256-bit-secret-key-here-must-be-at-least-256-bits
expiration: 86400000
header: Authorization
prefix: Bearer4.2 JWT Token Provider
java
@Component
public class JwtTokenProvider {
private final JwtConfig jwtConfig;
private final SecretKey secretKey;
public JwtTokenProvider(JwtConfig jwtConfig) {
this.jwtConfig = jwtConfig;
this.secretKey = Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes());
}
public String generateToken(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtConfig.getExpiration());
return Jwts.builder()
.subject(userDetails.getUsername())
.claim("userId", userDetails.getId())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
public String generateToken(User user) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtConfig.getExpiration());
return Jwts.builder()
.subject(user.getUsername())
.claim("userId", user.getId())
.claim("roles", user.getRoles().stream()
.map(role -> "ROLE_" + role.getName())
.collect(Collectors.toList()))
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
public String generateRefreshToken(String username) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtConfig.getExpiration() * 7);
return Jwts.builder()
.subject(username)
.claim("type", "refresh")
.issuedAt(now)
.expiration(expiryDate)
.signWith(secretKey)
.compact();
}
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
}
public Long getUserIdFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get("userId", Long.class);
}
@SuppressWarnings("unchecked")
public List<String> getRolesFromToken(String token) {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return claims.get("roles", List.class);
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public boolean isTokenExpired(String token) {
try {
Claims claims = Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getExpiration().before(new Date());
} catch (JwtException e) {
return true;
}
}
}5. 认证过滤器
5.1 JWT 认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService userDetailsService;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider,
CustomUserDetailsService userDetailsService) {
this.tokenProvider = tokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}6. 安全配置
6.1 Security 配置
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
private final CustomAccessDeniedHandler accessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}6.2 异常处理
java
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiResponse<Void> apiResponse = ApiResponse.error(
HttpServletResponse.SC_UNAUTHORIZED,
"未授权访问: " + authException.getMessage()
);
response.getWriter().write(new ObjectMapper().writeValueAsString(apiResponse));
}
}
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ApiResponse<Void> apiResponse = ApiResponse.error(
HttpServletResponse.SC_FORBIDDEN,
"权限不足: " + accessDeniedException.getMessage()
);
response.getWriter().write(new ObjectMapper().writeValueAsString(apiResponse));
}
}7. 认证服务
7.1 DTO 类
java
public record LoginRequest(
@NotBlank(message = "用户名不能为空")
String username,
@NotBlank(message = "密码不能为空")
String password
) {}
public record RegisterRequest(
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度3-20位")
String username,
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{8,}$",
message = "密码至少8位,包含字母和数字")
String password,
@Email(message = "邮箱格式不正确")
String email
) {}
public record JwtResponse(
String accessToken,
String refreshToken,
String tokenType,
long expiresIn,
UserInfo user
) {
public static JwtResponse of(String accessToken, String refreshToken,
long expiresIn, User user) {
return new JwtResponse(
accessToken,
refreshToken,
"Bearer",
expiresIn,
new UserInfo(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getRoles().stream()
.map(Role::getName)
.collect(Collectors.toList())
)
);
}
}
public record UserInfo(
Long id,
String username,
String email,
List<String> roles
) {}7.2 认证服务实现
java
@Service
@Transactional
public class AuthService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider tokenProvider;
private final AuthenticationManager authenticationManager;
public JwtResponse login(LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.username(),
request.password()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
String accessToken = tokenProvider.generateToken(authentication);
String refreshToken = tokenProvider.generateRefreshToken(userDetails.getUsername());
User user = userRepository.findByUsername(userDetails.getUsername())
.orElseThrow(() -> new ResourceNotFoundException("用户不存在"));
return JwtResponse.of(accessToken, refreshToken, 86400000L, user);
}
public JwtResponse register(RegisterRequest request) {
if (userRepository.existsByUsername(request.username())) {
throw new BusinessException("用户名已存在");
}
if (userRepository.existsByEmail(request.email())) {
throw new BusinessException("邮箱已被注册");
}
User user = new User();
user.setUsername(request.username());
user.setPassword(passwordEncoder.encode(request.password()));
user.setEmail(request.email());
user.setEnabled(true);
Role userRole = roleRepository.findByName("USER")
.orElseThrow(() -> new ResourceNotFoundException("角色不存在"));
user.setRoles(Set.of(userRole));
User savedUser = userRepository.save(user);
String accessToken = tokenProvider.generateToken(savedUser);
String refreshToken = tokenProvider.generateRefreshToken(savedUser.getUsername());
return JwtResponse.of(accessToken, refreshToken, 86400000L, savedUser);
}
public JwtResponse refreshToken(String refreshToken) {
if (!tokenProvider.validateToken(refreshToken)) {
throw new BusinessException("无效的刷新令牌");
}
String username = tokenProvider.getUsernameFromToken(refreshToken);
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在"));
String newAccessToken = tokenProvider.generateToken(user);
String newRefreshToken = tokenProvider.generateRefreshToken(username);
return JwtResponse.of(newAccessToken, newRefreshToken, 86400000L, user);
}
public void logout(String token) {
// 可以将 token 加入黑名单
// tokenBlacklistService.addToBlacklist(token);
}
}8. 认证控制器
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;
@PostMapping("/login")
public ResponseEntity<ApiResponse<JwtResponse>> login(
@Valid @RequestBody LoginRequest request) {
JwtResponse response = authService.login(request);
return ResponseEntity.ok(ApiResponse.success(response));
}
@PostMapping("/register")
public ResponseEntity<ApiResponse<JwtResponse>> register(
@Valid @RequestBody RegisterRequest request) {
JwtResponse response = authService.register(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(response));
}
@PostMapping("/refresh")
public ResponseEntity<ApiResponse<JwtResponse>> refreshToken(
@RequestBody Map<String, String> request) {
String refreshToken = request.get("refreshToken");
JwtResponse response = authService.refreshToken(refreshToken);
return ResponseEntity.ok(ApiResponse.success(response));
}
@PostMapping("/logout")
public ResponseEntity<ApiResponse<Void>> logout(
@RequestHeader("Authorization") String authorization) {
String token = authorization.substring(7);
authService.logout(token);
return ResponseEntity.ok(ApiResponse.success(null));
}
@GetMapping("/me")
public ResponseEntity<ApiResponse<UserInfo>> getCurrentUser(
@AuthenticationPrincipal CustomUserDetails userDetails) {
User user = userDetails.getUser();
UserInfo userInfo = new UserInfo(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getRoles().stream()
.map(Role::getName)
.collect(Collectors.toList())
);
return ResponseEntity.ok(ApiResponse.success(userInfo));
}
}9. Token 黑名单
9.1 Redis 实现
java
@Service
public class TokenBlacklistService {
private final RedisTemplate<String, String> redisTemplate;
private static final String BLACKLIST_PREFIX = "token:blacklist:";
public void addToBlacklist(String token, long expiration) {
String key = BLACKLIST_PREFIX + token;
redisTemplate.opsForValue().set(key, "1", expiration, TimeUnit.MILLISECONDS);
}
public boolean isBlacklisted(String token) {
String key = BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
}9.2 更新过滤器
java
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
if (tokenBlacklistService.isBlacklisted(token)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token 已失效");
return;
}
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}10. 统一响应
java
public record ApiResponse<T>(
int code,
String message,
T data,
long timestamp
) {
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data, System.currentTimeMillis());
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(200, message, data, System.currentTimeMillis());
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null, System.currentTimeMillis());
}
public static <T> ApiResponse<T> error(int code, String message, T data) {
return new ApiResponse<>(code, message, data, System.currentTimeMillis());
}
}11. 全局异常处理
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, "参数验证失败", errors));
}
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
return ResponseEntity.status(ex.getStatus())
.body(ApiResponse.error(ex.getStatus(), ex.getMessage()));
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(
ResourceNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(404, ex.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(500, "服务器内部错误"));
}
}
public class BusinessException extends RuntimeException {
private final int status;
public BusinessException(String message) {
super(message);
this.status = 400;
}
public BusinessException(int status, String message) {
super(message);
this.status = status;
}
public int getStatus() {
return status;
}
}
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}12. 测试接口
12.1 使用 curl 测试
bash
# 注册
curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"Test1234","email":"test@example.com"}'
# 登录
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"Test1234"}'
# 获取当前用户
curl http://localhost:8080/api/auth/me \
-H "Authorization: Bearer <token>"
# 刷新 Token
curl -X POST http://localhost:8080/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"<refresh_token>"}'13. 小结
本章学习了 Spring Security JWT 认证的完整实现:
| 内容 | 要点 |
|---|---|
| JWT 结构 | Header、Payload、Signature |
| Token 生成 | Jwts.builder()、签名算法 |
| Token 验证 | Jwts.parser()、密钥验证 |
| 认证过滤器 | OncePerRequestFilter、Token 解析 |
| 安全配置 | 无状态 Session、过滤器链 |
| 异常处理 | AuthenticationEntryPoint、AccessDeniedHandler |
| Token 黑名单 | Redis 存储、主动失效 |
下一章将学习 Spring Boot 4 的快速入门。