说说Spring Security- 微服务架构下的统一认证方案

这两天一直在研究这个话题,踩了几个坑,把遇到的东西整理成文,供有需要的朋友参考。

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的麻烦。 🎯 本文将围绕Spring Security这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!

文章目录

3. 启用 Authorization Server4. 启动项目并测试获取 Token 🛡️ 资源服务中的 JWT 验证(Resource Server) 🌉 使用 Spring Cloud Gateway 统一入口 🔁 更安全的授权模式:Authorization Code with PKCE 🔄 刷新 Token 机制 🧩 权限精细化控制:基于角色与 Scope 🧯 安全防护常用措施 📊 分布式会话 vs 无状态 JWT🔄 注销与 Token 黑名单管理 🧪 测试你的认证系统 🌐 外部参考资源🧭 总结与最佳实践 ✅

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_tokenrefresh_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")));
}


🌐 外部参考资源


🧭 总结与最佳实践 ✅

我们已经搞定了一个完整的微服务统一认证方案的设计与实现。以下是关键要点回顾:

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)

暂无评论