前言
最近为了学习微服务项目的开发,我自己开发了一个分布式博客项目。
在项目的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~ 谢谢