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。核心逻辑分两步:
- 验证客户端证书的有效性(如果存在)
- 证书有效时,提取请求头中的真实用户名,从数据库加载用户信息构建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
相关产品推荐
相关产品推荐

