You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Cloud Gateway 组合安全配置方案咨询(X.509证书 + HTTP请求头)

针对你这个Spring Cloud Gateway(WebFlux)下的安全配置需求,我来给你梳理一套适配的方案,刚好能满足证书有效性验证+请求头构建Principal的核心需求,同时兼容匿名访问的场景:

第一步:先搞定SSL层面的客户端证书验证

首先要让网关能接受并验证门户发来的客户端证书,我们需要在application.yml里配置SSL相关参数,重点是设置client-auth: WANT——这个参数的意思是:允许无证书的请求进来,但如果有证书,必须验证为可信证书(否则直接拒绝)。示例配置如下:

server:
  ssl:
    enabled: true
    # 网关自身的证书配置(如果门户需要验证网关身份的话)
    key-store: classpath:gateway-keystore.jks
    key-store-password: your-keystore-password
    key-password: your-key-password
    # 信任库:存放用来验证门户客户端证书的CA证书
    trust-store: classpath:gateway-truststore.jks
    trust-store-password: your-truststore-password
    client-auth: WANT  # 关键:允许无证书请求,有证书则必须验证有效

注意:你的信任库必须包含颁发门户客户端证书的CA根证书,这样网关才能确认证书的合法性。

第二步:自定义Spring WebFlux Security逻辑

因为是响应式架构,我们要用SecurityWebFilterChain而非传统的HttpSecurity。核心逻辑分两步:

  1. 验证客户端证书的有效性(如果存在)
  2. 证书有效时,提取请求头中的真实用户名,从数据库加载用户信息构建Principal;用户不存在则仍视为匿名

1. 自定义ReactiveUserDetailsService

先写一个服务类,用来从数据库加载用户信息,这里要实现ReactiveUserDetailsService(响应式版本的用户详情服务):

@Service
public class CustomReactiveUserDetailsService implements ReactiveUserDetailsService {

    private final UserRepository userRepository; // 替换成你实际的用户DAO/Repository

    public CustomReactiveUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public Mono<UserDetails> findByUsername(String username) {
        // 从数据库查询用户,转换为Spring Security的UserDetails对象
        return userRepository.findByUsername(username)
                .map(user -> User.withUsername(user.getUsername())
                        .password("{noop}dummy") // 因为证书已经验证过,这里密码可以设为noop(不需要验证密码)
                        .authorities(user.getRoles().stream()
                                .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                                .collect(Collectors.toList()))
                        .build())
                .switchIfEmpty(Mono.empty()); // 用户不存在时返回空,后续处理为匿名
    }
}

2. 配置SecurityWebFilterChain

接下来是核心的安全过滤链配置,我们要添加两个过滤器:

  • 第一个过滤器:验证客户端证书,生成临时认证标记
  • 第二个过滤器:基于临时标记,提取请求头用户名并构建真实Principal
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    private final CustomReactiveUserDetailsService userDetailsService;

    public SecurityConfig(CustomReactiveUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        // 过滤器1:验证客户端证书有效性
        X509AuthenticationWebFilter x509Filter = new X509AuthenticationWebFilter();
        x509Filter.setAuthenticationConverter(exchange -> {
            // 从请求SSL信息中获取客户端证书
            return exchange.getRequest().getSslInfo()
                    .map(SslInfo::getPeerCertificates)
                    .filter(certs -> certs.length > 0)
                    .flatMap(certs -> {
                        // SSL层面已经验证了证书的可信度,这里可以加额外校验(比如检查证书CN/OU)
                        X509Certificate cert = certs[0];
                        // 示例:校验证书的CN是否符合预期
                        String certCn = cert.getSubjectX500Principal().getName().split("CN=")[1].split(",")[0];
                        if (!"authorized-portal".equals(certCn)) {
                            return Mono.empty(); // 证书不符合要求,视为无效
                        }
                        // 生成临时认证,标记证书已验证通过
                        return Mono.just(new UsernamePasswordAuthenticationToken("CERT_VALIDATED", null, Collections.emptyList()));
                    })
                    .switchIfEmpty(Mono.empty()); // 无证书,返回空
        });

        // 过滤器2:基于证书验证标记,从请求头提取真实用户
        AuthenticationWebFilter headerAuthFilter = new AuthenticationWebFilter(
                new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService)
        );
        headerAuthFilter.setServerAuthenticationConverter(exchange -> {
            // 先检查是否已经通过证书验证
            return exchange.getPrincipal()
                    .filter(auth -> "CERT_VALIDATED".equals(auth.getName()))
                    .flatMap(auth -> {
                        // 从请求头获取真实用户名(和门户约定好的头,比如X-REAL-USER)
                        String realUsername = exchange.getRequest().getHeaders().getFirst("X-REAL-USER");
                        if (realUsername == null || realUsername.isBlank()) {
                            return Mono.empty();
                        }
                        // 转换为认证请求,交给UserDetailsService处理
                        return Mono.just(new UsernamePasswordAuthenticationToken(realUsername, null));
                    })
                    .switchIfEmpty(Mono.empty()); // 未通过证书验证或无请求头用户,返回空
        });

        return http
                .authorizeExchange(exchanges -> exchanges
                        // 配置允许匿名访问的路由
                        .pathMatchers("/public/**", "/health").permitAll()
                        // 其他路由可以根据需求配置,比如需要认证用户才能访问:.anyExchange().authenticated()
                        // 如果允许匿名用户访问所有路由,但认证用户有额外权限,就用.anyExchange().permitAll()
                        .anyExchange().permitAll()
                )
                // 添加两个过滤器,顺序很重要:先验证证书,再处理请求头用户
                .addFilterAt(x509Filter, SecurityWebFiltersOrder.AUTHENTICATION)
                .addFilterAfter(headerAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .anonymous() // 启用匿名认证,无有效认证时自动生成AnonymousAuthenticationToken
                .and()
                .csrf().disable() // 网关一般不需要CSRF保护
                .build();
    }
}

关键逻辑说明

  • 证书验证:通过X509AuthenticationWebFilter检查请求中的客户端证书,SSL层面已经验证证书是否在信任库中,你还可以额外添加证书属性校验(比如CN、有效期等)
  • 请求头用户处理:只有证书验证通过的请求,才会进入请求头用户提取逻辑;如果数据库中没有该用户,ReactiveUserDetailsService返回空,最终用户仍为匿名
  • 匿名访问:无证书、证书无效、请求头用户不存在这三种情况,都会被视为匿名用户,可访问你配置的permitAll路由

额外注意事项

  • 确保门户不会篡改X-REAL-USER请求头,因为网关信任这个头的真实性(毕竟网关在门户后方,门户要负责这个头的合法性)
  • 如果不需要额外的证书属性校验,可以去掉X509过滤器中的CN校验逻辑
  • 匿名用户的Authentication类型是AnonymousAuthenticationToken,你可以在后续的过滤器或服务中通过这个类型判断用户是否匿名

内容的提问来源于stack exchange,提问作者Yaneek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 13:57:32