跳到主要内容

SpringCloud OAuth2服务安全深度解析

📖 概述

在微服务架构中,服务安全和认证是保障系统稳定性和数据安全的核心组件。OAuth2 作为行业标准授权框架,结合 Spring Security 提供了完整的安全解决方案。本文详细解析 SpringCloud OAuth2 的核心原理、实战应用和面试要点。

🎯 学习目标

  • 理解 OAuth2 的核心概念和授权模式
  • 掌握 Spring Security OAuth2 的架构和配置
  • 熟悉 JWT 令牌机制和最佳实践
  • 了解微服务架构下的认证与授权方案
  • 掌握单点登录和网关安全实现
  • 熟悉安全设计模式和面试高频问题

1. OAuth2 基础概念

1.1 什么是 OAuth2

OAuth2 是一个授权框架,允许第三方应用程序获得对用户账户的有限访问权限。它定义了四种角色:

1.2 OAuth2 四种角色详解

角色描述示例
资源所有者能够授权访问受保护资源的实体用户本人
资源服务器托管受保护资源的服务API 服务、文件服务
客户端代表资源所有者请求访问资源的应用移动应用、Web 应用
授权服务器成功认证资源所有者并颁发令牌的服务认证中心

1.3 OAuth2 核心术语

  • Access Token:访问令牌,用于访问受保护的资源
  • Refresh Token:刷新令牌,用于获取新的访问令牌
  • Scope:权限范围,定义访问令牌的权限边界
  • Grant Type:授权类型,客户端获取访问令牌的方式
  • Client Credentials:客户端凭证,用于客户端认证

2. OAuth2 授权模式详解

2.1 授权码模式(Authorization Code)

最常用和最安全的授权模式,适用于 Web 应用。

@RestController
@RequestMapping("/oauth")
public class AuthorizationCodeController {

@GetMapping("/authorize")
public String authorize(
@RequestParam String response_type,
@RequestParam String client_id,
@RequestParam String redirect_uri,
@RequestParam String scope,
@RequestParam String state) {

// 1. 验证客户端信息
if (!validateClient(client_id, redirect_uri)) {
return "error: invalid_client";
}

// 2. 构建授权请求
String authRequest = String.format(
"https://auth-server.com/oauth/authorize?" +
"response_type=%s&client_id=%s&redirect_uri=%s&scope=%s&state=%s",
response_type, client_id, redirect_uri, scope, state
);

return "redirect:" + authRequest;
}

@PostMapping("/token")
public ResponseEntity<TokenResponse> getToken(
@RequestParam String grant_type,
@RequestParam String code,
@RequestParam String redirect_uri,
@RequestParam String client_id,
@RequestParam String client_secret) {

// 1. 验证授权码
AuthorizationCode authCode = validateAuthorizationCode(code);
if (authCode == null || authCode.isExpired()) {
return ResponseEntity.badRequest().build();
}

// 2. 验证客户端凭证
if (!authenticateClient(client_id, client_secret)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

// 3. 生成访问令牌
TokenResponse tokenResponse = generateTokens(authCode.getUserId(), authCode.getScope());

return ResponseEntity.ok(tokenResponse);
}
}

2.2 隐藏式模式(Implicit)

适用于单页应用(SPA),但不推荐在生产环境中使用。

// 前端 JavaScript 实现
class ImplicitFlowClient {
constructor(clientId, redirectUri, authServer) {
this.clientId = clientId;
this.redirectUri = redirectUri;
this.authServer = authServer;
}

// 获取授权
authorize(scopes = ['read', 'write']) {
const params = new URLSearchParams({
response_type: 'token',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: scopes.join(' '),
state: this.generateState()
});

const authUrl = `${this.authServer}/oauth/authorize?${params.toString()}`;
window.location.href = authUrl;
}

// 处理回调
handleCallback() {
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);

const accessToken = params.get('access_token');
const tokenType = params.get('token_type');
const expiresIn = params.get('expires_in');
const state = params.get('state');

if (this.validateState(state)) {
this.storeToken(accessToken, expiresIn);
return true;
}

return false;
}

generateState() {
return Math.random().toString(36).substring(2, 15);
}

validateState(state) {
const storedState = localStorage.getItem('oauth_state');
return storedState === state;
}

storeToken(accessToken, expiresIn) {
localStorage.setItem('access_token', accessToken);
if (expiresIn) {
const expiresAt = Date.now() + (parseInt(expiresIn) * 1000);
localStorage.setItem('token_expires_at', expiresAt);
}
}
}

2.3 客户端凭证模式(Client Credentials)

适用于服务端到服务端的调用。

@Service
public class ClientCredentialsService {

@Value("${oauth.client-id}")
private String clientId;

@Value("${oauth.client-secret}")
private String clientSecret;

@Value("${oauth.token-uri}")
private String tokenUri;

@Autowired
private RestTemplate restTemplate;

public String getAccessToken() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(clientId, clientSecret);

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
body.add("scope", "read write");

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);

try {
ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
tokenUri, request, TokenResponse.class
);

if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody().getAccessToken();
}
} catch (Exception e) {
log.error("获取访问令牌失败", e);
}

throw new AuthenticationException("无法获取访问令牌");
}
}

2.4 密码模式(Resource Owner Password Credentials)

适用于可信的应用程序,如官方移动应用。

@RestController
@RequestMapping("/oauth")
public class PasswordGrantController {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private TokenService tokenService;

@PostMapping("/token")
public ResponseEntity<TokenResponse> getTokenWithPassword(
@RequestParam String grant_type,
@RequestParam String username,
@RequestParam String password,
@RequestParam String client_id,
@RequestParam String client_secret,
@RequestParam(required = false) String scope) {

// 1. 验证客户端凭证
if (!authenticateClient(client_id, client_secret)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

// 2. 认证用户
Authentication authentication = authenticateUser(username, password);
if (authentication == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

// 3. 生成令牌
TokenResponse tokenResponse = tokenService.createTokens(
authentication.getName(),
scope != null ? scope : "read"
);

return ResponseEntity.ok(tokenResponse);
}

private Authentication authenticateUser(String username, String password) {
try {
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password);
return authenticationManager.authenticate(token);
} catch (AuthenticationException e) {
return null;
}
}
}

3. Spring Security OAuth2 架构

3.1 授权服务器配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private TokenStore tokenStore;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client-app")
.secret(passwordEncoder().encode("client-secret"))
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.scopes("read", "write")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(2592000)
.redirectUris("http://localhost:8080/callback")
.and()
.withClient("service-to-service")
.secret(passwordEncoder().encode("service-secret"))
.authorizedGrantTypes("client_credentials")
.scopes("read")
.accessTokenValiditySeconds(7200);
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenStore(tokenStore)
.accessTokenConverter(accessTokenConverter())
.reuseRefreshTokens(false);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("jwt-secret-key");
return converter;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

3.2 资源服务器配置

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("resource-server-id")
.stateless(true)
.tokenStore(tokenStore());
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("jwt-secret-key");
converter.setVerifierKey("jwt-secret-key");
return converter;
}

@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}

3.3 Web 安全配置

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Autowired
private PasswordEncoder passwordEncoder;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login?logout")
.permitAll();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build(),
User.withUsername("admin")
.password(passwordEncoder.encode("admin"))
.roles("ADMIN", "USER")
.build()
);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

4. JWT 令牌机制详解

4.1 JWT 结构和组成

JWT 由三部分组成:Header、Payload、Signature

@Component
public class JwtTokenUtil {

@Value("${jwt.secret}")
private String jwtSecret;

@Value("${jwt.expiration}")
private int jwtExpiration;

// 生成 JWT 令牌
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", userDetails.getUsername());
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
claims.put("iat", System.currentTimeMillis() / 1000);
claims.put("exp", (System.currentTimeMillis() + jwtExpiration * 1000) / 1000);

return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}

// 解析 JWT 令牌
public Claims parseToken(String token) {
try {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new JwtTokenExpiredException("令牌已过期");
} catch (UnsupportedJwtException e) {
throw new JwtTokenMalformedException("不支持的令牌");
} catch (MalformedJwtException e) {
throw new JwtTokenMalformedException("令牌格式错误");
} catch (SignatureException e) {
throw new JwtTokenMalformedException("令牌签名无效");
}
}

// 验证令牌
public boolean validateToken(String token, UserDetails userDetails) {
try {
Claims claims = parseToken(token);
String username = claims.getSubject();
return username.equals(userDetails.getUsername()) &&
!isTokenExpired(claims);
} catch (Exception e) {
return false;
}
}

private boolean isTokenExpired(Claims claims) {
Date expiration = claims.getExpiration();
return expiration != null && expiration.before(new Date());
}

// 刷新令牌
public String refreshToken(String token) {
Claims claims = parseToken(token);
claims.setIssuedAt(new Date());
claims.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration * 1000));

return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
}

4.2 JWT 认证过滤器

public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

final String requestTokenHeader = request.getHeader("Authorization");

String username = null;
String jwtToken = null;

if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("无法获取 JWT 令牌", e);
} catch (ExpiredJwtException e) {
logger.error("JWT 令牌已过期", e);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "令牌已过期");
return;
}
}

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}

filterChain.doFilter(request, response);
}
}

4.3 JWT 令牌增强

@Component
public class CustomTokenEnhancer implements TokenEnhancer {

@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> additionalInfo = new HashMap<>();

// 添加用户信息
User user = (User) authentication.getPrincipal();
additionalInfo.put("userId", user.getId());
additionalInfo.put("email", user.getEmail());
additionalInfo.put("department", user.getDepartment());

// 添加权限信息
List<String> permissions = user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());
additionalInfo.put("permissions", permissions);

// 添加令牌元数据
additionalInfo.put("tokenType", "JWT");
additionalInfo.put("issuer", "spring-cloud-oauth2-server");

((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}

5. 微服务认证与授权

5.1 网关统一认证

@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfig {

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/auth/**", "/actuator/**").permitAll()
.pathMatchers("/api/public/**").permitAll()
.anyExchange().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter())
.and()
.build();
}

@Bean
public Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("roles");

ReactiveJwtAuthenticationConverter converter = new ReactiveJwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}

@Bean
public GlobalFilter authGlobalFilter() {
return new AuthGlobalFilter();
}
}

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();

// 检查是否需要认证
if (isPublicPath(request.getURI().getPath())) {
return chain.filter(exchange);
}

// 提取 JWT 令牌
String token = extractToken(request);
if (token == null) {
return handleUnauthorized(exchange);
}

try {
// 验证令牌
Claims claims = jwtTokenUtil.parseToken(token);

// 添加用户信息到请求头
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Name", claims.get("name", String.class))
.header("X-User-Roles", String.join(",", (List<String>) claims.get("roles")))
.build();

return chain.filter(exchange.mutate().request(modifiedRequest).build());

} catch (Exception e) {
return handleUnauthorized(exchange);
}
}

private boolean isPublicPath(String path) {
return path.startsWith("/auth/") ||
path.startsWith("/actuator/") ||
path.startsWith("/api/public/");
}

private String extractToken(ServerHttpRequest request) {
String authHeader = request.getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}

private Mono<Void> handleUnauthorized(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);

String body = "{\"error\":\"unauthorized\",\"message\":\"认证失败\"}";
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes());
return response.writeWith(Mono.just(buffer));
}

@Override
public int getOrder() {
return -100;
}
}

5.2 服务间认证

@Configuration
public class ServiceToServiceConfig {

@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(List.of(new ServiceAuthInterceptor()));
return restTemplate;
}

@Bean
@LoadBalanced
public RestTemplate loadBalancedRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(List.of(new ServiceAuthInterceptor()));
return restTemplate;
}
}

@Component
public class ServiceAuthInterceptor implements ClientHttpRequestInterceptor {

@Autowired
private ServiceTokenService serviceTokenService;

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
// 获取服务令牌
String token = serviceTokenService.getServiceToken();

// 添加认证头
HttpRequest modifiedRequest = new HttpRequestDecorator(request) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders(super.getHeaders());
headers.setBearerAuth(token);
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};

return execution.execute(modifiedRequest, body);
}
}

@Service
public class ServiceTokenService {

@Value("${oauth2.client-id}")
private String clientId;

@Value("${oauth2.client-secret}")
private String clientSecret;

@Value("${oauth2.token-uri}")
private String tokenUri;

private String cachedToken;
private long tokenExpiryTime;

@Autowired
private RestTemplate restTemplate;

public String getServiceToken() {
if (cachedToken == null || System.currentTimeMillis() > tokenExpiryTime) {
refreshToken();
}
return cachedToken;
}

private synchronized void refreshToken() {
if (cachedToken != null && System.currentTimeMillis() < tokenExpiryTime) {
return;
}

try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBasicAuth(clientId, clientSecret);

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "client_credentials");
body.add("scope", "service");

HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);

ResponseEntity<TokenResponse> response = restTemplate.postForEntity(
tokenUri, request, TokenResponse.class
);

if (response.getStatusCode().is2xxSuccessful()) {
TokenResponse tokenResponse = response.getBody();
cachedToken = tokenResponse.getAccessToken();
tokenExpiryTime = System.currentTimeMillis() +
(tokenResponse.getExpiresIn() - 60) * 1000; // 提前1分钟刷新
}
} catch (Exception e) {
log.error("获取服务令牌失败", e);
throw new ServiceAuthException("无法获取服务令牌");
}
}
}

6. 单点登录(SSO)实现

6.1 SSO 配置

@Configuration
@EnableWebSecurity
public class SsoSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/home", "/error").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/")
.permitAll()
.and()
.oauth2Login()
.loginPage("/oauth2/authorization/sso-client")
.authorizationEndpoint()
.baseUri("/oauth2/authorization")
.and()
.redirectionEndpoint()
.baseUri("/login/oauth2/code/*")
.and()
.userInfoEndpoint()
.userService(customOAuth2UserService())
.and()
.successHandler(oAuth2AuthenticationSuccessHandler())
.failureHandler(oAuth2AuthenticationFailureHandler());
}

@Bean
public CustomOAuth2UserService customOAuth2UserService() {
return new CustomOAuth2UserService();
}

@Bean
public OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler() {
return new CustomOAuth2SuccessHandler();
}

@Bean
public OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler() {
return new SimpleUrlAuthenticationFailureHandler("/login?error");
}
}

@Component
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

@Autowired
private UserService userService;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);

String registrationId = userRequest.getClientRegistration().getRegistrationId();
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");

// 查找或创建用户
User user = userService.findOrCreateOAuthUser(registrationId, email, name);

// 转换为自定义用户对象
return new CustomOAuth2User(oauth2User, user);
}
}

@Component
public class CustomOAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

@Autowired
private JwtTokenUtil jwtTokenUtil;

@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
CustomOAuth2User oauth2User = (CustomOAuth2User) authentication.getPrincipal();

// 生成 JWT 令牌
UserDetails userDetails = oauth2User.getUserDetails();
String token = jwtTokenUtil.generateToken(userDetails);

// 重定向到前端应用,携带令牌
String redirectUrl = String.format("%s?token=%s",
getDefaultTargetUrl(), token);

getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
}

6.2 多租户认证

@Component
public class MultiTenantAuthenticationManager implements AuthenticationManager {

@Autowired
private Map<String, AuthenticationManager> tenantAuthenticationManagers;

@Autowired
private TenantService tenantService;

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String tenantId = extractTenantId(authentication);

// 验证租户
if (!tenantService.isValidTenant(tenantId)) {
throw new BadCredentialsException("无效的租户");
}

// 获取租户专用的认证管理器
AuthenticationManager tenantAuthManager = tenantAuthenticationManagers.get(tenantId);
if (tenantAuthManager == null) {
throw new AuthenticationServiceException("租户认证管理器不存在");
}

// 执行租户认证
Authentication result = tenantAuthManager.authenticate(authentication);

// 添加租户信息到认证结果
((UsernamePasswordAuthenticationToken) result).setDetails(
TenantAuthenticationDetails.builder()
.tenantId(tenantId)
.tenantName(tenantService.getTenantName(tenantId))
.build()
);

return result;
}

private String extractTenantId(Authentication authentication) {
if (authentication.getDetails() instanceof WebAuthenticationDetails) {
String tenantHeader = ((WebAuthenticationDetails) authentication.getDetails())
.getSessionId(); // 或从请求头获取
return tenantHeader;
}
return "default";
}
}

7. 实战案例分析

7.1 电商平台安全架构

# application.yml - 认证服务配置
server:
port: 8080

spring:
application:
name: auth-service

datasource:
url: jdbc:mysql://localhost:3306/auth_db
username: auth_user
password: auth_password

redis:
host: localhost
port: 6379
database: 0

security:
oauth2:
authorizationserver:
issuer: http://localhost:8080

# JWT 配置
jwt:
secret: ${JWT_SECRET:mySecretKey}
expiration: 3600
refresh-expiration: 2592000

# OAuth2 客户端配置
oauth2:
clients:
web-app:
client-id: web-client
client-secret: ${WEB_CLIENT_SECRET}
authorized-grant-types: authorization_code,refresh_token
scopes: read,write,profile
redirect-uris: http://localhost:3000/callback
mobile-app:
client-id: mobile-client
client-secret: ${MOBILE_CLIENT_SECRET}
authorized-grant-types: password,refresh_token
scopes: read,write,profile
admin-app:
client-id: admin-client
client-secret: ${ADMIN_CLIENT_SECRET}
authorized-grant-types: client_credentials
scopes: admin,read,write
// 电商平台用户服务
@Service
@Transactional
public class ECommerceUserService implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Autowired
private RoleRepository roleRepository;

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private MailService mailService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));

if (!user.isActive()) {
throw new UserAccountExpiredException("账户已被禁用");
}

return buildUserDetails(user);
}

private UserDetails buildUserDetails(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());

// 添加权限
user.getRoles().forEach(role ->
role.getPermissions().forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission.getName()))
)
);

return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.accountExpired(!user.isActive())
.accountLocked(user.isLocked())
.credentialsExpired(user.isPasswordExpired())
.disabled(!user.isActive())
.build();
}

// 用户注册
public User registerUser(RegistrationRequest request) {
// 1. 验证用户信息
validateRegistrationRequest(request);

// 2. 创建用户
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setPhone(request.getPhone());
user.setActive(false); // 需要邮箱验证
user.setEmailVerificationToken(UUID.randomUUID().toString());

// 3. 分配默认角色
Role defaultRole = roleRepository.findByName("USER")
.orElseThrow(() -> new RuntimeException("默认角色不存在"));
user.getRoles().add(defaultRole);

// 4. 保存用户
user = userRepository.save(user);

// 5. 发送验证邮件
mailService.sendEmailVerification(user.getEmail(), user.getEmailVerificationToken());

return user;
}

// 邮箱验证
public void verifyEmail(String token) {
User user = userRepository.findByEmailVerificationToken(token)
.orElseThrow(() -> new RuntimeException("验证令牌无效"));

if (user.isActive()) {
throw new RuntimeException("账户已经激活");
}

user.setActive(true);
user.setEmailVerificationToken(null);
user.setEmailVerifiedAt(LocalDateTime.now());

userRepository.save(user);
}

// 重置密码
public void resetPassword(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new RuntimeException("邮箱不存在"));

String resetToken = UUID.randomUUID().toString();
user.setPasswordResetToken(resetToken);
user.setPasswordResetExpiry(LocalDateTime.now().plusHours(24));

userRepository.save(user);

mailService.sendPasswordReset(email, resetToken);
}

// 更改密码
public void changePassword(String token, String newPassword) {
User user = userRepository.findByPasswordResetToken(token)
.orElseThrow(() -> new RuntimeException("重置令牌无效"));

if (user.getPasswordResetExpiry().isBefore(LocalDateTime.now())) {
throw new RuntimeException("重置令牌已过期");
}

user.setPassword(passwordEncoder.encode(newPassword));
user.setPasswordResetToken(null);
user.setPasswordResetExpiry(null);
user.setPasswordChangedAt(LocalDateTime.now());

userRepository.save(user);
}

private void validateRegistrationRequest(RegistrationRequest request) {
// 用户名唯一性检查
if (userRepository.existsByUsername(request.getUsername())) {
throw new RuntimeException("用户名已存在");
}

// 邮箱唯一性检查
if (userRepository.existsByEmail(request.getEmail())) {
throw new RuntimeException("邮箱已注册");
}

// 手机号唯一性检查
if (userRepository.existsByPhone(request.getPhone())) {
throw new RuntimeException("手机号已注册");
}
}
}

7.2 订单服务权限控制

@RestController
@RequestMapping("/api/orders")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public class OrderController {

@Autowired
private OrderService orderService;

@Autowired
private PermissionService permissionService;

@GetMapping
@PreAuthorize("hasAuthority('ORDER_READ')")
public ResponseEntity<Page<OrderResponse>> getOrders(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String status,
Authentication authentication) {

// 获取当前用户信息
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Long userId = userDetails.getUserId();

// 检查权限
if (!permissionService.hasPermission(userId, "ORDER_READ")) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}

Page<OrderResponse> orders = orderService.getUserOrders(userId, page, size, status);
return ResponseEntity.ok(orders);
}

@PostMapping
@PreAuthorize("hasAuthority('ORDER_CREATE')")
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request,
Authentication authentication) {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

// 验证用户权限
if (!permissionService.canCreateOrder(userDetails.getUserId(), request)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}

OrderResponse order = orderService.createOrder(userDetails.getUserId(), request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}

@PutMapping("/{orderId}")
@PreAuthorize("hasAuthority('ORDER_UPDATE')")
public ResponseEntity<OrderResponse> updateOrder(
@PathVariable Long orderId,
@Valid @RequestBody UpdateOrderRequest request,
Authentication authentication) {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

// 检查订单所有权
if (!orderService.isOrderOwner(orderId, userDetails.getUserId())) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}

OrderResponse order = orderService.updateOrder(orderId, request);
return ResponseEntity.ok(order);
}

@DeleteMapping("/{orderId}")
@PreAuthorize("hasAuthority('ORDER_DELETE')")
public ResponseEntity<Void> deleteOrder(
@PathVariable Long orderId,
Authentication authentication) {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

// 检查订单所有权和删除权限
if (!orderService.isOrderOwner(orderId, userDetails.getUserId()) ||
!permissionService.canDeleteOrder(userDetails.getUserId(), orderId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}

orderService.deleteOrder(orderId);
return ResponseEntity.noContent().build();
}
}

// 自定义权限注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasPermission(#orderId, 'ORDER', 'OWNER')")
public @interface OrderOwner {
}

@Service
public class PermissionService {

@Autowired
private UserRepository userRepository;

@Autowired
private RoleRepository roleRepository;

public boolean hasPermission(Long userId, String permission) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("用户不存在"));

return user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.anyMatch(p -> p.getName().equals(permission));
}

public boolean canCreateOrder(Long userId, CreateOrderRequest request) {
// 检查用户是否有创建订单的权限
if (!hasPermission(userId, "ORDER_CREATE")) {
return false;
}

// 检查用户状态
User user = userRepository.findById(userId).orElse(null);
if (user == null || !user.isActive()) {
return false;
}

// 其他业务规则检查
return true;
}

public boolean canDeleteOrder(Long userId, Long orderId) {
// 检查删除权限
if (!hasPermission(userId, "ORDER_DELETE")) {
return false;
}

// 检查订单状态(只有特定状态的订单可以删除)
return true;
}
}

// 自定义用户详情类
public class CustomUserDetails implements UserDetails {
private Long userId;
private String username;
private String password;
private String email;
private List<GrantedAuthority> authorities;
private boolean active;
private String tenantId;

// 构造函数、getter 和 setter 方法

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return active;
}
}

8. 面试高频问题

8.1 基础概念题

Q1: 什么是 OAuth2?它解决了什么问题?

A: OAuth2 是一个授权框架,允许第三方应用程序获得对用户账户的有限访问权限。它解决了以下问题:

  • 安全性:避免第三方应用存储用户凭据
  • 权限控制:细粒度的权限管理
  • 用户体验:用户只需授权一次,无需重复登录
  • 标准化:提供了标准的授权流程

Q2: OAuth2 的四种授权模式有什么区别?

A:

模式适用场景安全性复杂度
授权码模式Web 应用,服务器端
隐藏式模式单页应用,移动端
密码模式可信的第一方应用
客户端凭证服务端到服务端

8.2 原理理解题

Q3: JWT 令牌的工作原理是什么?有什么优缺点?

A: JWT (JSON Web Token) 是一个开放标准,用于在各方之间安全地传输信息。

工作原理:

  1. 用户登录成功后,服务器生成包含用户信息的 JWT
  2. 服务器将 JWT 返回给客户端
  3. 客户端在后续请求中携带 JWT
  4. 服务器验证 JWT 的有效性

优点:

  • 无状态,易于扩展
  • 支持跨域
  • 包含用户信息,减少数据库查询
  • 自包含,验证简单

缺点:

  • 无法撤销已签发的令牌
  • 令牌大小较大
  • 需要确保传输安全

Q4: Spring Security 的认证流程是怎样的?

A: Spring Security 的认证流程包括:

  1. 用户提交请求:携带认证信息
  2. 过滤器拦截:SecurityContextPersistenceFilter 等
  3. 认证管理器:AuthenticationManager 处理认证
  4. 认证提供者:AuthenticationProvider 具体验证逻辑
  5. 用户详情服务:UserDetailsService 加载用户信息
  6. 创建认证对象:Authentication 对象
  7. 存储安全上下文:SecurityContextHolder
  8. 授权检查:基于角色和权限的访问控制

8.3 实战应用题

Q5: 如何实现微服务架构下的统一认证?

A: 微服务统一认证的几种方案:

// 方案1:网关统一认证
@Configuration
public class GatewayAuthConfig {

@Bean
public GlobalFilter authFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();

// 公开路径直接通过
if (isPublicPath(path)) {
return chain.filter(exchange);
}

// 验证 JWT 令牌
String token = extractToken(request);
if (validateToken(token)) {
return chain.filter(exchange);
}

// 返回未授权
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
};
}
}

// 方案2:服务间认证
@Service
public class ServiceAuthService {

public String getServiceToken() {
// 使用客户端凭证模式获取令牌
return oauth2Client.getToken();
}

public boolean validateServiceToken(String token) {
// 验证服务令牌
return tokenValidator.validate(token);
}
}

Q6: 如何实现细粒度的权限控制?

A: 基于角色和权限的访问控制(RBAC):

@Entity
public class User {
@ManyToMany
private Set<Role> roles;
}

@Entity
public class Role {
@ManyToMany
private Set<Permission> permissions;
}

@Entity
public class Permission {
private String name;
private String resource;
private String action;
}

@Service
public class PermissionService {

public boolean hasPermission(Long userId, String resource, String action) {
User user = userRepository.findById(userId).orElse(null);
if (user == null) return false;

return user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.anyMatch(p -> p.getResource().equals(resource) &&
p.getAction().equals(action));
}
}

// 使用注解进行权限控制
@PreAuthorize("@permissionService.hasPermission(authentication.name, 'ORDER', 'READ')")
public List<Order> getOrders() {
return orderService.findAll();
}

Q7: 如何处理令牌刷新和安全存储?

A: 令牌刷新和安全存储方案:

@Service
public class TokenRefreshService {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

public String refreshAccessToken(String refreshToken) {
// 1. 验证刷新令牌
if (!validateRefreshToken(refreshToken)) {
throw new TokenRefreshException("无效的刷新令牌");
}

// 2. 获取用户信息
String username = extractUsernameFromRefreshToken(refreshToken);

// 3. 生成新的访问令牌
String newAccessToken = generateAccessToken(username);

// 4. 更新 Redis 中的令牌映射
updateTokenMapping(username, newAccessToken, refreshToken);

return newAccessToken;
}

public void revokeToken(String token) {
String username = extractUsername(token);
String key = "token:blacklist:" + token;
redisTemplate.opsForValue().set(key, "revoked", Duration.ofHours(24));
}
}

9. 安全最佳实践

9.1 令牌安全

@Configuration
public class TokenSecurityConfig {

@Bean
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
// 添加安全相关的额外信息
Map<String, Object> additionalInfo = new HashMap<>();

// 添加令牌指纹(防止令牌劫持)
additionalInfo.put("jti", UUID.randomUUID().toString());

// 添加颁发者信息
additionalInfo.put("iss", "https://auth.company.com");

// 添加受众信息
additionalInfo.put("aud", "company-apps");

// 添加权限范围
additionalInfo.put("scope", getScopes(authentication));

((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}

@Bean
public TokenStore tokenStore() {
// 使用 JWT + Redis 存储,既保证无状态又支持撤销
return new JwtTokenStore(accessTokenConverter());
}
}

9.2 密码安全

@Service
public class PasswordSecurityService {

@Autowired
private PasswordEncoder passwordEncoder;

@Value("${password.policy.min-length:8}")
private int minPasswordLength;

@Value("${password.policy.require-uppercase:true}")
private boolean requireUppercase;

@Value("${password.policy.require-lowercase:true}")
private boolean requireLowercase;

@Value("${password.policy.require-digits:true}")
private boolean requireDigits;

@Value("${password.policy.require-special-chars:true}")
private boolean requireSpecialChars;

public void validatePassword(String password) {
if (password == null || password.length() < minPasswordLength) {
throw new PasswordPolicyException("密码长度至少为 " + minPasswordLength + " 位");
}

if (requireUppercase && !containsUppercase(password)) {
throw new PasswordPolicyException("密码必须包含大写字母");
}

if (requireLowercase && !containsLowercase(password)) {
throw new PasswordPolicyException("密码必须包含小写字母");
}

if (requireDigits && !containsDigits(password)) {
throw new PasswordPolicyException("密码必须包含数字");
}

if (requireSpecialChars && !containsSpecialChars(password)) {
throw new PasswordPolicyException("密码必须包含特殊字符");
}

// 检查密码是否在常见密码列表中
if (isCommonPassword(password)) {
throw new PasswordPolicyException("密码过于常见,请使用更复杂的密码");
}
}

public String encodePassword(String rawPassword) {
// 使用 BCrypt 加密,包含盐值
return passwordEncoder.encode(rawPassword);
}

public boolean checkPassword(String rawPassword, String encodedPassword) {
return passwordEncoder.matches(rawPassword, encodedPassword);
}

// 密码强度评估
public PasswordStrength assessPasswordStrength(String password) {
int score = 0;

if (password.length() >= 12) score += 2;
else if (password.length() >= 8) score += 1;

if (containsUppercase(password)) score += 1;
if (containsLowercase(password)) score += 1;
if (containsDigits(password)) score += 1;
if (containsSpecialChars(password)) score += 1;
if (password.length() >= 16) score += 1;

if (score >= 6) return PasswordStrength.STRONG;
if (score >= 4) return PasswordStrength.MEDIUM;
return PasswordStrength.WEAK;
}

private boolean containsUppercase(String password) {
return !password.equals(password.toLowerCase());
}

private boolean containsLowercase(String password) {
return !password.equals(password.toUpperCase());
}

private boolean containsDigits(String password) {
return password.chars().anyMatch(Character::isDigit);
}

private boolean containsSpecialChars(String password) {
return password.chars().anyMatch(ch ->
"!@#$%^&*()_+-=[]{}|;:,.<>?".indexOf(ch) >= 0);
}

private boolean isCommonPassword(String password) {
List<String> commonPasswords = Arrays.asList(
"123456", "password", "12345678", "qwerty", "123456789",
"12345", "1234", "111111", "1234567", "dragon"
);
return commonPasswords.contains(password.toLowerCase());
}
}

9.3 防护措施

@Component
public class SecurityProtectionFilter extends OncePerRequestFilter {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

private static final int MAX_ATTEMPTS = 5;
private static final Duration BLOCK_DURATION = Duration.ofMinutes(15);

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

String clientIp = getClientIp(request);
String userAgent = request.getHeader("User-Agent");

// 1. 检查 IP 是否被封禁
if (isIpBlocked(clientIp)) {
handleBlockedIp(response);
return;
}

// 2. 检查暴力破解
if (isBruteForceAttempt(clientIp, userAgent)) {
recordFailedAttempt(clientIp, userAgent);
handleBruteForce(response);
return;
}

// 3. 检查可疑活动
if (isSuspiciousActivity(request)) {
logSuspiciousActivity(request);
}

filterChain.doFilter(request, response);
}

private boolean isIpBlocked(String ip) {
String key = "blocked:ip:" + ip;
return redisTemplate.hasKey(key);
}

private boolean isBruteForceAttempt(String ip, String userAgent) {
String key = String.format("attempts:%s:%s", ip, DigestUtils.md5Hex(userAgent));
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
return attempts != null && attempts >= MAX_ATTEMPTS;
}

private void recordFailedAttempt(String ip, String userAgent) {
String key = String.format("attempts:%s:%s", ip, DigestUtils.md5Hex(userAgent));
redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, BLOCK_DURATION);

// 如果超过阈值,封禁 IP
Integer attempts = (Integer) redisTemplate.opsForValue().get(key);
if (attempts >= MAX_ATTEMPTS) {
String blockKey = "blocked:ip:" + ip;
redisTemplate.opsForValue().set(blockKey, "blocked", BLOCK_DURATION);
}
}

private boolean isSuspiciousActivity(HttpServletRequest request) {
// 检查异常的请求头
String userAgent = request.getHeader("User-Agent");
if (userAgent == null || userAgent.isEmpty()) {
return true;
}

// 检查异常的请求频率
String ip = getClientIp(request);
String rateKey = "rate:" + ip;
Long requests = redisTemplate.opsForValue().increment(rateKey);
if (requests == 1) {
redisTemplate.expire(rateKey, Duration.ofMinutes(1));
}

return requests > 100; // 每分钟超过 100 次请求
}

private String getClientIp(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
return request.getRemoteAddr();
}

private void handleBlockedIp(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("IP 已被封禁,请稍后再试");
}

private void handleBruteForce(HttpServletResponse response) throws IOException {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("检测到异常登录行为,请稍后再试");
}

private void logSuspiciousActivity(HttpServletRequest request) {
String ip = getClientIp(request);
String userAgent = request.getHeader("User-Agent");
String uri = request.getRequestURI();

log.warn("检测到可疑活动 - IP: {}, UserAgent: {}, URI: {}", ip, userAgent, uri);
}
}

10. 常见问题与解决方案

10.1 跨域问题

问题: 前端应用访问后端 API 时出现跨域错误

解决方案:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:3000", "https://app.company.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

10.2 令牌过期处理

问题: JWT 令牌过期导致用户体验中断

解决方案:

@Component
public class TokenRefreshHandler {

@Autowired
private TokenRefreshService tokenRefreshService;

public Mono<Void> handleExpiredToken(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String refreshToken = extractRefreshToken(request);

if (refreshToken != null) {
try {
String newAccessToken = tokenRefreshService.refreshAccessToken(refreshToken);

// 在响应头中返回新的访问令牌
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("New-Access-Token", newAccessToken);

// 重试原始请求
ServerHttpRequest modifiedRequest = request.mutate()
.header("Authorization", "Bearer " + newAccessToken)
.build();

return chain.filter(exchange.mutate().request(modifiedRequest).build());

} catch (Exception e) {
// 刷新令牌也过期,返回 401
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}

exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}

10.3 权限配置问题

问题: @PreAuthorize 注解不生效

解决方案:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {

@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(new CustomPermissionEvaluator());
return handler;
}
}

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null || !authentication.isAuthenticated()) {
return false;
}

String permissionName = permission.toString();
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();

// 实现自定义权限逻辑
return hasUserPermission(userDetails.getUserId(), targetDomainObject, permissionName);
}

@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
// 处理基于 ID 的权限检查
return false;
}

private boolean hasUserPermission(Long userId, Object target, String permission) {
// 实现具体的权限检查逻辑
return true;
}
}

11. 总结

11.1 核心要点回顾

  1. OAuth2 是标准的授权框架,提供了多种授权模式适应不同场景
  2. JWT 是无状态的令牌,适合微服务架构的分布式环境
  3. Spring Security 提供了完整的安全解决方案,包括认证和授权
  4. 微服务架构需要统一的安全策略,网关认证和服务间认证都很重要
  5. 安全是一个持续的过程,需要不断更新和改进防护措施

11.2 面试重点

  • 理解 OAuth2 的四种授权模式和适用场景
  • 掌握 JWT 的工作原理和优缺点
  • 熟悉 Spring Security 的核心组件和工作流程
  • 能够设计微服务架构下的安全方案
  • 了解常见的安全漏洞和防护措施

11.3 最佳实践

  1. 使用强密码策略,定期更新密码
  2. 实施多层防护,包括网络层、应用层、数据层
  3. 定期安全审计,及时发现和修复漏洞
  4. 监控安全事件,快速响应安全威胁
  5. 遵循最小权限原则,避免过度授权

📚 参考资源


本文档持续更新中,欢迎提出宝贵建议和意见!