SpringCloud实战 Gateway网关整合SpringSecurity 自定义过滤器执行多次原因及解决方案

前言

最近为了学习微服务项目的开发,我自己开发了一个分布式博客项目。

在项目的API网关中,我添加一个过滤器并将其配置到了SpringSecurity的过滤器链中,该过滤器尝试从请求头中获取并解析token,封装用户认证凭证。

最近在测试时,我偶然发现该过滤器在每次请求时都会调用两次,后来经过排查及查资料,发现是因为该过滤器被封装到了过滤器链中两次。下面分享下这个问题及解决方案,希望可以帮到有需要的人,谢谢。

bug复现

如下图,我在过滤器中添加一些打印之后,发现每个请求都走了两次该过滤器。垃圾代码大佬轻喷

不难看出 如果能够拿到token,会通过openfeign调用用户服务。这个问题还是比较严重的,因为这会给用户服务带来压力,毕竟第二次调用是完全没有必要的。

分析

因为gateway默认采用webflux,而非webmvc。所以该过滤器实现了WebFilter接口,该过滤器之所以被执行两次,是因为它被封装到了webFilter链中的两个位置。

webflux原生过滤器链(webFilters)

在SpringBoot关于webflux的自动配置类HttpHandlerAutoConfiguration中,关于HttpHandle实例的创建,是先通过WebHttpHandlerBuilder的applicationContext方法创建了一个WebHttpHandlerBuilder实例,然后再调用了其build方法进行创建。

在applicationContext方法中,就涉及到了WebFilter的收集。

可以看出这里是从容器中获取了所有WebFilter类型的bean,并将其收集到webflux的过滤器链中。

因为项目中可能以后会存在多个过滤器,所以自定义的过滤器最好还是交由容器管理,也就是添加@Component注解。那这个地方就不可避免的会被收集到webflux的过滤器链中。

SpringSecurity过滤器链(SecurityWebFilterChain)

SpringSecurity的功能实现主要是基于一系列的过滤器,在SpringSecurity的配置类中,需要向容器提供一个SecurityWebFilterChain实例,即SpringSecurity的过滤器链。构建这个对象时,一般需要进行一些自定义的过滤器的新增或替换。

这里不再赘述,如有需要,可参考本人SpringSecurity相关文章。

解决方案

webmvc

相关的解决方案 其实在webmvc中已经有了,就是这个过滤器
org.springframework.web.filter.OncePerRequestFilter

在webmvc中,如果想要保证一个过滤器只执行一次,可以继承这个过滤器,它的实现原理,其实就是为当前过滤器设置了一个执行标记。

如果没有这个标记,则会执行doFilterInternal方法,执行子类过滤器的逻辑。并且在执行结束后,还将这个标记移除了。这里注意,移除时,整个过滤器链已经执行完了,所以没有影响。

webflux

我就参考OncePerRequestFilter写了一个过滤器,思路也是通过设置标记来过滤,代码如下:

java 复制代码
/**
 * 参考 webmvc 中的 org.springframework.web.filter.OncePerRequestFilter
 * 在 ServerWebExchange 中设置已执行标记,防止过滤器被执行多次
 */
public abstract class AbstractGatewayOncePerRequestFilter implements WebFilter {

    public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {

        //拼接过滤器执行过的标记名称
        String alreadyFilteredAttributeName = getClass().getName() + ALREADY_FILTERED_SUFFIX;

        //判断当前过滤器是否已执行过
        if (exchange.getAttribute(alreadyFilteredAttributeName) == null) {

            //当前过滤器未执行过 设置标记并执行
            exchange.getAttributes().put(alreadyFilteredAttributeName, true);

            return doFilter(exchange, chain)
                    .doOnSuccess(v -> exchange.getAttributes().remove(alreadyFilteredAttributeName));
        }

        //当前过滤器已执行过 跳过
        return chain.filter(exchange);
    }

    abstract Mono<Void> doFilter(ServerWebExchange exchange, WebFilterChain chain);
}

原本的AuthenticationTokenFilter继承AbstractGatewayOncePerRequestFilter,重写doFilter方法,具体逻辑写在这里即可。

java 复制代码
/**
 * token过滤器:解析请求头中的token 并放到上下文中 方便后面对用户登录状态进行判断
 */
@Component
public class AuthenticationTokenFilter extends AbstractGatewayOncePerRequestFilter {

    @Autowired
    private UserService userService;

    @Override
    public Mono<Void> doFilter(ServerWebExchange exchange, WebFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Token");

        if (StrUtil.isNotBlank(token)) {
            String username = AuthUtil.getLoginUsername(token);
            Result<AuthUserDto> result = userService.findByUsername(username);
            if ("0".equals(result.getCode())) {
                AuthUserDto dto = result.getData();
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword(), AuthorityUtils.NO_AUTHORITIES);
                return chain.filter(exchange).subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));
            }
        }
        return chain.filter(exchange);
    }
}

源码

如有需要 可直接参考下面项目代码:

如有帮助 欢迎star~ 谢谢

相关推荐
利威尔xu9 小时前
Go IM 源码实战(14):本地缓存快 3000 倍,失效却难 10 倍
后端·缓存·微服务·面试·golang
逃逸线LOF10 小时前
Ribbon负载均衡原理(Irule组件、源码分析)
后端·spring cloud·ribbon
利威尔xu10 小时前
Go IM 源码实战(13):30 个会话只有 2 个红点,未读数错在三条路径
redis·后端·微服务·面试·golang
8Qi813 小时前
DDD架构演进
java·分布式·mvc·ddd
方方洛13 小时前
ray教程-23-附录C-术语表
人工智能·分布式·机器学习
骑着蜗牛撵大象32720 小时前
Spring Boot 4.1 + Spring AI 2.0:RAG 供文、图谱供关系、MCP 供工具的 Java Agent 平台
java·spring boot·spring·微服务·知识图谱·rag·mcp
code_slave(码畜)21 小时前
微服务架构落地:慢 SQL 治理与连接池调优 —— 守住数据库的最后一道防线
数据库·spring boot·sql·spring cloud·微服务·架构
海棠Flower未眠21 小时前
下单请求处理流程:商品维度分布式锁与 Redis 预扣库存
redis·分布式·junit
罗四强1 天前
第10篇:打包上架:签名、打包 HAP、发布到华为应用市场
分布式·操作系统·harmonyos·arkts·鸿蒙系统
秋饼1 天前
Redis 全面解析:从数据结构到主从/哨兵/集群、分布式锁与面试高频考点,代码到原理一次讲透
数据库·redis·分布式·面试·后端开发