Shiro源码分析----认证流程
由于本文是基于源码分析Shiro认证流程,所以假设阅读者对Shiro已经有一定的了解。
Apache Shiro作为一个优秀的权限框架,其最重要的两项工作:其一是认证,即解决登录的用户的身份是否合法;其二是用户登录后有什么样的权限。本文将基于Shiro源码来剖析Shiro的认证流程,只有深层次的理解Shiro认证流程,认证过程中各个组件的作用,才能在实际应用中灵活使用。由于Shiro一般用于Web环境且会与Spring集成使用,所以此次认证流程的分析的前提也是Web环境且Shiro已与Spring集成。
特别说明:本文使用的Shiro版本:1.2.2。
Shiro与Spring集成时,需要在web.xml中配置Shiro入口过滤器:
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param></filter><filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern></filter-mapping>
熟悉Spring的人应该都知道DelegatingFilterProxy
的作用,该Spring提供的过滤器只起委托作用,执行流程委托给Spring容器中名为shiroFilter
的过滤器。所以还需要在Spring配置文件中配置shiroFilter
,如下:
<!-- Shiro的Web过滤器 --><bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /index.jsp = anon /unauthorized.jsp = anon /login.jsp = authc /logout = logout /authenticated.jsp = authc /** = user </value> </property></bean>
ShiroFilterFactoryBean
实现了org.springframework.beans.factory.FactoryBean
接口,所以shiroFilter
对象是由ShiroFilterFactoryBean
的getObject()
方法返回的:
public Object getObject() throws Exception { if (instance == null) { instance = createInstance(); } return instance;}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); } // 必须是Web环境的安全管理器 if (!(securityManager instanceof WebSecurityManager)) { String msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); } // 创建过滤器链管理器 FilterChainManager manager = createFilterChainManager(); // 创建基于路径匹配的过滤器链解析器 PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); // 返回SpringShiroFilter对象 return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);}
从上述源码中可以看到,最终返回了一个SpringShiroFilter
对象,即Spring配置文件中的shiroFilter
对象,该过滤器拥有三个重要对象:SecurityManager
、PathMatchingFilterChainResolver
、FilterChainManager
。
由于在Spring配置中设置了filterChainDefinitions
属性,所以会调用setFilterChainDefinitions
方法:
public void setFilterChainDefinitions(String definitions) { Ini ini = new Ini(); ini.load(definitions); //did they explicitly state a 'urls' section? Not necessary, but just in case: Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS); if (CollectionUtils.isEmpty(section)) { //no urls section. Since this _is_ a urls chain definition property, just assume the //default section contains only the definitions: section = ini.getSection(Ini.DEFAULT_SECTION_NAME); } /** 获取默认section,也就是加载 /index.jsp = anon /unauthorized.jsp = anon /login.jsp = authc /logout = logout /authenticated.jsp = authc /** = user 这段配置,从这段配置中可以知道哪种URL需要应用上哪些Filter,像anon、authc、logout就是Filter的名称, Ini.Section实现了Map接口,其key为URL匹配符,value为Filter名称 **/ // 设置filterChainDefinitionMap setFilterChainDefinitionMap(section);}
FilterChainManager
用于管理当前Shiro应用的所有Filter,有Shiro默认使用的Filter,也可以是自定义的Filter。下面我们看看FilterChainManager
是如何创建出来的:
protected FilterChainManager createFilterChainManager() { // 创建DefaultFilterChainManager DefaultFilterChainManager manager = new DefaultFilterChainManager(); // 创建Shiro默认Filter,根据org.apache.shiro.web.filter.mgt.DefaultFilter创建 Map<String, Filter> defaultFilters = manager.getFilters(); //apply global settings if necessary: for (Filter filter : defaultFilters.values()) { // 设置相关Filter的loginUrl、successUrl、unauthorizedUrl属性 applyGlobalPropertiesIfNecessary(filter); } // 获取在Spring配置文件中配置的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); } // 将配置的Filter添加至链中,如果同名Filter已存在则覆盖默认Filter manager.addFilter(name, filter, false); } } //build up the chains: 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(); // 为配置的每一个URL匹配创建FilterChain定义, // 这样当访问一个URL的时候,一旦该URL配置上则就知道该URL需要应用上哪些Filter // 由于URL配置符会配置多个,所以以第一个匹配上的为准,所以越具体的匹配符应该配置在前面,越宽泛的匹配符配置在后面 manager.createChain(url, chainDefinition); } } return manager;}
PathMatchingFilterChainResolver
对象职责很简单,就是使用ant路径匹配方法匹配访问的URL,由于pathMatchingFilterChainResolver
拥有FilterChainManager
对象,所以URL匹配上后可以获取该URL需要应用的FilterChain
了。
通过上述分析可以知道,Shiro就是通过一系列的URL匹配符配置URL应该应用上的Filter,然后在Filter中完成相应的任务,所以Shiro的所有功能都是通过Filter完成的。当然认证功能也不例外,在上述配置中认证功能是由org.apache.shiro.web.filter.authc.FormAuthenticationFilter
完成的。
下面我们就看看入口过滤器SpringShiroFilter
的执行流程,是如何执行到FormAuthenticationFilter
的。既然是Filter,那么最重要的就是doFilter
方法了,由于SpringShiroFilter
继承自OncePerRequestFilter
,doFilter
方法也是在OncePerRequestFilter
中定义的:
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 用于保证链中同一类型的Filter只会被执行一次 String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation 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 { // 执行真正的功能代码 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); } }}
doFilterInternal
方法定义AbstractShiroFilter
中:
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException { Throwable t = null; try { final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain); final ServletResponse response = prepareServletResponse(request, servletResponse, chain); // 创建Subject对象,由此可见,每一个请求到来,都会调用createSubject方法 final Subject subject = createSubject(request, response); // 通过Subject对象执行过滤器链, 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; } // 省略一些代码...}
先看一下,Subject
如果是如何创建的:
protected WebSubject createSubject(ServletRequest request, ServletResponse response) { return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();}
跟踪代码最终调用DefaultWebSubjectFactory.createSubject
方法:
public Subject createSubject(SubjectContext context) { if (!(context instanceof WebSubjectContext)) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); // 判断是已经认证,如果是在没有登录之前,明显返回是false boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletRequest request = wsc.resolveServletRequest(); ServletResponse response = wsc.resolveServletResponse(); return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager);}
接下来看看过滤器链是如何创建与执行的:
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain) throws IOException, ServletException { // 获取当前URL匹配的过滤器链 FilterChain chain = getExecutionChain(request, response, origChain); // 执行过滤器链中的过滤器 chain.doFilter(request, response);}protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) { FilterChain chain = origChain; // 获取过滤器链解析器,即上面创建的PathMatchingFilterChainResolver对象 FilterChainResolver resolver = getFilterChainResolver(); if (resolver == null) { log.debug("No FilterChainResolver configured. Returning original FilterChain."); return origChain; } // 调用其getChain方法,根据URL匹配相应的过滤器链 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;}
根据上述Spring配置,假设现在第一次访问URL: "/authenticated.jsp"
,则会应用上名为authc
的Filter,即FormAuthenticationFilter
,根据FormAuthenticationFilter
的继承体系,先执行dviceFilter.doFilterInternal
方法:
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { Exception exception = null; try { // 执行preHandle boolean continueChain = preHandle(request, response); if (log.isTraceEnabled()) { log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]"); } // 如果preHandle返回false则过滤器链不再执行 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); }}
接下来执行:PathMatchingFilter.preHandle
方法:
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()) { // 根据配置,访问URL:"/authenticated.jsp"时,会匹配上FormAuthenticationFilter, // 而FormAuthenticationFilter继承自PathMatchingFilter,所以返回true if (pathsMatch(path, request)) { log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path); Object config = this.appliedPaths.get(path); // 执行isFilterChainContinued方法,该方法调用onPreHandle方法 return isFilterChainContinued(request, response, path, config); } } //no path matched, allow the request to go through: return true;}
接着执行AccessControlFilter.onPreHandle
方法:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { // 如果isAccessAllowed方法返回false,则会执行onAccessDenied方法 return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);}
接着执行AuthenticatingFilter.isAccessAllowed
方法:
@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue));}super.isAccessAllowed方法,即AuthenticationFilter.isAccessAllowed方法:protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request, response); return subject.isAuthenticated();}
由以上代码可知,由于是第一次访问URL:"/authenticated.jsp"
,所以isAccessAllowed
方法返回false
,所以接着执行FormAuthenticationFilter.onAccessDenied
方法:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 第一次访问自然不是登录请求 if (isLoginRequest(request, response)) { // 判断是否是POST请求 if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); } 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; }}protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { // 将request对象保存在session中,以便登录成功后重新转至上次访问的URL saveRequest(request); // 重定向至登录页面,即:"/login.jsp" redirectToLogin(request, response);}
根据配置,访问URL:"/login.jsp"
时也会应用上FormAuthenticationFilter
,由于是重定向所以发起的是GET
请求,所以isLoginSubmission()
返回false
,所以没有执行executeLogin
方法,所以能够访问/login.jsp
页面。在登录表单中应该设置action=""
,这样登录请求会提交至/login.jsp
,这时为POST
请求,所以会执行executeLogin
方法:
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { // 根据表单填写的用户名密码创建AuthenticationToken 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 subject = getSubject(request, response); // 执行Subject.login方法进行登录 subject.login(token); // 如果登录成功,重定向至上次访问的URL return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { // 如果登录失败,则设置错误信息至request,并重新返回登录页面 return onLoginFailure(token, e, request, response); }}protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { // 重定向至上次访问的URL issueSucce***edirect(request, response); // 由于返回false,所以过滤器链不再执行 return false;}protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { // 设置错误信息至request setFailureAttribute(request, e); // 由于返回true,所以过滤器链继续执行,所以又返回了登录页面 return true;}
至此,认证流程大致流程就是这样了,限于篇幅,登录的流程具体,请期待下篇博文。
-------------------------------- END -------------------------------
及时获取更多精彩文章,请关注公众号《Java精讲》。
©著作权归作者所有:来自51CTO博客作者Java精讲的原创作品,如需转载,请注明出处,否则将追究法律责任更多相关文章
- IP地址分类及网络配置方法和多网卡绑定技术应用
- 大数据成神之路-Java高级特性增强(多线程)
- TP6 linux安装方法
- 硬盘显示没有初始化恢复资料方法
- 中了exe病毒文件夹变exe应用程序解决方法
- 磁盘出现“文件系统变RAW”的解决方法
- 存储卡出现“无法访问此卷不包含可识别的文件系统”的解决方法
- 【面试】PHP 字符串翻(反)转的几种方法
- 【面试】两个变量进行交替的N种方法