Shiro BasicHttpAuthenticationFilter 认证流程分析 以 Shiro 1.8.0 为例子,其中新增了一些新的组件,譬如 HttpAuthenticationFilter
之前使用的 1.4.1 并不存在 HttpAuthenticationFilter
组件
1. BasicHttpAuthenticationFilter 层次结构 要分析 BasicHttpAuthenticationFilter
的认证流程,其实也是分析该过滤器(doFilter
)的执行流程,而该过滤器的继承层次有一定复杂度,因此先了解一下其继承结构:
2. OncePerRequestFilter.doFilter 从继承关系可以看到 BasicHttpAuthenticationFilter
继承自抽象类 OncePerRequestFilter
。
OncePerRequestFilter
的字面意思是:Once Per Request,即每个请求只执行一次,它巧妙的设计使得无论添加多少个过滤器,都只执行一次。
如下是 OncePerRequestFilter
的 doFilter
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Override public final void doFilter (ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) { throw new ServletException ("OncePerRequestFilter just supports HTTP requests" ); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null ; if (hasAlreadyFilteredAttribute || skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) { filterChain.doFilter(request, response); } else { request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { doFilterInternal(httpRequest, httpResponse, filterChain); } finally { request.removeAttribute(alreadyFilteredAttributeName); } } }
3. AdviceFilter.doFilterInternal 可以知道 OncePerRequestFilter
已经实现了 doFilter
,而且知道,真正的处理逻辑在 doFilterInternal()
方法中。然而,doFilterInternal()
方法是在子类 AdviceFilter
实现的,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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(request, response, exception); } }
4. PathMatchingFilter.preHandle 下面先看 preHandle,PathMatchingFilter
已经实现了 preHandle()。PathMatchingFilter,顾名思义,路径匹配过滤器,它的作用就是来根据路径匹配结果,调用相应过滤器(没匹配上的直接 return true,即继续执行过滤器链)。
path 匹配是通过 FilterChainDefinitionMap
注册的,比如设置了 “/login”, “anon”,那么如果本次请求的地址也是 /login,则会匹配上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 protected boolean preHandle (ServletRequest request, ServletResponse response) throws Exception { 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); } } return true ; }
上述代码中的 config 对象具有非常高的灵活性,在 BasicHttpAuthenticationFilter
的流程中,你可以进行一些 HTTP Method、permissive 的特殊配置,这些设计都是内嵌在过滤器中的:
1 2 3 4 chainDefinition.addPathDefinition("/**" , "authcBasic[get,post]" ); chainDefinition.addPathDefinition("/permissive/**" , "authcBasic[permissive]" );
如果 path 匹配成功,则会先执行 isFilterChainContinued(),isFilterChainContinued() 方法也是在 PathMatchingFilter 实现的。它的作用就是判断过滤器是否可用,如果可用就继续执行;否则,跳过,return true。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private boolean isFilterChainContinued (ServletRequest request, ServletResponse response, String path, Object pathConfig) throws Exception { if (isEnabled(request, response, path, pathConfig)) { 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 }); } return true ; }
isEnabled 方法本质上是判断 enabled 是否为 true。其实几乎所有的过滤器都可以执行,因此 enabled 默认为 true,除非人为的去设置它的值:
5. AccessControlFilter.onPreHandle 从 preHandle()
走下来的,这里之所以起名为 onPreHandle()
,是因为这才是真正的执行逻辑,之前的种种都是可以看作判断。
onPreHandle()
在 PathMatchingFilter
的子类 AccessControlFilter
有了新的实现,它的返回值依赖两个方法 isAccessAllowed()、onAccessDenied()。
1 2 3 public boolean onPreHandle (ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue); }
上述逻辑可以转换为:
1 2 3 4 5 6 public boolean onPreHandle (ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { if (isAccessAllowed(request, response, mappedValue)) { return true ; } return onAccessDenied(request, response, mappedValue); }
6. HttpAuthenticationFilter.isAccessAllowed 当执行 AccessControlFilter.onPreHandle 会首先判断 isAccessAllowed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) { HttpServletRequest httpRequest = WebUtils.toHttp(request); String httpMethod = httpRequest.getMethod(); Set<String> methods = httpMethodsFromOptions((String[]) mappedValue); boolean authcRequired = methods.size() == 0 ; for (String m : methods) { if (httpMethod.toUpperCase(Locale.ENGLISH).equals(m)) { authcRequired = true ; break ; } } if (authcRequired) { return super .isAccessAllowed(request, response, mappedValue); } else { return true ; } }
上述校验 HTTP Method 的方法对 RESTful 风格非常有效: 实际上,该方法先做了一个 HTTP Method 的比对,自定义 FilterChainDefinitionMap 的时候,可以设置一批 HTTP method 是需要认证的,比如: 如果当前使用 RESTful 风格请求。现有 [PUT] /project 用于更新,[GET] /project 用于获取全部数据,这两个请求 URL 都是一样的,但如何让 GET 请求通过,PUT 请求需要授权呢?答案就是使用 HTTP Method 方法过滤。 配置 /project = authcBasic[PUT] 那么,访问 /project 的时候,GET 方法是不用认证的。 所以现在知道,即使没有写 GET,依然也会走 BasicHttpAuthenticationFilter,只是认证直接跳过(return true)。 因此,如果 HTTP Method 属于这一类 Method,那么就调用了 super.isAccessAllowed 进行判断。
7. AuthenticationFilter.isAccessAllowed 下面继续观察 super.isAccessAllowed() 方法到底做了什么?
首先,在继承链上,离 HttpAuthenticationFilter
最近的 AuthenticatingFilter
也实现了 isAccessAllowed() 方法,如下所示:
1 2 3 4 protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) { return super .isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue)); }
注意到,该方法也使用了 super 去调用父类方法,找到最近的有实现方法的父类 AuthenticationFilter,方法如下:
1 2 3 4 protected boolean isAccessAllowed (ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request, response); return subject.isAuthenticated(); }
获取 Subject,然后调用 isAuthenticated() 判断是否已经认证过了。
作用:判断是否认证过了,通俗来说,就是登陆了没。
如果 isAccessAllowed 返回 false,表示不允许访问,那么需要继续判断:
1 !isLoginRequest(request, response) && isPermissive(mappedValue)
isLoginRequest()
的判断,在本 BasicHttpAuthenticationFilter 的案例中,根据请求头判断isPermissive()
的判断是根据你是否在 chainDefinition 配置 [permissive]
8. BasicHttpAuthenticationFilter.isLoginRequest 1 2 3 4 5 6 7 8 protected final boolean isLoginRequest (ServletRequest request, ServletResponse response) { return this .isLoginAttempt(request, response); } protected boolean isLoginAttempt (ServletRequest request, ServletResponse response) { String authzHeader = getAuthzHeader(request); return authzHeader != null && isLoginAttempt(authzHeader); }
9. BasicHttpAuthenticationFilter.onAccessDenied 回到 AccessControlFilter.onPreHandle 第二个处理逻辑 —— onAccessDenied
该方法就是 isAccessAllowed 返回 false 之后执行的,即访问拒绝的逻辑。
BasicHttpAuthenticationFilter
实现了自己的 onAccessDenied:
1 2 3 4 5 6 7 8 9 10 11 12 protected boolean onAccessDenied (ServletRequest request, ServletResponse response) throws Exception { boolean loggedIn = false ; if (isLoginAttempt(request, response)) { loggedIn = executeLogin(request, response); } if (!loggedIn) { sendChallenge(request, response); } return loggedIn; }
10. AuthenticatingFilter.executeLogin BasicHttpAuthenticationFilter 是没有实现 executeLogin() 的,因此将调用父类 AuthenticatingFilter 的 executeLogin() 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } }
createToken(),该方法又是 BasicHttpAuthenticationFilter 来实现的,其实也就是从 Authorization 的 Request Header 提取base64 编码的用户名和密码,然后解析,最终会实例化 UsernamePasswordToken。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 protected AuthenticationToken createToken (ServletRequest request, ServletResponse response) { String authorizationHeader = getAuthzHeader(request); if (authorizationHeader == null || authorizationHeader.length() == 0 ) { return createToken("" , "" , request, response); } log.debug("Attempting to execute login with auth header" ); String[] prinCred = getPrincipalsAndCredentials(authorizationHeader, request); if (prinCred == null || prinCred.length < 2 ) { String username = prinCred == null || prinCred.length == 0 ? "" : prinCred[0 ]; return createToken(username, "" , request, response); } String username = prinCred[0 ]; String password = prinCred[1 ]; return createToken(username, password, request, response); }
在 createToken 之后,会 getSubject,执行 login()。后面会委托给 SecurityManager.login 方法,在 securityManager 中会对 token 进行验证,本质上就是调用 Realm 方法验证,如果验证过程中没有异常抛出,则顺利执行,
如果认证过程没有异常抛出,最终会走到 onLoginSuccess(),如果有异常抛出则执行 onLoginFailure()。
一般也就是在 Realm 的执行逻辑中抛出异常。