shiro相关Filter
0前言
shiro中的filer是使用servlet中的filter接口扩展的,但是shiro代理了servlet 原生的filter,请求到了后会先执行shiro的filter,再执行原生的filter。
前面文章项目启动时shiro加载过程中介绍过,shiro只配置了shiroFilter一个拦截器入口,那么shiro是怎样执行内部那么多filter的?shiro内部访问控制filter又是如何实现的呢?
带着上述两个问题,我们梳理下shiro的filter实现和工作流程。
filter看翻译是过滤器的意思,但是经常有人把它翻译成拦截器,SpringMVC 中的 Interceptor才是拦截器。我也经常搞错,但是这里我们严谨亿点点。
1shiro的filter实现
这里照搬一下zhangkaitao博客中的图。
(1)Filter
servlet的filter,不熟悉的可以看一下servlet规范。
(2)AbstractFilter
shiro实现,主要用来配置FilterConfig,init()方法里提供了一个模板方法onFilterConfigSet()供子类实现。
(3)NameableFilter
给filter起名字的,如shiro自带的anon、authc,名字要求唯一,不然会有问题。
(4)OncePerRequestFilter
用于防止多次执行 Filter 的;也就是说一次请求只会走一次过滤器链;另外提供 enabled 属性,表示是否开启该过滤器实例,默认 enabled=true 表示开启,如果不想让某个过滤器工作,可以设置为 false 即可。可以简单看一下dofilter实现,源码107行
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
//首先检查一下有没有执行过这个filter,通过在request里面设置attribute实现 if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation
//在判断一下这个过滤器需不需要执行enable属性为true才需要执行 if (/* added in 1.2: */ !isEnabled(request, response) || /* retain backwards compatibility: */ shouldNotFilter(request) ) { log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", getName());
//不需要执行就到过滤器链的下一个过滤器 filterChain.doFilter(request, response); } else { // Do invoke this filter... log.trace("Filter '{}' not yet executed. Executing now.", getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try {
//重点,shiro的过滤器都走这个逻辑,这是一个模板方法,具体实现交给子类实现 doFilterInternal(request, response, filterChain); } finally { // Once the request has finished, we're done and we don't // need to mark as 'already filtered' any more. request.removeAttribute(alreadyFilteredAttributeName); } } }
OncePerRequestFilter算是一个比较重要的Filter实现,shiro的所有filter只有这一次dofilter实现。OncePerRequestFilter子类分成了两个分支:
一个是AdviceFilter,shiro内部授权和验证的filter实现都是在这一分支。
一个是AbstractShiroFilter,也就是web.xml中配置的shiro入口filter。
下面先分析一下AdviceFilter这条线路
(5)AdviceFilter
该类提供了Filter的Aop风格支持,类似于 SpringMVC 中的 Interceptor:
//在过滤器执行链前执行,异常会中断过滤器链后续执行(类似于aop的前置增强)
boolean preHandle(ServletRequest request, ServletResponse response) throws Exception
//在过滤器执行链后执行(类似aop的后置增强) void postHandle(ServletRequest request, ServletResponse response) throws Exception
//最终执行,可以用于资源清理(类似于aop的后置最终增强) void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;
前面说过OncePerRequestFilter的dofilter调用了一个模板方法doFilterInternal,AdviceFilter正是通过实现doFilterInternal实现类似aop功能,AdviceFilter类源码124行
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { Exception exception = null; try { //执行前置增强逻辑 boolean continueChain = preHandle(request, response); if (log.isTraceEnabled()) { log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]"); }
//前置增强器正常返回才会继续执行过滤器链后续 if (continueChain) { executeChain(request, response, chain); }
//执行后置增强器 postHandle(request, response); if (log.isTraceEnabled()) { log.trace("Successfully invoked postHandle method"); } } catch (Exception e) { exception = e; } finally {
//cleanup里面调用了afterCompletion方法 cleanup(request, response, exception); } }
(6)PathMatchingFilter
PathMatchingFilter 提供了基于 Ant 风格的请求路径匹配功能及过滤器参数解析的功能.。
PathMatchingFilter重写了父类AdviceFilter的preHandle方法,PathMatchingFilter类源码163行
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
//appliedPath就是配置中指定的需要拦截的url if (this.appliedPaths == null || this.appliedPaths.isEmpty()) { if (log.isTraceEnabled()) { log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately."); } return true; } for (String path : this.appliedPaths.keySet()) {
//处理请求的路径匹配需要拦截的路径 if (pathsMatch(path, request)) { log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path); Object config = this.appliedPaths.get(path); return isFilterChainContinued(request, response, path, config); } } //no path matched, allow the request to go through: return true; }
主要执行的内容交给isFilterChainContinued,PathMatchingFilter类源码192行
private boolean isFilterChainContinued(ServletRequest request, ServletResponse response, String path, Object pathConfig) throws Exception {
//isEnabled判断这个过滤器生不生效,这里会调用OncePerRequestFilter的实现 if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2 if (log.isTraceEnabled()) { log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}]. " + "Delegating to subclass implementation for 'onPreHandle' check.", new Object[]{getName(), path, pathConfig}); }
//重点要关注的方法 return onPreHandle(request, response, pathConfig); } if (log.isTraceEnabled()) { log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}]. " + "The next element in the FilterChain will be called immediately.", new Object[]{getName(), path, pathConfig}); } //This filter is disabled for this specific request, //return 'true' immediately to indicate that the filter will not process the request //and let the request/response to continue through the filter chain: return true; }
onPreHandle方法处理匹配路径时需要处理的过滤器逻辑,PathMatchingFilter这里默认返回true,主要交给子类实现。
AnonymousFilter,也就是配置里的anon,就是实现PathMatchingFilter,我们可以看到anon的onPreHandle方法啥也没干(没有拦截逻辑),直接返回true。
public class AnonymousFilter extends PathMatchingFilter { /** * Always returns <code>true</code> allowing unchecked access to the underlying path or resource. * * @return <code>true</code> always, allowing unchecked access to the underlying path or resource. */ @Override protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) { // Always return true since we allow access to anyone return true; } }
(7)AccessControlFilter
AccessControlFilter 提供了访问控制的基础功能,比如是否允许访问/当访问拒绝时如何处理等。
这是比较重要的过滤器实现,shiro自己内部访问控制Filter都是基于该Filter实现,我们自己定义的Filter也可以直接扩展该类,(不需要复杂访问控制逻辑自定义Filter也可以直接扩展PathMatchingFilter )
AccessControlFilter重写了父类PathMatchingFilter的onPreHandle方法,相当于该过滤器的入口。AccessControlFilter类源码161行。
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); }
isAccessAllowed方法返回true表示通过访问控制验证,否则才会执行后面的onAccessDenied,onAccessDenied表示访问拒绝时需要执行的逻辑。
isAccessAllowed和onAccessDenied都是需要子类扩展的。这里以authc为例看看怎么实现的。authc表示FormAuthenticationFilter。
FormAuthenticationFilter继承AuthenticatingFilter,AuthenticatingFilter又继承AuthenticationFilter,最后AuthenticationFilter继承AccessControlFilter
首先isAccessAllowed实现
//AuthenticatingFilter第121行
@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue)); }
//AuthenticatingFilter父类AuthenticationFilter第79行,判断有没有登陆
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}
如果登陆了,直接返回true,否则看看是不是请求的登陆url或者配置了许可的请求,满足其一就返回true
然后是onAccessDenied实现
onAccessDenied没有那么多弯弯道道,直接由FormAuthenticationFilter实现,源码148行
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//如果请求路径是登陆请求,有没有显示的配置loginUrl,没有的话默认/login.jsp
if (isLoginRequest(request, response)) {
//登陆请求必须是post的 if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); }
//执行login逻辑,这里用户可以不用自己写登陆入口,前提是需要配置loginUrl return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } //其他请求会保留现场,并重定向到登陆页面 saveRequestAndRedirectToLogin(request, response); return false; } }
下面看一下authc的登陆实现executeLogin,源代码AuthenticatingFilter类第44行
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { AuthenticationToken token = createToken(request, response); if (token == null) { String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " + "must be created in order to execute a login attempt."; throw new IllegalStateException(msg); } try { Subject subject = getSubject(request, response); subject.login(token);
//登陆成功后会重定向到登陆前请求的页面,successUrl只是个备选重定向地址 return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) {
//失败的话会在request的attribute里面记录失败异常,请求会继续回到当前页 return onLoginFailure(token, e, request, response); } }
若不是登陆请求saveRequestAndRedirectToLogin则会存下request,并重定向到登陆页面loginUrl
如果不想用shiro提供的登陆逻辑呢。而且现在项目基本都是前后端分离,使用异步请求,路由的逻辑一般都会交给前端处理,重定向不是那么友好。
我想到的有三个思路:
1)第一个是authc不拦截登陆url
自己在controller里实现登陆的逻辑,但是authc拦截器会拦截其他的url,没有登陆的话请求后端其他接口还是会重定向到loginUrl。
这里后端可以配一个假的loginUrl,重定向到“前端host/noauth”,然后前端要起一个代理服务打到“后端服务/noauth”,后端response返回一个401给前端,前端接收到401响应自己路由到登陆页面。
这样的话前端要起一个代理服务,并且后端还是重定向了,只能说没有走authc的executeLogin。可以但是没有必要
2)自定义扩展AccessControlFilter
可以参考FormAuthenticationFilter实现,没有认证时不重定向,直接response设置401即可。授权模块的AuthorizationFilter就是这么做的,参考源码AuthorizationFilter类106行
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { Subject subject = getSubject(request, response); // If the subject isn't identified, redirect to login URL if (subject.getPrincipal() == null) { saveRequestAndRedirectToLogin(request, response); } else { // If subject is known but not authorized, redirect to the unauthorized URL if there is one // If no unauthorized URL is specified, just return an unauthorized HTTP status code String unauthorizedUrl = getUnauthorizedUrl(); //SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit: if (StringUtils.hasText(unauthorizedUrl)) { WebUtils.issueRedirect(request, response, unauthorizedUrl); } else { WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED); } } return false; }
配置了unauthorizedUrl就走重定向逻辑,没有配置就直接返回response 401
3)干脆不用shiro的访问控制Filter
自己写一个Filter或者实现SpringMVC 中的 Interceptor,自己做登陆拦截或者权限拦截也很nice
到此,我们回答了一个问题:shiro内部访问控制filter又是如何实现的
这些Filter都没有配置到web.xml,spring也没有显示处理这部分filter配置。那么这些filter是如何生效的,我们知道web项目唯一配置的Filter是ShiroFilter,
也许我们可以从shiro Filter的另一个实现分支AbstractShiroFilter中找到答案。
2shiro的Filter工作流程
先看入口ShiroFilter,shiroFilter只是提供了init方法的实现,用于绑定SecurityManager和FilterChainResolver,主要的功能还是在其父类AbstractShiroFilter实现的。
AbstractShiroFilter直接继承了OncePerRequestFilter,所以我们还是从模板方法doFilterInternal突破,请求会到这里来,源代码AbstractShiroFilter第350行。
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { Throwable t = null; try {
//HttpServletRequest会被包装为ShiroHttpServletRequest final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
//HttpServletResponse会被包装为ShiroHttpServletResponse final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
//为没有请求/响应对创建一个WebSubject实例,这个实例会在整个请求响应执行过程中使用 final Subject subject = createSubject(request, response); //noinspection unchecked subject.execute(new Callable() { public Object call() throws Exception {
//更新session访问时间,只会管理shiro自己的session(HttpSession不会管)
//通过调用session.touch()实现 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); } }
next
//源代码AbstractShiroFilter第446行
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException {
//注意,这里会代理FilterChain FilterChain chain = getExecutionChain(request, response, origChain); chain.doFilter(request, response); }
//源代码AbstractShiroFilter第406行
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
//获取初始化时注入的FilterChainResolver
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
}
//重点,生成代理的FilterChain
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;
}
源代码PathMatchingFilterChainResolver类第93行
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
//(1)FilterChainManager维护了shiro默认Filter和用户自定义Filter,
//还维护了filterChains表示用户配置的pathPattern到多个filter的对应关系 FilterChainManager filterChainManager = getFilterChainManager(); if (!filterChainManager.hasChains()) { return null; } String requestURI = getPathWithinApplication(request); //the 'chain names' in this implementation are actually path patterns defined by the user. We just use them //as the chain name for the FilterChainManager's requirements for (String pathPattern : filterChainManager.getChainNames()) { // If the path does match, then pass on to the subclass implementation for specific checks:
//如果请求路径和pathPattern匹配 if (pathMatches(pathPattern, requestURI)) { if (log.isTraceEnabled()) { log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " + "Utilizing corresponding filter chain..."); }
//代理原生的FilterChain return filterChainManager.proxy(originalChain, pathPattern); } } return null; }
这里说明下FilterChainManager 中维护的filterChains,表示用户配置的pathPattern到多个filter的对应关系。pathPattern是用户配置的url,如/login、/role/*等,filter表示这些路径请求需要执行的过滤器如authc、anon等。
下面跟进filterChainManager是如何代理的,
//源代码DefaultFilterChainManager类第318行
public FilterChain proxy(FilterChain original, String chainName) { NamedFilterList configured = getChain(chainName); if (configured == null) { String msg = "There is no configured chain under the name/key [" + chainName + "]."; throw new IllegalArgumentException(msg); }
// return configured.proxy(original); }
//源代码SimpleNamedFilterList类,第78行
public FilterChain proxy(FilterChain orig) {
return new ProxiedFilterChain(orig, this);
}
最终找到了代理类ProxiedFilterChain
public class ProxiedFilterChain implements FilterChain { //TODO - complete JavaDoc private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class); private FilterChain orig;
//filters表示本次请求路径匹配的pathPattern对应的所有Filter private List<Filter> filters; private int index = 0; public ProxiedFilterChain(FilterChain orig, List<Filter> filters) { if (orig == null) { throw new NullPointerException("original FilterChain cannot be null."); } this.orig = orig; this.filters = filters; this.index = 0; } 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."); }
//如果匹配到shiro的Filter为空,或者匹配到shiro的filter都执行完了,才会执行servlet原本的Filter逻辑 this.orig.doFilter(request, response); } else { if (log.isTraceEnabled()) { log.trace("Invoking wrapped filter at index [" + this.index + "]"); }
//这里开始一个个按顺序执行shiro自己的Filter逻辑 this.filters.get(this.index++).doFilter(request, response, this); } } }
这就是ShiroFilter整个代理执行过程。