这两天一直在研究这个话题,踩了几个坑,把遇到的东西整理成文,供有需要的朋友参考。
👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的麻烦。 🎯 本文将围绕Spring Security这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
- Spring Security - 微服务架构下的统一认证方案 🌐🔐
- 🔍 为什么要统一认证?
- 🧱 技术选型概览
- 🛠️ 搭建统一认证服务(Authorization Server)
- 1. 创建 Maven 工程并添加依赖
- 2. 配置用户存储与客户端详情
- 自定义用户服务类
- 配置 ClientDetails
Spring Security - 微服务架构下的统一认证方案 🌐🔐
在当今的软件开发世界中,微服务架构已经成为构建大型、可扩展和高可用系统的首选模式。随着业务模块的拆分和服务数量的增长,如何在多个服务之间达成安全、可靠且一致的身份认证与权限控制,成为系统设计中的关键挑战之一。
传统的单体应用通常将用户认证逻辑集中在一个模块内,比如通过 Spring Security 提供的表单登录、记住我、CSRF 防护等功能即可满足需求。但在微服务环境中,每个服务独立部署、独立运行,若每个服务都自行处理认证流程,不仅会造成代码重复,还会带来 Token 校验不一致、会话状态难以同步等麻烦。
为此,大家要一个统一认证中心(Unified Authentication Center),来集中管理用户的登录、鉴权、Token 发放与校验等操作。本文将深入探讨如何使用 Spring Security + OAuth2 + JWT + Spring Cloud Gateway 构建一套适用于微服务架构的统一认证方案,并结合实际 Java 代码示例进行说明,帮助你快速搭建安全可靠的分布式身份验证体系。💪
🔍 为什么要统一认证?
在微服务架构中,常用的服务包括:
- 用户服务(User Service)
- 订单服务(Order Service)
- 商品服务(Product Service)
- 支付服务(Payment Service)
- 通知服务(Notification Service)
1. 重复编码:每个服务都要写一遍认证逻辑。
2. Token 不统一:不同服务签发的 Token 签名方式、过期时间可能不同。
3. 权限混乱:角色和权限分散在各个服务中,难以统一管理。
4. 用户体验差:用户需要在多个服务间反复登录。
而统一认证的核心思想是:所有服务都不直接处理登录请求,而是将认证过程交给专门的认证服务器(Authorization Server),其他服务只负责资源保护(Resource Server)。
这正是 OAuth 2.0 协议所解决的问题 —— 它定义了四种授权模式,其中最适用于前后端分离+微服务的是 “密码模式”(Password Grant) 和更推荐使用的 “授权码模式 + PKCE”。
⚠️ 注意:虽然密码模式简单易懂,但根据 OAuth 2.1 的最新建议,已不再推荐直接使用密码模式,尤其是在公共客户端中。大家将在后续章节介绍更安全的替代方案。
🧱 技术选型概览
大家将采用如下技术栈构建统一认证系统:
组件作用Spring Boot快速构建独立运行的服务Spring Security提供认证与授权核心能力Spring Authorization Server达成 OAuth2 授权服务器(取代旧的 Spring Security OAuth)JWT (JSON Web Token)无状态 Token,用于跨服务传递用户信息Spring Cloud GatewayAPI 网关,统一路由与全局过滤器Redis(可选)存储黑名单 Token 或缓存用户信息
整个系统的架构可以用下面的 Mermaid 图表示:
Add Auth Header
Add Auth Header
Add Auth Header
Client Browser/App
Sprint Cloud Gateway
Route To?
Auth Service - /oauth2/token
User Service
Order Service
Product Service
Database
User Credentials
如图所示,所有的外部请求首先经过 API 网关,网关负责路由转发,并可在必要时注入认证头或执行权限检查。真正的认证逻辑由专用的 Auth Service 处理,其他服务作为资源服务器,仅验证 JWT Token 的合法性并提取用户上下文。
🛠️ 搭建统一认证服务(Authorization Server)
我们先从最核心的部分开始:搭建一个基于 Spring Authorization Server 的 OAuth2 认证中心。
1. 创建 Maven 工程并添加依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
org.springframework.security
spring-security-oauth2-authorization-server
org.springframework.boot
spring-boot-starter-data-jpa
com.h2database
h2
runtime
💡 提示:你可以将数据库替换为 MySQL 或 PostgreSQL,这里为了演示方便使用 H2 内存数据库。
2. 配置用户存储与客户端详情
我们需要配置两类信息:
- Client Registration:第三方应用(如前端 SPA、移动端)注册的信息,包含 client_id、client_secret、授权类型等。
- User Details:系统用户的账号密码信息。
自定义用户服务类
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里应从数据库查询用户
if ("admin".equals(username)) {
return User.withUsername("admin")
.password("{noop}123456") // 使用 {noop} 表示明文密码,生产环境请用 BCrypt
.authorities("ROLE_ADMIN", "SCOPE_read", "SCOPE_write")
.build();
}
throw new UsernameNotFoundException("User not found: " + username);
}
}
🔒 生产环境中务必使用加密密码,比如:
>> .password("$2a$10$dXJjOY9n7VZkQ8zZyZzZz.eLqKzZzZzZzZzZzZzZzZzZzZzZzZzZz") // BCrypt 加密后的 123456 >
配置 ClientDetails
@Configuration
public class AuthorizationServerConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient registeredClient = RegisteredClient.withId("web-client")
.clientId("web-client-id")
.clientSecret("{noop}web-client-secret") // 同样,生产环境需加密
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.PASSWORD) // 支持密码模式
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/login/oauth2/code/web-client") // 若使用授权码模式
.scope("read")
.scope("write")
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(2))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
@Bean
public JWKSource jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID("rsa-key-id")
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPairGenerator keyPairGenerator;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder()
.issuer("https://auth.example.com") // 可自定义 issuer
.build();
}
}
3. 启用 Authorization Server
创建主配置类并启用授权服务器功能:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.exceptionHandling(exceptions ->
exceptions.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login")));
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance(); // 测试用,生产请用 BCryptPasswordEncoder
}
}
4. 启动项目并测试获取 Token
启动服务后,发送 POST 请求到 /oauth2/token 获取 JWT:
curl -X POST http://localhost:8080/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "web-client-id:web-client-secret" \
-d "grant_type=password&username=admin&password=123456&scope=read"
成功响应如下:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx",
"token_type": "Bearer",
"expires_in": 7200,
"scope": "read",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.yyyyy"
}
你现在已经拥有了一枚有效的 JWT Token!🎉 下一步是在资源服务中解析它。
🛡️ 资源服务中的 JWT 验证(Resource Server)
现在我们来创建一个简单的资源服务,比如 用户信息服务,它只允许携带有效 JWT 的请求访问。
1. 创建 Resource Service 项目
同样使用 Spring Boot,引入以下依赖:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-oauth2-resource-server
org.springframework.boot
spring-boot-starter-security
2. 配置 JWT 解析
在 application.yml 中指定认证服务器的 JWKS 地址(用于验证签名):
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://auth.example.com # 必须与 Authorization Server 的 issuer 一致
3. 编写受保护的接口
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/me")
@PreAuthorize("hasAuthority('SCOPE_read')")
public Map getCurrentUser(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
Map user = new HashMap();
user.put("name", principal.getAttribute("username"));
user.put("roles", principal.getAuthorities());
user.put("uid", principal.getName());
return user;
}
}
4. 配置安全规则
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
);
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
// 将 JWT 中的 authorities 提取出来
private Converter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
converter.setAuthoritiesClaimName("authorities");
converter.setAuthorityPrefix("");
return new JwtAuthenticationConverter() {
@Override
protected Collection extractAuthorities(Jwt jwt) {
Collection authorities = super.extractAuthorities(jwt);
List scopes = jwt.getClaim("scope");
if (scopes != null) {
for (String scope : scopes) {
authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope));
}
}
return authorities;
}
};
}
}
此时,当你用上一步获取的 Token 发起请求:
curl http://localhost:8081/api/users/me \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx"
你会得到当前用户信息,说明 JWT 已被正确解析 ✅
🌉 使用 Spring Cloud Gateway 统一入口
在微服务架构中,直接暴露多个服务的端口是不安全也不优雅的做法。我们应当通过一个统一的 API 网关对外提供服务。
1. 创建 Gateway 项目
添加依赖:
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.boot
spring-boot-starter-oauth2-client
org.springframework.boot
spring-boot-starter-webflux
2. 配置路由规则
spring:
cloud:
gateway:
routes:
- id: auth-service
uri: http://localhost:8080
predicates:
- Path=/oauth2/**
- id: user-service
uri: http://localhost:8081
predicates:
- Path=/api/users/**
filters:
- TokenRelay=
- id: order-service
uri: http://localhost:8082
predicates:
- Path=/api/orders/**
filters:
- TokenRelay=
security:
oauth2:
client:
registration:
web-client:
provider: custom-provider
client-id: web-client-id
client-secret: web-client-secret
scope: read,write
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
custom-provider:
issuer-uri: https://auth.example.com
📌 TokenRelay= 是 Spring Cloud Security 提供的一个 Gateway Filter,它会自动将当前会话中的 access token 添加到向下游服务转发的请求头中。
3. 全局认证过滤器(可选增强)
你可以编写自定义全局过滤器,在请求进入时统一校验 Token 是否存在:
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final ReactiveOAuth2AuthorizedClientService authorizedClientService;
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 白名单路径放行
if (path.startsWith("/oauth2") || path.startsWith("/login")) {
return chain.filter(exchange);
}
// 检查是否有 Authorization Header
List authHeaders = request.getHeaders().getOrEmpty("Authorization");
if (authHeaders.isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String token = authHeaders.get(0).substring(7); // Bearer 后的内容
if (!isValidJwt(token)) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
private boolean isValidJwt(String token) {
try {
// 可调用远程 JWKS 接口验证签名,或本地缓存公钥
// 这里简化处理
return token.length() > 10;
} catch (Exception e) {
return false;
}
}
@Override
public int getOrder() {
return -1; // 优先于其他过滤器执行
}
}
这样,所有非认证相关的请求都必须携带合法 Token 才能通过网关 🛡️
🔁 更安全的授权模式:Authorization Code with PKCE
前面我们使用了 Password Grant 模式,虽然便于理解,但它要求客户端直接收集用户名和密码,存在安全隐患。
现代最佳实践推荐使用 Authorization Code Flow with PKCE(Proof Key for Code Exchange),尤其适合 SPA 和移动应用。
PKCE 原理简述 🔗
1. 客户端生成一个 code_verifier(随机字符串)
2. 对其进行哈希得到 code_challenge
3. 请求授权时带上 code_challenge
4. 获取 Token 时提交原始 code_verifier
5. 认证服务器重新计算哈希并与之前保存的比对
这种方式即使 Authorization Code 被截获,也无法换取 Token,因为攻击者不知道 code_verifier。
在 Spring Authorization Server 中启用授权码模式
修改之前的 RegisteredClient 配置:
RegisteredClient registeredClient = RegisteredClient.withId("spa-client")
.clientId("spa-client-id")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE) // 公共客户端无需 secret
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:4200/callback")
.scope("read")
.clientSettings(ClientSettings.builder()
.requireProofKey(true) // 开启 PKCE
.build())
.build();
前端可通过类似 Okta 的 PKCE 示例 的方式实现完整流程。
🔄 刷新 Token 机制
Access Token 通常较短有效期(如 2 小时),Refresh Token 较长(如 7 天)。当 Access Token 过期后,客户端可用 Refresh Token 申请新的 Token,而无需用户重新登录。
获取新 Token
curl -X POST http://localhost:8080/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "web-client-id:web-client-secret" \
-d "grant_type=refresh_token&refresh_token=yyyyy"
返回新的 access_token 和 refresh_token。
⚠️ 安全建议:
Refresh Token 应绑定客户端 IP 或设备指纹每次使用后应滚动更新(即旧的失效,返回新的)提供撤销 Token 的接口(如 /logout)
🧩 权限精细化控制:基于角色与 Scope
除了基本的认证,我们还需要做细粒度的权限控制。
使用 Method-Level Security
Spring Security 支持在办法级别进行权限判断:
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.username")
public Order getOrder(String userId, String orderId) {
// 只有管理员或本人可以查看订单
}
@PreAuthorize("hasAuthority('SCOPE_write')")
@PostMapping("/orders")
public Order createOrder(@RequestBody Order order) {
// 仅拥有 write scope 的用户可创建订单
}
}
确保在配置类上启用 @EnableMethodSecurity。
动态权限决策器(Custom Access Decision)
对于复杂业务逻辑,可实现 AccessDecisionManager 或使用 @PostFilter / @PreFilter 进行数据级过滤:
@GetMapping("/orders")
@PreFilter("filterObject.owner == authentication.principal.username")
public List getAllOrders(List orders) {
return orders;
}
🧯 安全防护常用措施
1. CSRF 防护(仅适用于 Cookie 登录)
如果你仍使用 Session 登录(不推荐于微服务),需开启 CSRF 防护:
http.csrf(c -> c.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
但对于纯 JWT 方案,由于不依赖 Cookie,CSRF 并非核心威胁。
2. CORS 配置
避免跨域问题:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(Arrays.asList("*"));
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.addExposedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
3. 登录失败锁定机制
防止暴力破解:
// 可集成 Redis 实现登录失败次数统计
public class LoginFailureHandler {
private final RedisTemplate redisTemplate;
public void recordFailure(String username) {
String key = "login:fail:" + username;
Integer count = redisTemplate.opsForValue().get(key);
if (count == null) {
redisTemplate.opsForValue().set(key, 1, Duration.ofMinutes(15));
} else if (count >= 5) {
throw new LockedException("Account temporarily locked");
} else {
redisTemplate.opsForValue().increment(key);
}
}
}
📊 分布式会话 vs 无状态 JWT
特性分布式会话(Session + Redis)无状态 JWT可靠登出✅ 容易实现(删除 Session)❌ 需维护黑名单扩展性⚠️ 依赖 Redis 性能✅ 完全无状态跨域支持⚠️ 需处理 Cookie✅ 通过 Header 传输数据大小小(仅 ID)大(含全部信息)适用场景内部管理系统微服务、API 平台
推荐:微服务架构下优先选择 JWT,并通过网关统一处理 Token 黑名单(如 Redis Bloom Filter 优化性能)。
🔄 注销与 Token 黑名单管理
JWT 是无状态的,无法像 Session 一样直接销毁。但我们可以通过以下方式实现“伪注销”:
方案一:Redis 黑名单(简单有效)
@Service
public class TokenBlacklistService {
private final StringRedisTemplate redisTemplate;
public void blacklistToken(String jwt, long expirationSeconds) {
String tokenId = parseTokenId(jwt); // 从 JWT payload 中提取 jti
redisTemplate.opsForValue().set("blacklist:" + tokenId, "true", expirationSeconds, TimeUnit.SECONDS);
}
public boolean isTokenBlacklisted(String tokenId) {
return Boolean.TRUE.equals(redisTemplate.hasKey("blacklist:" + tokenId));
}
}
并在资源服务器中加入拦截器:
@Component
public class JwtBlacklistFilter implements Filter {
@Autowired
private TokenBlacklistService blacklistService;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String authHeader = req.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
String tokenId = extractJti(token);
if (blacklistService.isTokenBlacklisted(tokenId)) {
((HttpServletResponse) response).sendError(401, "Token revoked");
return;
}
}
chain.doFilter(request, response);
}
}
方案二:短期 Token + 强制刷新
设置较短的过期时间(如 15 分钟),前端定期用 Refresh Token 获取新 Token。一旦用户注销,后端使 Refresh Token 失效即可。
🧪 测试你的认证系统
良好的单元测试是保障安全的基础。
测试控制器权限
@WebMvcTest(UserController.class)
@WithMockUser(roles = "USER")
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnUserWhenAuthenticated() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("user"));
}
}
测试 JWT 解析
@Test
void shouldParseJwtAndExtractAuthorities() {
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("scope", Arrays.asList("read", "write"))
.build();
var converter = new CustomJwtAuthenticationConverter();
var auth = converter.convert(jwt);
assertTrue(auth.getAuthorities().contains(new SimpleGrantedAuthority("SCOPE_read")));
}
🌐 外部参考资源
- OAuth 2.1 规范草案 — 最新的 OAuth 协议演进方向
- OWASP Top 10 API Security Risks — 了解常见的 API 安全漏洞
- Spring Security Official Documentation — 权威指南
- JWT.io Debugger — 在线解析和调试 JWT Token
🧭 总结与最佳实践 ✅
我们已经搞定了一个完整的微服务统一认证方案的设计与实现。以下是关键要点回顾:
1. 统一认证中心 是微服务安全的基石,避免重复建设。
2. OAuth2 + JWT 是主流组合,适合无状态、分布式的场景。
3. Spring Authorization Server 是新一代标准,取代旧版 Spring Security OAuth。
4. API 网关 应承担路由、鉴权转发、日志记录等职责。
5. 优先使用 Authorization Code + PKCE,避免密码模式。
6. 合理设置 Token 过期时间,平衡安全性与用户体验。
7. 实现 Token 注销机制,如黑名单或短期 Token 策略。
8. 加强监控与审计,记录登录行为、异常访问等。
🌟 最终目标:让用户一次登录,畅享所有服务;让黑客寸步难行,系统坚如磐石!
通过本文的学,你应该已经掌握了如何在 Spring 生态中构建一个现代化、可扩展、高安全性的统一认证体系。下一步,可以尝试将其集成到真实的微服务项目中,并结合 Nacos/Eureka 做服务发现,搭配 Sleuth 做链路追踪,打造真正的企业级云原生架构。
Keep coding, stay secure! 💻🔐🚀
🙌
本次分享就到这里。技术这东西越研究越有意思,后续有新的收获我也会继续更新。
评论 (0)
暂无评论