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) 是一个开放标准,用于在各方之间安全地传输信息。
工作原理:
- 用户登录成功后,服务器生成包含用户信息的 JWT
- 服务器将 JWT 返回给客户端
- 客户端在后续请求中携带 JWT
- 服务器验证 JWT 的有效性
优点:
- 无状态,易于扩展
- 支持跨域
- 包含用户信息,减少数据库查询
- 自包含,验证简单
缺点:
- 无法撤销已签发的令牌
- 令牌大小较大
- 需要确保传输安全
Q4: Spring Security 的认证流程是怎样的?
A: Spring Security 的认证流程包括:
- 用户提交请求:携带认证信息
- 过滤器拦截:SecurityContextPersistenceFilter 等
- 认证管理器:AuthenticationManager 处理认证
- 认证提供者:AuthenticationProvider 具体验证逻辑
- 用户详情服务:UserDetailsService 加载用户信息
- 创建认证对象:Authentication 对象
- 存储安全上下文:SecurityContextHolder
- 授权检查:基于角色和权限的访问控制
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 核心要点回顾
- OAuth2 是标准的授权框架,提供了多种授权模式适应不同场景
- JWT 是无状态的令牌,适合微服务架构的分布式环境
- Spring Security 提供了完整的安全解决方案,包括认证和授权
- 微服务架构需要统一的安全策略,网关认证和服务间认证都很重要
- 安全是一个持续的过程,需要不断更新和改进防护措施
11.2 面试重点
- 理解 OAuth2 的四种授权模式和适用场景
- 掌握 JWT 的工作原理和优缺点
- 熟悉 Spring Security 的核心组件和工作流程
- 能够设计微服务架构下的安全方案
- 了解常见的安全漏洞和防护措施
11.3 最佳实践
- 使用强密码策略,定期更新密码
- 实施多层防护,包括网络层、应用层、数据层
- 定期安全审计,及时发现和修复漏洞
- 监控安全事件,快速响应安全威胁
- 遵循最小权限原则,避免过度授权
📚 参考资源
本文档持续更新中,欢迎提出宝贵建议和意见!