微服务架构下Spring Security会话验证权限拒绝异常咨询
嘿,这个问题我之前帮不少开发者踩过坑——网关层明明做了授权,下游服务还是抛出org.springframework.security.access.AccessDeniedException: Access is denied,核心问题大多是下游服务没正确识别网关传递的已认证会话/身份信息,或者自己额外触发了不必要的权限校验。下面给你一步步拆解解决方案,结合会话机制来搞定:
第一步:让网关正确传递会话/认证信息到下游
网关已经完成了认证授权,现在要把已认证主体的关键信息(用户ID、权限、认证状态)安全地传递给下游服务,这里有两种贴合会话机制的常用方式:
方式1:用Spring Session实现跨服务会话共享
如果网关和下游服务使用同一种会话存储(比如Redis),可以通过Spring Session让它们共享会话数据,这样下游服务能直接读取网关写入的认证信息:
- 网关+所有下游服务都引入Spring Session依赖(以Redis为例):
<!-- Maven依赖 --> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> - 统一配置会话存储(application.yaml):
spring: session: store-type: redis redis: host: your-redis-host port: your-redis-port password: your-redis-password # 如果有密码的话 - 网关认证通过后,Spring Security会自动把
Authentication对象存入共享会话,下游服务只要配置Spring Security信任这个会话,就能直接获取已认证主体,无需重复认证。
方式2:网关将认证信息封装到请求头传递(轻量化方案)
如果不想共享会话,网关可以在认证通过后,把已认证用户的核心信息放到请求头里,下游服务从请求头提取并构建本地认证上下文:
网关侧自定义GatewayFilter实现:
@Component public class AuthPropagationFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 从网关的SecurityContext获取已认证主体 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated()) { // 将用户ID、权限列表写入请求头 exchange.getRequest().mutate() .header("X-Auth-User-ID", auth.getName()) .header("X-Auth-Authorities", auth.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(","))) .build(); } return chain.filter(exchange); } }
第二步:下游服务配置Spring Security信任网关的认证结果
下游服务默认会自己触发认证校验逻辑,所以必须修改配置,让它信任网关传递的认证信息,避免重复拦截:
针对会话共享场景的配置
下游服务的Security配置类要关闭不必要的认证过滤器,同时允许从共享会话读取认证信息:
@Configuration @EnableWebSecurity public class DownstreamSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth // 可以限制仅允许网关IP的请求,更安全 .anyRequest().authenticated() ) .sessionManagement(session -> session // 复用共享会话,不需要下游自己创建新会话 .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ) // 关闭网关已经做过的认证方式(比如表单登录、Basic认证) .formLogin().disable() .httpBasic().disable(); return http.build(); } }
针对请求头传递场景的配置
下游服务需要自定义过滤器,从请求头提取信息并构建Authentication对象,存入本地SecurityContext:
@Component public class AuthContextInitializerFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String userId = request.getHeader("X-Auth-User-ID"); String authoritiesStr = request.getHeader("X-Auth-Authorities"); if (userId != null && authoritiesStr != null) { // 从请求头解析权限列表 List<GrantedAuthority> authorities = Arrays.stream(authoritiesStr.split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // 构建已认证的Authentication对象 Authentication auth = new UsernamePasswordAuthenticationToken(userId, null, authorities); // 存入SecurityContext,让后续的权限校验能识别 SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } }
然后在Security配置中注册这个过滤器,放到Spring Security的默认认证过滤器之前:
@Configuration @EnableWebSecurity public class DownstreamSecurityConfig { @Autowired private AuthContextInitializerFilter authContextFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .addFilterBefore(authContextFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) // 下游服务可以设置为无状态,因为认证信息来自请求头 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .formLogin().disable() .httpBasic().disable(); return http.build(); } }
第三步:排查常见坑点
- 确保网关和下游服务的Spring Security、Spring Session版本一致,避免会话序列化/反序列化失败
- 如果用Redis共享会话,检查序列化配置,推荐用
Jackson2JsonRedisSerializer来序列化Authentication对象,避免默认JDK序列化的兼容性问题 - 下游服务如果用了
@PreAuthorize等细粒度权限注解,要确保权限字符串和网关传递的完全一致(比如网关传的是ROLE_ADMIN,注解里就不能写ADMIN) - 多线程场景下(比如下游服务用异步方法),要设置SecurityContextHolder的策略为
MODE_INHERITABLETHREADLOCAL,避免线程切换后丢失认证信息:@PostConstruct public void initSecurityContextStrategy() { SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); }
内容的提问来源于stack exchange,提问作者ConsultingEasy
相关产品推荐
相关产品推荐

