1. 为什么选择Token认证
在现代Web应用中,认证机制是保障系统安全的第一道防线。传统的Session认证方式虽然简单易用,但在分布式系统、微服务架构中暴露出诸多问题:服务器需要维护Session状态,跨域访问困难,CSRF攻击风险高等。而基于Token的认证机制恰好能解决这些痛点。
Token认证的核心思想是:客户端首次登录后,服务器生成一个包含用户身份信息的Token返回给客户端,客户端在后续请求中携带这个Token,服务器验证Token的有效性即可完成认证。整个过程服务器不需要保存任何状态,天然支持分布式部署。
Spring Security作为Java生态中最成熟的安全框架,提供了完善的Token认证支持。通过合理配置,我们可以实现:
- 无状态的认证机制
- 跨域资源共享(CORS)支持
- 防止CSRF攻击
- 灵活的权限控制
- 易于扩展的认证流程
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JWT Token的实现原理
2.1 JWT的结构解析
JSON Web Token(JWT)是目前最流行的Token实现方案。一个标准的JWT由三部分组成,用点号(.)连接:
code复制Header.Payload.Signature
Header部分通常包含两个信息:
json复制{
"alg": "HS256",
"typ": "JWT"
}
- alg表示签名算法,如HS256表示使用HMAC SHA-256
- typ固定为JWT
Payload部分包含声明(claims),声明分为三类:
- 注册声明(Registered claims):预定义但不强制使用的声明,如iss(签发者)、exp(过期时间)、sub(主题)等
- 公共声明(Public claims):可以自定义的声明
- 私有声明(Private claims):用于在同意使用它们的各方之间共享信息
示例Payload:
json复制{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
Signature部分是对前两部分的签名,防止数据篡改。生成方式是将编码后的header和payload用指定算法加密:
code复制HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
2.2 JWT的工作流程
- 用户使用凭证(如用户名密码)登录
- 服务器验证凭证,生成JWT返回给客户端
- 客户端存储JWT(通常放在localStorage或cookie中)
- 客户端在后续请求的Authorization头中携带JWT
- 服务器验证JWT的有效性和签名
- 服务器返回受保护资源
注意:JWT一旦签发,在有效期内将一直有效,无法中途废止。这是JWT的一个重要特性,也是需要考虑的安全问题。
3. Spring Security集成JWT
3.1 基础环境配置
首先添加必要的依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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工具类处理Token的生成和验证:
java复制public class JwtTokenProvider {
private final String secretKey;
private final long validityInMilliseconds;
public JwtTokenProvider(String secretKey, long validityInMilliseconds) {
this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
this.validityInMilliseconds = validityInMilliseconds;
}
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date validity = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
public Authentication getAuthentication(String token) {
UserDetails userDetails = // 根据token中的用户名加载用户详情
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (JwtException | IllegalArgumentException e) {
throw new InvalidJwtAuthenticationException("Expired or invalid JWT token");
}
}
}
3.2 配置Spring Security
创建Security配置类:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtConfigurer(jwtTokenProvider));
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
创建JWT过滤器配置:
java复制public class JwtConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final JwtTokenProvider jwtTokenProvider;
public JwtConfigurer(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
public void configure(HttpSecurity http) {
JwtTokenFilter customFilter = new JwtTokenFilter(jwtTokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
3.3 实现JWT过滤器
创建JWT过滤器,用于拦截请求并验证Token:
java复制public class JwtTokenFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = resolveToken(request);
try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (InvalidJwtAuthenticationException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token");
return;
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest req) {
String bearerToken = req.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
4. 认证接口实现
4.1 用户认证接口
创建认证控制器处理登录请求:
java复制@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
public AuthController(AuthenticationManager authenticationManager,
JwtTokenProvider jwtTokenProvider,
UserDetailsService userDetailsService) {
this.authenticationManager = authenticationManager;
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody AuthRequest authRequest) {
try {
String username = authRequest.getUsername();
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, authRequest.getPassword()));
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
String token = jwtTokenProvider.createToken(username, roles);
return ResponseEntity.ok(new AuthResponse(token));
} catch (AuthenticationException e) {
throw new BadCredentialsException("Invalid username/password");
}
}
}
4.2 Token刷新机制
JWT的一个缺点是过期后需要重新登录。我们可以实现Token刷新机制改善用户体验:
- 在生成JWT时同时生成一个刷新Token(通常有更长有效期)
- 客户端在访问Token过期时,使用刷新Token获取新Token
- 服务器验证刷新Token的有效性,签发新Token
实现刷新Token接口:
java复制@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest refreshTokenRequest) {
String refreshToken = refreshTokenRequest.getRefreshToken();
try {
if (jwtTokenProvider.validateToken(refreshToken)) {
String username = jwtTokenProvider.getUsernameFromToken(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
String newToken = jwtTokenProvider.createToken(username, roles);
return ResponseEntity.ok(new AuthResponse(newToken));
}
} catch (JwtException | IllegalArgumentException e) {
throw new InvalidJwtAuthenticationException("Refresh token is invalid");
}
throw new InvalidJwtAuthenticationException("Refresh token is missing or invalid");
}
5. 安全最佳实践与常见问题
5.1 JWT安全配置建议
- 使用强密钥:密钥长度至少256位,避免使用简单字符串
- 合理设置过期时间:访问Token建议15-30分钟,刷新Token可设置较长时间(如7天)
- HTTPS传输:防止Token在传输过程中被截获
- 存储安全:
- 浏览器端:使用HttpOnly的Cookie比localStorage更安全
- 移动端:使用安全存储机制
- 黑名单机制:虽然JWT本身无状态,但可以实现Token黑名单处理提前注销
5.2 常见问题排查
问题1:Token验证失败(403 Forbidden)
可能原因:
- Token已过期(检查exp声明)
- 签名验证失败(检查密钥是否一致)
- Token格式不正确(检查是否包含Bearer前缀)
问题2:Token被篡改
解决方案:
- 确保使用足够强度的签名算法(推荐HS256或RS256)
- 密钥严格保密,不要硬编码在客户端
问题3:跨域问题
解决方案:
- 正确配置CORS
- 确保Authorization头被允许
java复制@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.addExposedHeader("Authorization");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
5.3 性能优化建议
- 减少Payload大小:只包含必要信息,避免存储过多用户数据
- 缓存用户权限:避免每次请求都查询数据库获取权限信息
- 异步验证:对于高并发系统,可以考虑异步验证Token
- 分布式验证:在微服务架构中,可以使用公钥/私钥机制(RSA)避免共享密钥
在实际项目中,我曾遇到一个性能问题:每次请求都从数据库加载用户权限导致响应变慢。解决方案是在Token中缓存用户权限,并设置较短的过期时间,同时实现权限变更时的Token失效机制。这样既保证了安全性,又提高了性能。
