JWT(JSON Web Token)本质上是一个开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。这种信息可以被验证和信任,因为它是经过数字签名的。在实际开发中,JWT最常见的应用场景就是身份验证和信息交换。
一个完整的JWT由三部分组成,用点号(.)分隔:
code复制header.payload.signature
让我们通过一个实际案例来理解这三部分的生成过程。假设我们需要为一个用户ID为123的用户生成token:
Header部分通常包含两部分信息:
json复制{
"alg": "HS256",
"typ": "JWT"
}
这个JSON表明使用HS256算法进行签名,类型是JWT。经过Base64Url编码后,这部分变成:
code复制eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload部分包含所谓的claims(声明),即关于实体(通常是用户)和其他数据的声明。例如:
json复制{
"sub": "123",
"name": "John Doe",
"iat": 1516239022
}
经过Base64Url编码后变为:
code复制eyJzdWIiOiIxMjMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9
Signature部分是通过将编码后的header和payload用点号连接,然后使用header中指定的算法(这里是HS256)和密钥进行签名。例如:
code复制HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your-256-bit-secret
)
最终生成的完整JWT看起来像这样:
code复制eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
重要提示:虽然JWT的header和payload是Base64Url编码的,但这并不意味着它们是加密的。任何人都可以解码这些部分查看原始内容。因此,绝对不要在JWT的payload中存放敏感信息如密码等。
在传统的Web应用中,服务器使用Session来保存用户的认证状态。这种机制的工作流程是:
这种模式在分布式系统中会遇到几个典型问题:
相比之下,JWT的工作机制完全不同:
这种无状态的设计带来了几个显著优势:
然而,JWT也不是银弹,它有自己的局限性:
JWT特别适合以下场景:
在实际使用JWT时,有几个关键的安全注意事项:
实践经验:在金融级应用中,建议结合JWT和Session的优点,使用短期有效的JWT配合完善的注销机制,既保持无状态的优势,又能快速撤销权限。
在Spring Boot项目中集成JWT,首先需要在pom.xml中添加必要的依赖。除了基础的Spring Boot Starter Web外,我们主要需要jjwt库:
xml复制<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- JJWT 依赖 -->
<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>
<!-- 其他可能需要的依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
这里使用的是JJWT的最新稳定版(截至2023年),注意我们明确区分了API、实现和Jackson模块,这是JJWT 0.11.x版本的推荐方式。
接下来,在application.yml中配置JWT相关参数:
yaml复制app:
jwt:
secret: "your-256-bit-secret" # 推荐使用至少256位的随机字符串
access-token-expiration: 900 # 访问令牌过期时间(秒),这里设置为15分钟
refresh-token-expiration: 86400 # 刷新令牌过期时间(秒),这里设置为24小时
token-header: "Authorization" # 请求头中携带token的字段名
token-prefix: "Bearer " # token前缀,通常使用Bearer
安全提示:生产环境中,secret应该通过环境变量或配置中心注入,而不是直接写在配置文件中。可以使用如下的方式:
yaml复制app: jwt: secret: ${JWT_SECRET:default-secret}然后通过环境变量JWT_SECRET传入实际密钥。
创建一个完整的JwtTokenUtil工具类,封装所有JWT相关操作:
java复制import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Slf4j
@Component
public class JwtTokenUtil {
@Value("${app.jwt.secret}")
private String secret;
@Value("${app.jwt.access-token-expiration}")
private long accessTokenExpiration;
@Value("${app.jwt.refresh-token-expiration}")
private long refreshTokenExpiration;
@Value("${app.jwt.token-prefix}")
private String tokenPrefix;
// 生成安全的签名密钥
private SecretKey getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
// 生成访问令牌
public String generateAccessToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 可以添加自定义claims
claims.put("roles", userDetails.getAuthorities());
return buildToken(claims, userDetails.getUsername(), accessTokenExpiration);
}
// 生成刷新令牌
public String generateRefreshToken(UserDetails userDetails) {
return buildToken(new HashMap<>(), userDetails.getUsername(), refreshTokenExpiration);
}
private String buildToken(Map<String, Object> claims, String subject, long expiration) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 从token中提取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 从token中提取过期时间
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// 通用的claim提取方法
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// 解析token中的所有claims
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 验证token是否有效
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 检查token是否过期
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// 从请求头中解析token
public String resolveToken(String bearerToken) {
if (bearerToken != null && bearerToken.startsWith(tokenPrefix)) {
return bearerToken.substring(tokenPrefix.length());
}
return null;
}
}
这个工具类提供了完整的JWT操作功能,包括:
为了实现JWT认证,我们需要创建一个拦截器来验证请求中的token:
java复制import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
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 {
// 1. 从请求头中获取token
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// 2. 解析token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.extractUsername(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.warn("JWT Token has expired");
} catch (MalformedJwtException e) {
logger.error("Invalid JWT Token");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// 3. 验证token并设置认证信息
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);
}
}
为了让拦截器生效,需要在Security配置中注册它:
java复制import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).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()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
}
这个安全配置做了以下几件事:
最后,我们实现认证相关的API端点:
java复制import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
// 1. 认证用户名密码
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 2. 生成token
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String accessToken = jwtTokenUtil.generateAccessToken(userDetails);
String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails);
// 3. 返回响应
return ResponseEntity.ok(new JwtResponse(
accessToken,
refreshToken,
jwtTokenUtil.getAccessTokenExpiration(),
userDetails.getUsername(),
userDetails.getAuthorities()
));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody TokenRefreshRequest request) {
String requestRefreshToken = request.getRefreshToken();
// 验证刷新令牌
if (!jwtTokenUtil.validateToken(requestRefreshToken)) {
return ResponseEntity.badRequest().body("Invalid refresh token");
}
// 生成新的访问令牌
String username = jwtTokenUtil.extractUsername(requestRefreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String newAccessToken = jwtTokenUtil.generateAccessToken(userDetails);
return ResponseEntity.ok(new TokenRefreshResponse(
newAccessToken,
requestRefreshToken,
jwtTokenUtil.getAccessTokenExpiration()
));
}
}
// 请求和响应DTO
class LoginRequest {
private String username;
private String password;
// getters and setters
}
class JwtResponse {
private String accessToken;
private String refreshToken;
private Long expiresIn;
private String username;
private Collection<?> roles;
// constructor and getters
}
class TokenRefreshRequest {
private String refreshToken;
// getter and setter
}
class TokenRefreshResponse {
private String accessToken;
private String refreshToken;
private Long expiresIn;
// constructor and getters
}
这个控制器提供了两个主要端点:
/api/auth/login - 用户登录,验证凭证后返回JWT令牌/api/auth/refresh - 使用刷新令牌获取新的访问令牌短期有效的访问令牌配合长期有效的刷新令牌是一种常见的安全实践。让我们深入实现这一机制:
java复制public class JwtTokenUtil {
// ... 原有代码 ...
// 专门验证刷新令牌的方法
public Boolean validateRefreshToken(String token) {
try {
final String username = extractUsername(token);
return !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
// 获取访问令牌过期时间
public Long getAccessTokenExpiration() {
return accessTokenExpiration;
}
}
java复制@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestBody TokenRefreshRequest request,
HttpServletResponse response) {
String requestRefreshToken = request.getRefreshToken();
if (!jwtTokenUtil.validateRefreshToken(requestRefreshToken)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("Invalid refresh token"));
}
String username = jwtTokenUtil.extractUsername(requestRefreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 生成新的访问令牌
String newAccessToken = jwtTokenUtil.generateAccessToken(userDetails);
// 可以选择是否生成新的刷新令牌(滚动刷新)
String newRefreshToken = jwtTokenUtil.generateRefreshToken(userDetails);
// 设置响应头
response.setHeader("Access-Control-Expose-Headers", "Authorization");
response.setHeader("Authorization", "Bearer " + newAccessToken);
return ResponseEntity.ok(new TokenRefreshResponse(
newAccessToken,
newRefreshToken,
jwtTokenUtil.getAccessTokenExpiration()
));
}
前端应用应该按照以下逻辑处理令牌刷新:
javascript复制async function callApiWithTokenRefresh() {
let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');
try {
// 第一次尝试用当前accessToken调用API
return await callProtectedApi(accessToken);
} catch (error) {
if (error.response.status === 401) {
// Token过期,尝试刷新
try {
const refreshResponse = await axios.post('/api/auth/refresh', {
refreshToken: refreshToken
});
// 保存新的tokens
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = refreshResponse.data;
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
// 用新的accessToken重试原始请求
return await callProtectedApi(newAccessToken);
} catch (refreshError) {
// 刷新失败,跳转到登录页
redirectToLogin();
throw refreshError;
}
} else {
throw error;
}
}
}
虽然JWT设计上是无状态的,但有时我们需要实现即时失效功能。可以通过令牌黑名单来实现:
java复制@Service
public class TokenBlacklistService {
private final Set<String> blacklistedTokens = Collections.newSetFromMap(new ConcurrentHashMap<>());
public void blacklistToken(String token) {
blacklistedTokens.add(token);
}
public boolean isTokenBlacklisted(String token) {
return blacklistedTokens.contains(token);
}
}
java复制@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private TokenBlacklistService tokenBlacklistService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// ... 原有代码 ...
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
// 检查token是否在黑名单中
if (tokenBlacklistService.isTokenBlacklisted(jwtToken)) {
chain.doFilter(request, response);
return;
}
// ... 原有验证逻辑 ...
}
chain.doFilter(request, response);
}
}
java复制@PostMapping("/logout")
public ResponseEntity<?> logoutUser(@RequestHeader("Authorization") String authHeader) {
String token = jwtTokenUtil.resolveToken(authHeader);
if (token != null) {
tokenBlacklistService.blacklistToken(token);
}
return ResponseEntity.ok("Logout successful");
}
生产环境建议:对于分布式系统,应该使用Redis等分布式缓存来实现黑名单,而不是内存中的Set。
减少JWT体积:
优化验证性能:
安全加固措施:
防御重放攻击:
java复制// 在生成token时添加jti
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("jti", UUID.randomUUID().toString());
// ... 其他claims
return buildToken(claims, userDetails.getUsername());
}
// 在验证时检查jti是否已被使用
public Boolean validateToken(String token) {
// ... 其他验证
String jti = extractClaim(token, Claims::getId);
if (tokenUsageService.isTokenUsed(jti)) {
return false;
}
tokenUsageService.markTokenAsUsed(jti);
return true;
}
完善的监控可以帮助发现潜在的安全问题和性能瓶颈:
关键指标监控:
详细日志记录:
java复制@Slf4j
@Component
public class JwtTokenUtil {
public Boolean validateToken(String token) {
try {
// ... 验证逻辑
} catch (ExpiredJwtException ex) {
log.warn("Expired JWT token: {}", ex.getMessage());
throw ex;
} catch (MalformedJwtException ex) {
log.warn("Invalid JWT token: {}", ex.getMessage());
throw ex;
} catch (Exception ex) {
log.error("Unexpected error during JWT validation", ex);
throw ex;
}
}
}
问题1:令牌被盗用
问题2:无法强制令牌失效
问题3:XSS攻击导致令牌泄露
问题4:重放攻击
问题1:大型JWT导致请求变慢
问题2:频繁的签名验证消耗CPU
问题3:分布式系统中的验证延迟
与Spring Security集成问题
SessionCreationPolicy.STATELESS)与OAuth 2.0集成问题
@EnableResourceServer和JwtTokenStorejava复制@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.resource.jwt.key-value}")
private String publicKey;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore());
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(publicKey);
return converter;
}
}
与微服务架构集成问题
java复制@Configuration
public class FeignConfig {
@Bean
public RequestInterceptor requestTokenBearerInterceptor() {
return requestTemplate -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getCredentials() instanceof String) {
String token = (String) authentication.getCredentials();
requestTemplate.header("Authorization", "Bearer " + token);
}
};
}
}
在将JWT认证部署到生产环境前,请检查以下事项:
密钥管理:
令牌设置:
安全传输:
防护措施:
性能考虑:
文档与流程: