一、引言
在分布式系统和前后端分离架构中,传统的基于 Session 的认证方式存在跨域难处理、服务端存储压力大等问题。JWT(JSON Web Token) 作为一种轻量级的身份认证与授权方案,凭借其无状态、可跨域、易于扩展的特性,成为 Spring Boot 项目中实现认证授权的主流选择。本文将从环境搭建、核心实现到进阶优化,完整讲解 Spring Boot 整合 JWT 实现登录认证与接口授权的全流程。
二、技术栈与环境准备
1. 核心依赖
在 Spring Boot 项目的pom.xml(Maven)或build.gradle(Gradle)中引入以下
- Spring Security:提供认证与授权的基础框架
- JJWT:Java 领域主流的 JWT 工具库,支持 JWT 的生成、解析与验证
- Spring Web:用于编写接口测试认证授权流程
Maven 依赖配置示例:
xml
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<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>
2. 核心概念说明
JWT 结构: 由 Header(头部)、Payload(载荷)、Signature(签名)三部分组成,以.分隔,例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Header:存储算法类型和令牌类型,如{"alg":"HS256","typ":"JWT"}
- Payload:存储用户身份信息(如用户名、角色)和过期时间等声明,不建议存放敏感信息
- Signature:通过 Header 指定的算法,结合密钥对 Header 和 Payload 进行加密,保证令牌不被篡改
认证流程: 用户登录成功后,服务端生成 JWT 返回给客户端;客户端后续请求携带 JWT,服务端验证令牌有效性后完成授权
三、核心功能实现
1. JWT 工具类封装
java
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token-expire-time}")
private long accessTokenExpireTime;
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;
/**
* 生成访问令牌
*/
public String generateAccessToken(String username) {
Map<String, Object> claims = new HashMap<>();
return generateToken(claims, username, accessTokenExpireTime);
}
/**
* 生成刷新令牌
*/
public String generateRefreshToken(String username) {
Map<String, Object> claims = new HashMap<>();
return generateToken(claims, username, refreshTokenExpireTime);
}
/**
* 生成token
*/
private String generateToken(Map<String, Object> claims, String subject, long expireTime) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
/**
* 从token中获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getSubject();
}
/**
* 验证token是否有效
*/
public boolean validateToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 判断token是否过期
*/
public boolean isTokenExpired(String token) {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
}
/**
* 从token中获取claims
*/
private Claims getClaimsFromToken(String token) {
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
2. 实现认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 跳过登录和刷新令牌的接口
String requestURI = request.getRequestURI();
if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
chain.doFilter(request, response);
return;
}
// 从请求头获取token
String token = getTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtils.validateToken(token)) {
String username = jwtUtils.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);
} else {
SecurityContextHolder.clearContext();
}
chain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
3. 实现无感刷新令牌过滤器
创建JwtRefreshFilter实现令牌的无感刷新:
java
@Component
public class JwtRefreshFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
// 当token剩余有效期小于10分钟时,自动刷新
private static final long REFRESH_THRESHOLD = 10 * 60 * 1000;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 跳过登录和刷新接口
String requestURI = request.getRequestURI();
if (requestURI.equals("/api/auth/login") || requestURI.equals("/api/auth/refresh")) {
filterChain.doFilter(request, response);
return;
}
// 获取请求头中的token
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authorizationHeader.substring(7);
try {
if (jwtUtils.validateToken(token)) {
// 检查token是否即将过期
long remainingTime = jwtUtils.getTokenRemainingTime(token);
if (remainingTime < REFRESH_THRESHOLD) {
String username = jwtUtils.getUsernameFromToken(token);
String newAccessToken = jwtUtils.generateAccessToken(username);
// 将新token添加到响应头
response.setHeader("Authorization", "Bearer " + newAccessToken);
}
}
} catch (Exception e) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}
4. 配置 Spring Security
创建SecurityConfig配置安全规则:
java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtAuthFilter,
JwtRefreshFilter jwtRefreshFilter) throws Exception {
http
.cors().and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.antMatchers("/api/auth/**", "/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint((request, response, ex) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Unauthorized\",\"message\":\"Authentication required\"}");
});
// 添加JWT过滤器
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtRefreshFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
5. 实现认证服务
创建AuthService接口和实现类处理登录和刷新令牌业务:
java
public interface AuthService {
LoginResponseDTO login(LoginRequestDTO loginRequest);
LoginResponseDTO refreshToken(RefreshTokenRequestDTO refreshTokenRequest);
}
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;
@Override
public LoginResponseDTO login(LoginRequestDTO loginRequest) {
// 验证输入
if (loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername()) ||
StringUtils.isEmpty(loginRequest.getPassword())) {
throw new BadCredentialsException("Invalid login request");
}
try {
// 执行认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成token
String username = loginRequest.getUsername();
String accessToken = jwtUtils.generateAccessToken(username);
String refreshToken = jwtUtils.generateRefreshToken(username);
// 存储refresh token到Redis
String refreshTokenKey = "refresh_token:" + username;
stringRedisTemplate.opsForValue().set(
refreshTokenKey,
refreshToken,
refreshTokenExpireTime,
TimeUnit.MILLISECONDS
);
// 构建响应
LoginResponseDTO response = new LoginResponseDTO();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(refreshTokenExpireTime);
return response;
} catch (AuthenticationException e) {
throw new BadCredentialsException("Invalid username or password");
}
}
@Override
public LoginResponseDTO refreshToken(RefreshTokenRequestDTO refreshTokenRequest) {
// 验证输入
if (refreshTokenRequest == null || StringUtils.isEmpty(refreshTokenRequest.getRefreshToken())) {
throw new BadCredentialsException("Invalid refresh token request");
}
String refreshToken = refreshTokenRequest.getRefreshToken();
// 验证refresh token
if (!jwtUtils.validateToken(refreshToken)) {
throw new BadCredentialsException("Invalid refresh token");
}
String username = jwtUtils.getUsernameFromToken(refreshToken);
if (StringUtils.isEmpty(username)) {
throw new BadCredentialsException("Invalid refresh token");
}
// 验证Redis中存储的refresh token
String storedToken = stringRedisTemplate.opsForValue().get("refresh_token:" + username);
if (storedToken == null || !storedToken.equals(refreshToken)) {
throw new BadCredentialsException("Invalid refresh token");
}
// 生成新的token
String newAccessToken = jwtUtils.generateAccessToken(username);
String newRefreshToken = jwtUtils.generateRefreshToken(username);
// 更新Redis中的refresh token
stringRedisTemplate.opsForValue().set(
"refresh_token:" + username,
newRefreshToken,
refreshTokenExpireTime,
TimeUnit.MILLISECONDS
);
LoginResponseDTO response = new LoginResponseDTO();
response.setAccessToken(newAccessToken);
response.setRefreshToken(newRefreshToken);
response.setExpiresIn(refreshTokenExpireTime);
return response;
}
}
四、相关注解总结
| 注解名称 | 核心作用 | 使用场景 | 关键注意事项 |
|---|---|---|---|
@EnableWebSecurity |
开启 Spring Security 的 Web 安全功能,加载安全过滤器链和相关配置 | 标注在自定义的 Spring Security 配置类上 | 必须搭配@Configuration使用,否则配置不生效 |
@EnableGlobalMethodSecurity |
开启方法级权限控制,支持多种权限注解 | 标注在 Security 配置类上,需指定启用的注解类型 | 常用属性:prePostEnabled=true(启用@PreAuthorize等)、securedEnabled=true(启用@Secured) |
@PreAuthorize |
方法执行前校验权限,支持 SpEL 表达式,可实现复杂权限判断 | 控制器接口方法、服务层方法的权限控制(细粒度权限) | 依赖@EnableGlobalMethodSecurity(prePostEnabled=true),支持角色、权限、请求参数校验 |
@PostAuthorize |
方法执行后校验权限,基于方法返回值判断 | 需根据返回结果控制权限的场景(极少使用,避免方法执行产生副作用) | SpEL 表达式中用returnObject指代方法返回值 |
@Secured |
基于角色的粗粒度权限控制 | 控制器或服务层方法的角色校验 | 需启用securedEnabled=true,角色名称必须以ROLE_为前缀 |
@RolesAllowed |
JSR-250 规范注解,基于角色的权限控制 | 控制器或服务层方法的多角色授权 | 需启用jsr250Enabled=true,角色名称可省略ROLE_前缀(框架自动补充) |
@AuthenticationPrincipal |
直接获取当前认证用户的UserDetails或自定义用户信息 |
控制器方法参数中,需获取当前登录用户信息时 | 无需手动从SecurityContextHolder获取,直接注入即可 |
@PreFilter |
方法执行前,对集合类型参数进行过滤,仅保留符合权限的元素 | 数据查询前的参数过滤(数据级权限控制) | 仅对集合类型参数生效,SpEL 表达式用filterObject指代集合元素 |
@PostFilter |
方法执行后,对集合类型返回值进行过滤,仅保留符合权限的元素 | 数据返回后的结果过滤(数据级权限控制) | 仅对集合类型返回值生效,依赖@EnableGlobalMethodSecurity启用 |
总结
本指南介绍了如何在 Spring Boot 应用中实现基于 JWT 的认证授权功能,包括:
- JWT 令牌的生成与验证
- 基于 Spring Security 的认证过滤器
- 令牌刷新机制与无感刷新实现
- 完整的登录与授权流程
通过这种方式,我们可以实现无状态的认证系统,适合分布式应用和前后端分离架构。实际应用中,还可以根据需要添加更多功能,如令牌撤销、角色权限控制等。