业务需求背景:
项目采用微服务架构,在各个服务前面配置一个网关,通过SpringCloud生态中的Zuul组件实现。
该网关同时负责页面调度,在各个单页面应用子产品的页面之间进行调度。
ZuulFilter挺有意思,对于本服务的Controller请求不会进行拦截,因此需要针对页面请求做一个认证鉴权的Filter。
实现第一版
首先实现一个Filter进行鉴权及页面重定向(未登录认证状态下跳转到登录页面)。
大体逻辑如下:
①通过WebFilter进行Filter声明,这样容器在进行部署的时候就会处理该Filter,创建实例并创建配置对象FilterConfig,然后会将该Filter应用到urlPatterns所指定的url;
②在init方法中获取到初始化参数,自定义的excludedUrls,作为成员在后续执行过滤逻辑的时候使用;
③在doFilter中进行url的鉴定,如果需要执行认证鉴权处理,则执行相应逻辑。不满足条件的情况下重定向到登录页;
④Filter类增加Component注解,让该Filter被容器管理。
@Component
@WebFilter(filterName = "WebAuthFilter", urlPatterns = "/web/*",
initParams = {
@WebInitParam(name = "excludedUrls", value = "/web/login")
}
)
public class WebAuthFilter implements Filter {
private List<String> excludedUrlList;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String excludedUrls = filterConfig.getInitParameter("excludeUrls");
excludedUrlList = Splitter.on(",").omitEmptyStrings().splitToList(excludedUrls);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String url = ((HttpServletRequest) request).getRequestURI();
if (excludedUrlList.contains(url)) {
chain.doFilter(request, response);
} else {
String sToken = ((HttpServletRequest) request).getHeader("Authorization");
if (sToken != null) {
Map<String, Object> map = TokenUtils.parseToken(sToken);
if (map == null) {
((HttpServletResponse)response).sendRedirect("/web/login");
}
} else {
((HttpServletResponse)response).sendRedirect("/web/login");
}
}
}
@Override
public void destroy() {
}
}
然后在SpringBoot的Application中增加注解@ServletComponentScan,这样容器会扫描到@Component注解的Filter。
问题出现
出现的问题是:访问的url为/user/*或者/product/*的时候,该过滤器也执行了!
也就是说,WebFilter注解配置的urlPatterns没有起作用。
问题定位:
在查看容器启动日志的时候,发现WebAuthFilter被两次注册,两次映射:
从上图可以看到,WebAuthFilter这个Filter是我们自己定义的,它被做了两次映射,而且两次映射的名字不同(WebAuthFilter和webAuthFilter),分别映射到的URL是“/web/*”和”“/*”。其中WebAuthFilter是我们自己命名的。
这样就解释了为什么所有的URL都会被该Filter处理。
问题定位
WebAuthFilter的第一次映射容易理解,是我们自己通过@WebFilter定义的。
那么webAuthFilter是谁给映射的呢?
必然是Spring容器处理的。
在跟踪源码的时候找到AbstractFilterRegistrationBean抽象类,该类中有一个方法onStartup,应该是容器启动的时候执行的,做的是一些Bean注册的工作。该方法最后调用了configure,在该方法中进行了映射处理。
if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {
this.logger.info("Mapping filter: '" + registration.getName() + "' to: "
+ Arrays.asList(DEFAULT_URL_MAPPINGS));
registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter,
DEFAULT_URL_MAPPINGS);
}
else {
if (!servletNames.isEmpty()) {
this.logger.info("Mapping filter: '" + registration.getName()
+ "' to servlets: " + servletNames);
registration.addMappingForServletNames(dispatcherTypes, this.matchAfter,
servletNames.toArray(new String[servletNames.size()]));
}
if (!this.urlPatterns.isEmpty()) {
this.logger.info("Mapping filter: '" + registration.getName()
+ "' to urls: " + this.urlPatterns);
registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter,
this.urlPatterns.toArray(new String[this.urlPatterns.size()]));
}
}
在servletNames和urlPatterns为空的情况下,进行了缺省映射,即映射到“/*”。
置于servletNames和urlPatterns为空的情况,这里没有深究了。
那么,为什么会出现定义的WebAuthFilter被两次注册的情况呢?
仔细分析了一下,认为可能的原因是:@Component和@WebFilter双重注册导致的。
解决办法
解决办法一@WebFilter
在这种情况下,去掉了@Component注解,再次启动服务。查看日志,发现该Filter仅被映射一次,通过浏览器访问相应的url也表现正确。
解决办法二@Component
这种情况下,保留了@Component注解,那么要进行配置的urlPatterns怎么处理呢?
通过FilterRegistrationBean进行@Bean声明,查看源码知道,onStartup进行注册的时候,实际上也是找到了各类RegistrationBean然后分别注册,配置映射。
有各种类型的RegistrationBean:
①AbstractFilterRegistrationBean;
②FilterRegistrationBean;
③ServletListenerRegistrationBean;
④ServletRegistrationBean;
那么我们自然可以通过自声明一个FilterRegistrationBean来进行注册。这种处理方式如下:
去掉FIlter上的@WebFilter注解,增加如下的Configuration类:
@Configuration
public class WebAuthFilterConfig {
@Bean
public FilterRegistrationBean webAuthFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(webAuthFilter());
registration.setName("WebAuthFilter");
registration.addUrlPatterns("/web/*");
registration.addInitParameter("excludeUrls", "/web/login");
registration.setOrder(0);
return registration;
}
@Bean
public Filter webAuthFilter() {
return new WebAuthFilter();
}
}
如此处理,也能达到同样的效果。
经过对比,当然第一种解决方案更直白,更简洁。
后述:网上的很多东西都是带着坑的,直接搬过来用真的有风险!