Shiro 一段时间没用,好多东西忘了,今天自定义Filter 注册到Shiro中的时候发现了几个问题:
1.这个Filter会执行2次
2.受Shiro管理的filter 无法使用Spring 的bean
所以干脆直接debug源码一探究竟:
先声明一点,我们在配置中通常都是通过ShiroFilterFactoryBean来配置的
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor
并且我们能够发现他还实现了BeanPostProcessor接口,是个后置处理器,也就意味着他的加载是早与Bean的,同时也说明了Shiro的加载优先于Bean
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}
通过createInstance()生成具体的Shiro
protected AbstractShiroFilter createInstance() throws Exception {
log.debug("Creating Shiro Filter instance.");
SecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
String msg = "SecurityManager property must be set.";
throw new BeanInitializationException(msg);
}
if (!(securityManager instanceof WebSecurityManager)) {
String msg = "The security manager does not implement the WebSecurityManager interface.";
throw new BeanInitializationException(msg);
}
FilterChainManager manager = createFilterChainManager();
//Expose the constructed FilterChainManager by first wrapping it in a
// FilterChainResolver implementation. The AbstractShiroFilter implementations
// do not know about FilterChainManagers - only resolvers:
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
//FilterChainResolver. It doesn't matter that the instance is an anonymous inner class
//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
//injection of the SecurityManager and FilterChainResolver:
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}
1.先绑定SecurityManager,如果是web环境下则匹配是否是instanceof WebSecurityManager
2.创建FilterChain ,Shiro的过滤器链,如下所示
protected FilterChainManager createFilterChainManager() {
DefaultFilterChainManager manager = new DefaultFilterChainManager();
Map<String, Filter> defaultFilters = manager.getFilters();
同时我们从代码中也能看出,Shiro率先配置Shiro自带的默认的Filter
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}
然后配置我们在配置文件中自定义的Filter
Map<String, Filter> filters = getFilters();
if (!CollectionUtils.isEmpty(filters)) {
for (Map.Entry<String, Filter> entry : filters.entrySet()) {
String name = entry.getKey();
Filter filter = entry.getValue();
applyGlobalPropertiesIfNecessary(filter);
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
添加,内部会进行是否有判断,然后设置
manager.addFilter(name, filter, false);
}
}
为这些filter配置动态URL,与我们具体原先的具体配置有关
Map<String, String> chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue();
manager.createChain(url, chainDefinition);
}
}
return manager;
}
制此shiro的过滤器链是创建成功了
下一步添加到FileterChainResolver中去,你配置了过滤器链总要有人调用处理吧
createInstance()至此结束,接下来的就是Spring 容器加载Bean的过程了,想了解的可以看下我的这篇文章,有点简陋,但是应该也看得懂
ok,我们需要重现bug了:浏览器访问filter拦截的url:
这是filter的执行流程前置:有点拗口,我编的因为
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ServletRequestAttributes attributes = new ServletRequestAttributes(request, response);
初始化绑定上下文,不多说了
initContextHolders(request, attributes);
try {
这是核心方法
filterChain.doFilter(request, response);
}
finally {
resetContextHolders();
if (logger.isDebugEnabled()) {
logger.debug("Cleared thread-bound request context: " + request);
}
attributes.requestCompleted();
}
}
进入的第一个方法是ApplicationFilterChain的
@Override
public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
字面翻译就行了,是否安全,返回的是系统的SecurityManager不是Shiro的,至于为何是null我
未深入debug,执行的是else中的方法
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
@Override
public Void run()
throws ServletException, IOException {
internalDoFilter(req,res);
return null;
}
}
);
} catch( PrivilegedActionException pe) {
Exception e = pe.getException();
if (e instanceof ServletException)
throw (ServletException) e;
else if (e instanceof IOException)
throw (IOException) e;
else if (e instanceof RuntimeException)
throw (RuntimeException) e;
else
throw new ServletException(e.getMessage(), e);
}
} else {
执行的是这个方法
internalDoFilter(request,response);
}
}
internalDoFilter方法,这才是内部的onion: 注意此处的filters ,我们可以查看属性
可以看出有AuthFilter 和shiroFilter,注意shiroFIlter我们在上面得知内部也配置了一个authFIlter(这是我自定义的一个filter),所以最终会执行两次,这跟SpringBoot的机制有关,SpringBoot会自动将实现了Filter接口的Bean收集到WebFilter中,这就导致了自定义Filter和ShiroFilter同级了,也就是说相当于注册了相同的bean2次 ,ok 那第一个问题解决了,去除@Bean 的注册即可,采用new的方式
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
pos代表当前坐标,有一个filter的数组,这个意味着是否传递下去给下一个filter调用
if (pos < n) {
获取filterConfig的配置
ApplicationFilterConfig filterConfig = filters[pos++];
try {
获取filter
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(
filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE);
}
if( Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res, this};
SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal);
} else {
进入对应的filter处理中
filter.doFilter(request, response, this);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
return;
}
//filter 结束了,ok servlet它lei了
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !servletSupportsAsync) {
request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR,
Boolean.FALSE);
}
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) &&
(response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED ) {
final ServletRequest req = request;
final ServletResponse res = response;
Principal principal =
((HttpServletRequest) req).getUserPrincipal();
Object[] args = new Object[]{req, res};
SecurityUtil.doAsPrivilege("service",
servlet,
classTypeUsedInService,
args,
principal);
} else {
servlet.service(request, response);
}
} catch (IOException | ServletException | RuntimeException e) {
throw e;
} catch (Throwable e) {
e = ExceptionUtils.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(null);
lastServicedResponse.set(null);
}
}
}
然后呢它会到AbstractShiroFilter chain中
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
提前声明异常,之所以前置声明是因为要根据不同的Error采取不同措施
Throwable t = null;
try {
准备阶段
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
subject.execute(new Callable() {
public Object call() throws Exception {
字面翻译即可
updateSessionLastAccessTime(request, response);
这是核心方法
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
subject.execute核心方法如下:
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
FilterChain chain = getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}
//获取过滤器链
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
获取过滤器处理器
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
}
这个方法解释在下面
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
log.trace("Resolved a configured FilterChain for the current request.");
chain = resolved;
} else {
log.trace("No FilterChain configured for the current request. Using the default.");
}
return chain;
}
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}
获取访问的url路径
String requestURI = getPathWithinApplication(request);
循环遍历匹配是否符合要拦截的url
for (String pathPattern : filterChainManager.getChainNames()) {
if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " +
"Utilizing corresponding filter chain...");
}
return filterChainManager.proxy(originalChain, pathPattern);
内部有个挺好的idea:,这个方法不是这个重入函数里的
public FilterChain proxy(FilterChain orig) {
return new ProxiedFilterChain(orig, this);
}
调用的同时生成一个新的代理Chain,从而继续代理
}
}
return null;
}
接下来就是具体的doFilter方法:这是属于ProxyFilterChain对象的方法,在这里Shiro将内部配置的bean一个一个都循环遍历下去,当然前提是要符合拦截要求
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.filters == null || this.filters.size() == this.index) {
//we've reached the end of the wrapped chain, so invoke the original one:
if (log.isTraceEnabled()) {
log.trace("Invoking original filter chain.");
}
this.orig.doFilter(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Invoking wrapped filter at index [" + this.index + "]");
}
this.filters.get(this.index++).doFilter(request, response, this);
}
}
OK, 这就是差不多是过滤器链的流程了
至于第二个bug的解决方式有以下几种:
第一种方法:.耦合度较高的一种:通过构造函数来注入: 如 A(IServiceB b) 然后在ShiroFactoryBean中添加属性参数 (IServiceB b)即可
第二种方法:.通过添加一个util来处理:
@Component
public class ApplicationContextUtil implements ApplicationContextAware
{
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
if (null != applicationContext)
{
ApplicationContextUtil.applicationContext = applicationContext;
} else
{
throw new RuntimeException("ApplicationContext配置为空");
}
}
public static ApplicationContext getApplicationContext()
{
return ApplicationContextUtil.applicationContext;
}
}
然后在doFilter方法中通过 :
if(null==loadBalancerClient)
{
this.loadBalancerClient = ApplicationContextUtil.getApplicationContext().getBean(LoadBalancerClient.class);
}
if(null==tmallAdminConfigProperty)
{
this.tmallAdminConfigProperty = ApplicationContextUtil.getApplicationContext()
.getBean(TmallAdminConfigProperty.class);
}
进行调用,或许有人会说在init中初始化,这是不行的哦,因为init方法并不会执行,当然或许有人想通过@PostConsturct初始化,但是注意这个加载完毕的时候那个bean还没初始化呢,不过可以这个也实现ApplicationContext接口,这是可以的,但是做不到通用
第三种方法:可以算是最快捷方便的吧,将自定义的Filter注册为Bean,是的,注册为Bean,然后借助SpringBoot提供的FilterRegistrationBean<T> 这个类:禁止这个filter ,只让Shiro管理即可:代码如下:
@Bean
public AuthFilter authFilter()
{
return new AuthFilter();
}
@Bean
public FilterRegistrationBean<Filter>filterReg()
{
FilterRegistrationBean<Filter>filterRegistrationBean=new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(authFilter());
filterRegistrationBean.setEnabled(false);
return filterRegistrationBean;
}
并且这个方式可以使得这个Filter正常的通过@Autowired 注入Bean了