本文将深入探讨如何在Spring框架中整合JWT和OAuth2实现现代化的授权认证体系,通过实战代码和详细配置,帮助开发者构建安全可靠的微服务架构。
前言:现代应用的安全挑战
在微服务架构盛行的今天,传统的Session-Based认证方式已难以满足分布式系统的需求。JWT(JSON Web Token)和OAuth2作为现代认证授权的黄金组合,为开发者提供了更加灵活、安全的解决方案。
💡 TRAE IDE 智能提示:在编写安全配置类时,TRAE IDE会智能提示Spring Security的最新配置方式,避免使用过时的API,让你的代码始终保持最佳实践。
01|核心概念解析:JWT与OAuth2的协同机制
JWT:无状态认证的艺术
JWT是一种开放标准(RFC 7519),用于在网络应用环境间安全地传输信息。它的核心优势在于:无状态、可扩展、跨平台。
// JWT令牌结构示例
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"authorities": ["ROLE_USER", "ROLE_ADMIN"]
}OAuth2:授权标准的王者
OAuth2是一个授权框架,定义了四种授权模式:
- 授权码模式(Authorization Code)
- 隐式授权模式(Implicit)
- 密码模式(Resource Owner Password Credentials)
- 客户端模式(Client Credentials)
02|项目架构设计:整合思路与依赖配置
技术栈选择
<!-- pom.xml 核心依赖配置 -->
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
<!-- Spring Security OAuth2 -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.7.0</version>
</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>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
</dependencies>🚀 TRAE IDE 效率提升:使用TRAE IDE的依赖管理功能,可以自动检测依赖冲突,一键解决版本兼容性问题,让项目搭建事半功倍。
架构设计图
graph TD
A[客户端] -->|请求| B[Spring Security过滤器链]
B --> C{JWT令牌验证}
C -->|有效| D[OAuth2资源服务器]
C -->|无效| E[返回401错误]
D --> F[权限校验]
F -->|通过| G[业务服务]
F -->|拒绝| H[返回403错误]
G --> I[返回数据]
03|JWT工具类实现:令牌生成与验证
JWT配置属性类
@Component
@ConfigurationProperties(prefix = "jwt")
@Data
public class JwtProperties {
private String secret = "mySecretKey";
private long expiration = 86400; // 24小时
private String header = "Authorization";
private String prefix = "Bearer ";
@PostConstruct
public void init() {
// 使用TRAE IDE的代码检查功能,确保密钥复杂度符合安全要求
if (secret.length() < 32) {
throw new IllegalArgumentException("JWT密钥长度必须至少32位");
}
}
}JWT工具核心实现
@Slf4j
@Component
public class JwtTokenProvider {
@Autowired
private JwtProperties jwtProperties;
@Autowired
private UserDetailsService userDetailsService;
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecret());
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 生成JWT令牌
*/
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date expiryDate = new Date(System.currentTimeMillis() + jwtProperties.getExpiration() * 1000);
// 获取用户权限
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.claim("authorities", authorities)
.claim("userId", userPrincipal.getId())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
/**
* 从令牌中获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
/**
* 验证令牌有效性
*/
public boolean validateToken(String authToken) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(authToken);
return true;
} catch (SecurityException 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;
}
/**
* 从请求中提取令牌
*/
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(jwtProperties.getHeader());
if (bearerToken != null && bearerToken.startsWith(jwtProperties.getPrefix())) {
return bearerToken.substring(7);
}
return null;
}
}04|OAuth2授权服务器配置
授权服务器配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client-app")
.secret(passwordEncoder().encode("client-secret"))
.authorizedGrantTypes("password", "authorization_code", "refresh_token")
.scopes("read", "write", "trust")
.accessTokenValiditySeconds(3600) // 1小时
.refreshTokenValiditySeconds(2592000) // 30天
.autoApprove(true)
.redirectUris("http://localhost:8080/callback");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(tokenEnhancer());
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("mySecretKey");
return converter;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}自定义令牌增强器
public class CustomTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();
// 添加自定义信息
if (authentication.getPrincipal() instanceof UserPrincipal) {
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
additionalInfo.put("userId", user.getId());
additionalInfo.put("email", user.getEmail());
additionalInfo.put("organization", user.getOrganization());
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}05|资源服务器安全配置
资源服务器配置
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("api-resource")
.tokenStore(tokenStore())
.tokenExtractor(new BearerTokenExtractor());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/**", "/oauth/**", "/public/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/users/**").hasAnyRole("USER", "ADMIN")
.antMatchers(HttpMethod.POST, "/api/users/**").hasRole("ADMIN")
.antMatchers(HttpMethod.PUT, "/api/users/**").hasRole("ADMIN")
.antMatchers(HttpMethod.DELETE, "/api/users/**").hasRole("ADMIN")
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler())
.and()
.addFilterBefore(new JwtAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("mySecretKey");
return converter;
}
}JWT认证过滤器
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
public JwtAuthenticationFilter(JwtTokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = tokenProvider.resolveToken(request);
if (jwt != null && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.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) {
log.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
}06|用户认证服务实现
用户详情服务
@Service
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return UserPrincipal.create(user);
}
@Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("用户", "id", id));
return UserPrincipal.create(user);
}
}用户主体类
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserPrincipal implements UserDetails {
private Long id;
private String username;
private String email;
private String password;
private String organization;
private Collection<? extends GrantedAuthority> authorities;
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
return UserPrincipal.builder()
.id(user.getId())
.username(user.getUsername())
.email(user.getEmail())
.password(user.getPassword())
.organization(user.getOrganization())
.authorities(authorities)
.build();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}07|认证控制器实现
认证API接口
@RestController
@RequestMapping("/auth")
@Slf4j
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return ResponseEntity.ok(JwtAuthenticationResponse.builder()
.accessToken(jwt)
.tokenType("Bearer")
.expiresIn(tokenProvider.getJwtProperties().getExpiration())
.user(UserInfo.builder()
.id(userPrincipal.getId())
.username(userPrincipal.getUsername())
.email(userPrincipal.getEmail())
.organization(userPrincipal.getOrganization())
.build())
.build());
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ApiResponse(false, "用户名或密码错误"));
}
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "用户名已存在!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "邮箱地址已被注册!"));
}
// 创建用户
User user = User.builder()
.username(signUpRequest.getUsername())
.email(signUpRequest.getEmail())
.password(passwordEncoder.encode(signUpRequest.getPassword()))
.organization(signUpRequest.getOrganization())
.build();
User result = userRepository.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentContextPath().path("/users/{username}")
.buildAndExpand(result.getUsername()).toUri();
return ResponseEntity.created(location)
.body(new ApiResponse(true, "用户注册成功"));
}
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(HttpServletRequest request) {
String token = tokenProvider.resolveToken(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
String newToken = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new ApiResponse(true, "令牌刷新成功"));
}
return ResponseEntity.badRequest()
.body(new ApiResponse(false, "无效的刷新令牌"));
}
}请求响应DTO
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private String password;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class JwtAuthenticationResponse {
private String accessToken;
private String tokenType = "Bearer";
private long expiresIn;
private UserInfo user;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private Long id;
private String username;
private String email;
private String organization;
}