SpringSecurity JWT令牌实战
JWT(JSON Web Token)是前后端分离场景下最流行的认证方案。本文实现一套完整的 JWT 认证系统。
项目结构
src/main/java/com/example/ ├── config/ │ └── SecurityConfig.java ├── controller/ │ └── AuthController.java ├── dto/ │ ├── LoginRequest.java │ └── TokenResponse.java ├── entity/ │ └── User.java ├── filter/ │ └── JwtAuthenticationFilter.java ├── repository/ │ └── UserRepository.java ├── service/ │ ├── AuthService.java │ └── CustomUserDetailsService.java └── util/ └── JwtUtil.java
|
依赖
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.12.3</version> </dependency> </dependencies>
|
JWT 工具类
@Component public class JwtUtil { @Value("${jwt.secret:mySecretKey}") private String secret; @Value("${jwt.expiration:86400000}") private long expiration; @Value("${jwt.refresh-expiration:604800000}") private long refreshExpiration; private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String generateToken(UserDetails userDetails) { return generateToken(new HashMap<>(), userDetails, expiration); } public String generateRefreshToken(UserDetails userDetails) { return generateToken(new HashMap<>(), userDetails, refreshExpiration); } public String generateToken(Map<String, Object> claims, UserDetails userDetails, long exp) { return Jwts.builder() .claims(claims) .subject(userDetails.getUsername()) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() + exp)) .signWith(getSigningKey()) .compact(); } public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } public boolean isTokenValid(String token, UserDetails userDetails) { String username = extractUsername(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } private boolean isTokenExpired(String token) { return extractClaim(token, Claims::getExpiration).before(new Date()); } private Claims extractAllClaims(String token) { return Jwts.parser() .verifyWith(getSigningKey()) .build() .parseSignedClaims(token) .getPayload(); } }
|
安全配置
@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Autowired private JwtAuthenticationFilter jwtAuthFilter; @Autowired private CustomUserDetailsService userDetailsService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ) .authenticationProvider(authenticationProvider()) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder()); return provider; } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
|
JWT 认证过滤器
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private CustomUserDetailsService userDetailsService; @Autowired private TokenBlacklistService tokenBlacklistService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } String jwt = authHeader.substring(7); if (tokenBlacklistService.isBlacklisted(jwt)) { filterChain.doFilter(request, response); return; } String username = jwtUtil.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.isTokenValid(jwt, userDetails)) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
|
认证服务
@Service public class AuthService { @Autowired private AuthenticationManager authenticationManager; @Autowired private JwtUtil jwtUtil; @Autowired private CustomUserDetailsService userDetailsService; @Autowired private TokenBlacklistService tokenBlacklistService; public TokenResponse login(LoginRequest request) { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword()) ); UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername()); String accessToken = jwtUtil.generateToken(userDetails); String refreshToken = jwtUtil.generateRefreshToken(userDetails); return TokenResponse.builder() .accessToken(accessToken) .refreshToken(refreshToken) .tokenType("Bearer") .expiresIn(86400) .build(); } public TokenResponse refresh(String refreshToken) { String username = jwtUtil.extractUsername(refreshToken); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (!jwtUtil.isTokenValid(refreshToken, userDetails)) { throw new RuntimeException("Invalid refresh token"); } String newAccessToken = jwtUtil.generateToken(userDetails); String newRefreshToken = jwtUtil.generateRefreshToken(userDetails); return TokenResponse.builder() .accessToken(newAccessToken) .refreshToken(newRefreshToken) .tokenType("Bearer") .expiresIn(86400) .build(); } public void logout(String token) { tokenBlacklistService.addToBlacklist(token); } }
|
Token 黑名单(Redis)
@Service public class TokenBlacklistService { @Autowired private StringRedisTemplate redisTemplate; private static final String BLACKLIST_PREFIX = "blacklist:"; public void addToBlacklist(String token) { long expiration = getExpirationFromToken(token); redisTemplate.opsForValue().set( BLACKLIST_PREFIX + token, "1", expiration, TimeUnit.MILLISECONDS); } public boolean isBlacklisted(String token) { return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token)); } private long getExpirationFromToken(String token) { return 86400000; } }
|
控制器
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthService authService; @PostMapping("/login") public Result<TokenResponse> login(@RequestBody @Valid LoginRequest request) { return Result.success(authService.login(request)); } @PostMapping("/refresh") public Result<TokenResponse> refresh(@RequestHeader("X-Refresh-Token") String refreshToken) { return Result.success(authService.refresh(refreshToken)); } @PostMapping("/logout") public Result<Void> logout(@RequestHeader("Authorization") String authHeader) { String token = authHeader.substring(7); authService.logout(token); return Result.success(); } }
|
前端使用
const login = async (username, password) => { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); localStorage.setItem('token', data.data.accessToken); localStorage.setItem('refreshToken', data.data.refreshToken); };
axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; });
axios.interceptors.response.use( response => response, async error => { if (error.response.status === 401) { const refreshToken = localStorage.getItem('refreshToken'); const response = await axios.post('/api/auth/refresh', null, { headers: { 'X-Refresh-Token': refreshToken } }); localStorage.setItem('token', response.data.data.accessToken); return axios(error.config); } return Promise.reject(error); } );
|
总结
JWT 认证方案的关键点:
- Access Token:短期有效,用于接口认证
- Refresh Token:长期有效,用于刷新 Access Token
- 黑名单:登出时加入 Redis 黑名单
- 无状态:服务端不存储会话,支持水平扩展
这套方案适用于大多数前后端分离项目。