Java JWT集成指南:在Spring Boot中使用jjwt实现认证授权
引言:JWT认证的痛点与解决方案
你是否还在为Spring Boot应用的认证授权方案而困扰?传统的Session认证在分布式系统中面临诸多挑战,如服务器内存占用高、水平扩展困难等问题。而JSON Web Token(JWT,JSON网络令牌)作为一种轻量级的认证机制,正逐渐成为解决这些问题的首选方案。
本文将详细介绍如何使用Java JWT库(jjwt)在Spring Boot应用中实现高效、安全的认证授权功能。通过阅读本文,你将能够:
- 理解JWT的基本概念和工作原理
- 掌握在Spring Boot中集成jjwt库的方法
- 实现基于JWT的用户登录和令牌验证
- 构建自定义的JWT认证过滤器
- 处理令牌过期、刷新等常见问题
- 了解JWT安全最佳实践
JWT简介
JWT的定义与结构
JSON Web Token(JWT)是一种开放标准(RFC 7519),定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。这些信息可以被验证和信任,因为它们是经过数字签名的。
JWT通常由三部分组成,用点(.)分隔:
- Header(头部)
- Payload(负载)
- Signature(签名)
因此,一个典型的JWT看起来是这样的:xxxxx.yyyyy.zzzzz
JWT的工作流程
JWT vs 传统Session认证
| 特性 | JWT认证 | Session认证 |
|---|---|---|
| 存储位置 | 客户端 | 服务器端 |
| 服务器负担 | 轻量 | 较重,需要存储Session |
| 扩展性 | 良好,无状态 | 较差,需要Session共享 |
| 跨域支持 | 良好 | 较差 |
| 安全性 | 依赖签名验证 | 依赖Cookie安全性 |
| 令牌大小 | 通常较小 | Session ID较小 |
| 过期处理 | 自包含过期信息 | 服务器端控制 |
准备工作:环境搭建与依赖配置
系统要求
- Java 8或更高版本
- Spring Boot 2.0或更高版本
- Maven或Gradle构建工具
添加依赖
在Spring Boot项目的pom.xml文件中添加以下依赖:
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT API -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<!-- JJWT Impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- JJWT Jackson Support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
配置JWT参数
在application.properties或application.yml中添加JWT相关配置:
# JWT配置
jwt:
# 签名密钥,实际使用时应使用更复杂的密钥并妥善保管
secret: your-secret-key-should-be-very-long-and-secure-for-production-use
# 令牌过期时间(毫秒),此处设置为24小时
expiration: 86400000
# 刷新令牌过期时间(毫秒),此处设置为7天
refresh-expiration: 604800000
# 令牌前缀
token-prefix: Bearer
# 请求头名称
header-string: Authorization
JJWT核心API详解
jjwt库提供了丰富的API来处理JWT令牌的创建、解析和验证。下面介绍一些核心类和接口:
Jwts类
Jwts是jjwt库的入口类,提供了创建JWT构建器、解析器等的静态方法。
// 创建JWT构建器
JwtBuilder builder = Jwts.builder();
// 创建JWT解析器构建器
JwtParserBuilder parserBuilder = Jwts.parser();
JwtBuilder接口
JwtBuilder用于构建JWT令牌,支持设置各种声明和签名算法。
String jwt = Jwts.builder()
.header() // 设置头部信息
.add("alg", "HS256") // 设置算法
.and()
.claims() // 设置负载声明
.issuer("myapp") // 签发者
.subject("user123") // 主题
.issuedAt(new Date()) // 签发时间
.expiration(new Date(System.currentTimeMillis() + 86400000)) // 过期时间
.and()
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS256) // 设置签名
.compact(); // 生成紧凑的JWT字符串
JwtParser接口
JwtParser用于解析和验证JWT令牌。
Jws<Claims> jws = Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes())) // 设置验证密钥
.build()
.parseSignedClaims(jwt); // 解析JWT并获取声明
Claims claims = jws.getBody(); // 获取负载部分
String subject = claims.getSubject(); // 获取主题
Date expiration = claims.getExpiration(); // 获取过期时间
签名算法
jjwt支持多种签名算法,通过Jwts.SIG类可以方便地访问这些算法:
// HMAC算法
Jwts.SIG.HS256 // HMAC-SHA256
Jwts.SIG.HS384 // HMAC-SHA384
Jwts.SIG.HS512 // HMAC-SHA512
// RSA算法
Jwts.SIG.RS256 // RSA-SHA256
Jwts.SIG.RS384 // RSA-SHA384
Jwts.SIG.RS512 // RSA-SHA512
// ECDSA算法
Jwts.SIG.ES256 // ECDSA-SHA256
Jwts.SIG.ES384 // ECDSA-SHA384
Jwts.SIG.ES512 // ECDSA-SHA512
// EdDSA算法
Jwts.SIG.EdDSA // EdDSA
实现JWT工具类
创建JWT工具类
首先,创建一个JWT工具类,封装令牌的生成、解析和验证等操作:
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secretKey;
@Value("${jwt.expiration}")
private long expirationTime;
@Value("${jwt.refresh-expiration}")
private long refreshExpirationTime;
// 生成访问令牌
public String generateAccessToken(UserDetails userDetails) {
return generateToken(userDetails, expirationTime);
}
// 生成刷新令牌
public String generateRefreshToken(UserDetails userDetails) {
return generateToken(userDetails, refreshExpirationTime);
}
// 生成令牌的通用方法
private String generateToken(UserDetails userDetails, long expiration) {
// 获取用户角色信息
List<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
// 创建自定义声明
Map<String, Object> claims = new HashMap<>();
claims.put("roles", roles);
// 构建并返回JWT令牌
return Jwts.builder()
.claims(claims)
.subject(userDetails.getUsername())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + expiration))
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS512)
.compact();
}
// 从令牌中获取用户名
public String getUsernameFromToken(String token) {
Jws<Claims> jws = Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseSignedClaims(token);
return jws.getBody().getSubject();
}
// 从令牌中获取角色
@SuppressWarnings("unchecked")
public List<String> getRolesFromToken(String token) {
Jws<Claims> jws = Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseSignedClaims(token);
return (List<String>) jws.getBody().get("roles");
}
// 验证令牌是否有效
public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseSignedClaims(token);
return true;
} catch (SignatureException ex) {
log.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
log.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
log.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
log.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
log.error("JWT claims string is empty");
}
return false;
}
}
实现用户认证
创建用户服务
实现用户详情服务,用于加载用户信息:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Autowired
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList())
);
}
}
实现认证控制器
创建认证控制器,处理用户登录请求并生成JWT令牌:
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtTokenProvider jwtTokenProvider;
@Autowired
public AuthController(AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtTokenProvider jwtTokenProvider) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtTokenProvider = jwtTokenProvider;
}
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
// 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
// 设置认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 加载用户详情
UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
// 生成JWT令牌
String accessToken = jwtTokenProvider.generateAccessToken(userDetails);
String refreshToken = jwtTokenProvider.generateRefreshToken(userDetails);
// 返回令牌
return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
String refreshToken = request.getRefreshToken();
if (jwtTokenProvider.validateToken(refreshToken)) {
String username = jwtTokenProvider.getUsernameFromToken(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
String newAccessToken = jwtTokenProvider.generateAccessToken(userDetails);
return ResponseEntity.ok(new JwtResponse(newAccessToken, refreshToken));
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
}
}
}
实现JWT认证过滤器
创建JWT认证过滤器,用于拦截请求并验证JWT令牌:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
@Autowired
public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider,
UserDetailsService userDetailsService) {
this.jwtTokenProvider = jwtTokenProvider;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// 从请求中获取JWT令牌
String jwt = getJwtFromRequest(request);
// 如果令牌存在且有效,则进行认证
if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
String username = jwtTokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 创建认证令牌
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
// 设置认证详情
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
// 继续过滤器链
filterChain.doFilter(request, response);
}
// 从请求头中获取JWT令牌
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
配置Spring Security
创建安全配置类
配置Spring Security,启用JWT认证:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
public SecurityConfig(UserDetailsService userDetailsService,
JwtAuthenticationFilter jwtAuthenticationFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 禁用CSRF保护,对于API通常不需要
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态会话
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 允许所有用户访问认证接口
.requestMatchers("/api/public/**").permitAll() // 允许所有用户访问公开接口
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 仅管理员可访问管理员接口
.anyRequest().authenticated() // 其他请求需要认证
)
.exceptionHandling(ex -> ex
.authenticationEntryPoint(unauthorizedHandler) // 未认证处理
.accessDeniedHandler(accessDeniedHandler) // 权限不足处理
);
// 添加JWT过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
实现资源访问控制
创建示例控制器
创建一个示例控制器,演示基于角色的访问控制:
@RestController
@RequestMapping("/api")
public class ApiController {
// 公开接口
@GetMapping("/public/hello")
public String publicHello() {
return "Hello, Public!";
}
// 认证用户可访问的接口
@GetMapping("/hello")
public String hello() {
return "Hello, User!";
}
// 仅管理员可访问的接口
@GetMapping("/admin/hello")
@PreAuthorize("hasRole('ADMIN')")
public String adminHello() {
return "Hello, Admin!";
}
// 使用方法级权限控制示例
@GetMapping("/users")
@PreAuthorize("hasAnyRole('ADMIN', 'USER_MANAGER')")
public List<String> getUsers() {
// 返回用户列表
return Arrays.asList("user1", "user2", "user3");
}
}
JWT高级特性
自定义声明
除了标准声明外,jjwt还支持添加自定义声明:
String jwt = Jwts.builder()
.claims()
.issuer("myapp")
.subject("user123")
.add("userId", 123) // 自定义用户ID
.add("roles", Arrays.asList("USER", "ADMIN")) // 自定义角色
.add("permissions", Arrays.asList("read", "write")) // 自定义权限
.and()
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS512)
.compact();
令牌压缩
jjwt支持对JWT负载进行压缩,减少令牌大小:
String jwt = Jwts.builder()
.claims()
.issuer("myapp")
.subject("user123")
.and()
.compressWith(CompressionCodecs.GZIP) // 使用GZIP压缩
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS512)
.compact();
加密JWT (JWE)
除了签名JWT(JWS)外,jjwt还支持加密JWT(JWE),提供更高的安全性:
// 生成加密密钥
SecretKey encryptionKey = Keys.secretKeyFor(Jwts.ENC.A256GCM);
// 创建JWE
String jwe = Jwts.builder()
.claims()
.issuer("myapp")
.subject("user123")
.and()
.encryptWith(encryptionKey, Jwts.KEY.A256KW, Jwts.ENC.A256GCM)
.compact();
// 解析JWE
Jwe<Claims> jwe = Jwts.parser()
.decryptWith(encryptionKey)
.build()
.parseEncryptedClaims(jwe);
Claims claims = jwe.getPayload();
JWT安全最佳实践1. 使用安全的签名算法
选择足够强度的签名算法,如HS256、RS256或ES256,避免使用不安全的算法。
// 推荐使用的签名算法
Jwts.SIG.HS256 // HMAC-SHA256,至少256位密钥
Jwts.SIG.RS256 // RSA-SHA256,至少2048位密钥
Jwts.SIG.ES256 // ECDSA-SHA256,使用P-256曲线
2. 保护签名密钥
签名密钥应保持机密,避免硬编码在代码中。在生产环境中,应使用安全的密钥管理服务。
// 不推荐:硬编码密钥
String secretKey = "my-secret-key";
// 推荐:从环境变量或安全配置服务获取密钥
String secretKey = System.getenv("JWT_SECRET_KEY");
3. 设置合理的令牌过期时间
访问令牌的过期时间不宜过长,建议设置为15分钟到1小时。对于长期访问,可以使用刷新令牌机制。
// 设置合理的过期时间(1小时)
long expirationTime = 3600000; // 毫秒
4. 使用HTTPS传输
始终使用HTTPS协议传输JWT令牌,防止中间人攻击和令牌被窃听。
5. 实现令牌撤销机制
尽管JWT本身不支持撤销,但可以通过以下方式实现类似功能:
- 维护令牌黑名单
- 使用短期令牌并结合刷新令牌机制
- 在关键声明中包含版本号或令牌ID
6. 验证所有必要的声明
在解析JWT时,确保验证所有必要的声明,如签发者、受众、过期时间等。
Jws<Claims> jws = Jwts.parser()
.verifyWith(verificationKey)
.requireIssuer("myapp") // 验证签发者
.requireAudience("myaudience") // 验证受众
.build()
.parseSignedClaims(jwt);
常见问题与解决方案
令牌过期处理
当JWT令牌过期时,客户端需要重新登录或使用刷新令牌获取新的访问令牌。
// 前端处理令牌过期的示例代码
axios.interceptors.response.use(
response => response,
error => {
const originalRequest = error.config;
// 如果是401错误且未尝试刷新令牌
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// 尝试使用刷新令牌获取新的访问令牌
return axios.post('/api/auth/refresh', { refreshToken: localStorage.getItem('refreshToken') })
.then(response => {
// 存储新的访问令牌
localStorage.setItem('accessToken', response.data.accessToken);
// 使用新令牌重试原始请求
originalRequest.headers['Authorization'] = 'Bearer ' + response.data.accessToken;
return axios(originalRequest);
})
.catch(err => {
// 刷新令牌也过期,需要重新登录
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(err);
});
}
return Promise.reject(error);
}
);
令牌吊销
实现令牌吊销的一种常见方法是维护一个令牌黑名单:
@Service
public class TokenBlacklistService {
private final Set<String> blacklist = ConcurrentHashMap.newKeySet();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
// 将令牌加入黑名单,并设置自动移除时间
public void blacklistToken(String token, long expirationTime) {
blacklist.add(token);
// 令牌过期后自动从黑名单中移除
scheduler.schedule(() -> blacklist.remove(token), expirationTime, TimeUnit.MILLISECONDS);
}
// 检查令牌是否在黑名单中
public boolean isTokenBlacklisted(String token) {
return blacklist.contains(token);
}
}
然后在JWT认证过滤器中添加黑名单检查:
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt)) {
// 检查令牌是否在黑名单中
if (tokenBlacklistService.isTokenBlacklisted(jwt)) {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "Token has been revoked");
return;
}
if (jwtTokenProvider.validateToken(jwt)) {
// 令牌有效,设置认证信息
// ...
}
}
} catch (Exception ex) {
// 处理异常
// ...
}
filterChain.doFilter(request, response);
}
总结与展望
本文详细介绍了如何在Spring Boot应用中使用jjwt库实现JWT认证授权。通过集成jjwt,我们可以构建出安全、高效、无状态的认证系统,非常适合分布式应用和微服务架构。
主要内容回顾:
- JWT的基本概念和工作原理
- 在Spring Boot中集成jjwt库
- 实现JWT工具类处理令牌的创建、解析和验证
- 构建基于JWT的认证和授权系统
- JWT高级特性和安全最佳实践
- 常见问题如令牌过期、刷新和吊销的解决方案
未来展望:
- 探索更高级的JWT特性,如JWE加密、声明加密等
- 结合OAuth2.0和OpenID Connect,构建更完整的身份认证系统
- 使用分布式缓存(如Redis)优化令牌黑名单和刷新令牌的存储
- 实现更细粒度的权限控制,如基于属性的访问控制(ABAC)
通过合理使用JWT,我们可以构建出更加安全、灵活和可扩展的认证授权系统,为用户提供更好的体验,同时简化系统架构和维护成本。
参考资料
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



