shiro认证流程

转载地址:https://blog.csdn.net/xtayfjpk/article/details/53729135

Shiro源码分析----认证流程

2016年12月18日 19:25:12

阅读数:4485

    由于本文是基于源码分析Shiro认证流程,所以假设阅读者对Shiro已经有一定的了解,如果对Shiro还不大了解的话,推荐一下博文:跟我学Shiro目录贴


Apache Shiro作为一个优秀的权限框架,其最重要的两项工作:其一是认证,即解决登录的用户的身份是否合法;其二是用户登录后有什么样的权限。本文将基于Shiro源码来剖析Shiro的认证流程,只有深层次的理解Shiro认证流程,认证过程中各个组件的作用,才能在实际应用中灵活使用。由于Shiro一般用于Web环境且会与Spring集成使用,所以此次认证流程的分析的前提也是Web环境且Shiro已与Spring集成。


特别说明:本文使用的Shiro版本:1.2.2。
 

Shiro与Spring集成时,需要在web.xml中配置Shiro入口过滤器:

 
  1. <filter>

  2. <filter-name>shiroFilter</filter-name>

  3. <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

  4. <async-supported>true</async-supported>

  5. <init-param>

  6. <param-name>targetFilterLifecycle</param-name>

  7. <param-value>true</param-value>

  8. </init-param>

  9. </filter>

  10. <filter-mapping>

  11. <filter-name>shiroFilter</filter-name>

  12. <url-pattern>/*</url-pattern>

  13. </filter-mapping>


熟悉Spring的人应该都知道DelegatingFilterProxy的作用,该Spring提供的过滤器只起委托作用,执行流程委托给Spring容器中名为shiroFilter的过滤器。所以还需要在Spring配置文件中配置shiroFilter,如下:

 
  1. <!-- Shiro的Web过滤器 -->

  2. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">

  3. <property name="securityManager" ref="securityManager"/>

  4. <property name="loginUrl" value="/login.jsp"/>

  5. <property name="unauthorizedUrl" value="/unauthorized.jsp"/>

  6. <property name="filters">

  7. <util:map>

  8. <entry key="authc" value-ref="formAuthenticationFilter"/>

  9. </util:map>

  10. </property>

  11. <property name="filterChainDefinitions">

  12. <value>

  13. /index.jsp = anon

  14. /unauthorized.jsp = anon

  15. /login.jsp = authc

  16. /logout = logout

  17. /authenticated.jsp = authc

  18. /** = user

  19. </value>

  20. </property>

  21. </bean>


ShiroFilterFactoryBean实现了org.springframework.beans.factory.FactoryBean接口,所以shiroFilter对象是由ShiroFilterFactoryBean的getObject()方法返回的:

 
  1. public Object getObject() throws Exception {

  2. if (instance == null) {

  3. instance = createInstance();

  4. }

  5. return instance;

  6. }

  7.  
  8. protected AbstractShiroFilter createInstance() throws Exception {

  9.  
  10. log.debug("Creating Shiro Filter instance.");

  11. // 获取配置文件中设置的安全管理器

  12. SecurityManager securityManager = getSecurityManager();

  13. if (securityManager == null) {

  14. String msg = "SecurityManager property must be set.";

  15. throw new BeanInitializationException(msg);

  16. }

  17. // 必须是Web环境的安全管理器

  18. if (!(securityManager instanceof WebSecurityManager)) {

  19. String msg = "The security manager does not implement the WebSecurityManager interface.";

  20. throw new BeanInitializationException(msg);

  21. }

  22.  
  23. // 创建过滤器链管理器

  24. FilterChainManager manager = createFilterChainManager();

  25.  
  26. // 创建基于路径匹配的过滤器链解析器

  27. PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();

  28. chainResolver.setFilterChainManager(manager);

  29.  
  30. // 返回SpringShiroFilter对象

  31. return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);

  32. }


从上述源码中可以看到,最终返回了一个SpringShiroFilter对象,即Spring配置文件中的shiroFilter对象,该过滤器拥有三个重要对象:SecurityManager、PathMatchingFilterChainResolver、FilterChainManager。


由于在Spring配置中设置了filterChainDefinitions属性,所以会调用setFilterChainDefinitions方法:

 
  1. public void setFilterChainDefinitions(String definitions) {

  2. Ini ini = new Ini();

  3. ini.load(definitions);

  4. //did they explicitly state a 'urls' section? Not necessary, but just in case:

  5. Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);

  6. if (CollectionUtils.isEmpty(section)) {

  7. //no urls section. Since this _is_ a urls chain definition property, just assume the

  8. //default section contains only the definitions:

  9. section = ini.getSection(Ini.DEFAULT_SECTION_NAME);

  10. }

  11. /** 获取默认section,也就是加载

  12. /index.jsp = anon

  13. /unauthorized.jsp = anon

  14. /login.jsp = authc

  15. /logout = logout

  16. /authenticated.jsp = authc

  17. /** = user

  18. 这段配置,从这段配置中可以知道哪种URL需要应用上哪些Filter,像anon、authc、logout就是Filter的名称,

  19. Ini.Section实现了Map接口,其key为URL匹配符,value为Filter名称

  20. **/

  21. // 设置filterChainDefinitionMap

  22. setFilterChainDefinitionMap(section);

  23. }


FilterChainManager用于管理当前Shiro应用的所有Filter,有Shiro默认使用的Filter,也可以是自定义的Filter。下面我们看看FilterChainManager是如何创建出来的:

 
  1. protected FilterChainManager createFilterChainManager() {

  2. // 创建DefaultFilterChainManager

  3. DefaultFilterChainManager manager = new DefaultFilterChainManager();

  4. // 创建Shiro默认Filter,根据org.apache.shiro.web.filter.mgt.DefaultFilter创建

  5. Map<String, Filter> defaultFilters = manager.getFilters();

  6. //apply global settings if necessary:

  7. for (Filter filter : defaultFilters.values()) {

  8. // 设置相关Filter的loginUrl、successUrl、unauthorizedUrl属性

  9. applyGlobalPropertiesIfNecessary(filter);

  10. }

  11.  
  12. // 获取在Spring配置文件中配置的Filter

  13. Map<String, Filter> filters = getFilters();

  14. if (!CollectionUtils.isEmpty(filters)) {

  15. for (Map.Entry<String, Filter> entry : filters.entrySet()) {

  16. String name = entry.getKey();

  17. Filter filter = entry.getValue();

  18. applyGlobalPropertiesIfNecessary(filter);

  19. if (filter instanceof Nameable) {

  20. ((Nameable) filter).setName(name);

  21. }

  22. // 将配置的Filter添加至链中,如果同名Filter已存在则覆盖默认Filter

  23. manager.addFilter(name, filter, false);

  24. }

  25. }

  26.  
  27. //build up the chains:

  28. Map<String, String> chains = getFilterChainDefinitionMap();

  29. if (!CollectionUtils.isEmpty(chains)) {

  30. for (Map.Entry<String, String> entry : chains.entrySet()) {

  31. String url = entry.getKey();

  32. String chainDefinition = entry.getValue();

  33. // 为配置的每一个URL匹配创建FilterChain定义,

  34. // 这样当访问一个URL的时候,一旦该URL配置上则就知道该URL需要应用上哪些Filter

  35. // 由于URL配置符会配置多个,所以以第一个匹配上的为准,所以越具体的匹配符应该配置在前面,越宽泛的匹配符配置在后面

  36. manager.createChain(url, chainDefinition);

  37. }

  38. }

  39.  
  40. return manager;

  41. }


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中定义的:

 
  1. public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)

  2. throws ServletException, IOException {

  3. // 用于保证链中同一类型的Filter只会被执行一次

  4. String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();

  5. if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {

  6. log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", getName());

  7. filterChain.doFilter(request, response);

  8. } else //noinspection deprecation

  9. if (/* added in 1.2: */ !isEnabled(request, response) ||

  10. /* retain backwards compatibility: */ shouldNotFilter(request) ) {

  11. log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.",

  12. getName());

  13. filterChain.doFilter(request, response);

  14. } else {

  15. // Do invoke this filter...

  16. log.trace("Filter '{}' not yet executed. Executing now.", getName());

  17. request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

  18.  
  19. try {

  20. // 执行真正的功能代码

  21. doFilterInternal(request, response, filterChain);

  22. } finally {

  23. // Once the request has finished, we're done and we don't

  24. // need to mark as 'already filtered' any more.

  25. request.removeAttribute(alreadyFilteredAttributeName);

  26. }

  27. }

  28. }



doFilterInternal方法定义AbstractShiroFilter中:

 
  1. protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)

  2. throws ServletException, IOException {

  3.  
  4. Throwable t = null;

  5.  
  6. try {

  7. final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);

  8. final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

  9.  
  10. // 创建Subject对象,由此可见,每一个请求到来,都会调用createSubject方法

  11. final Subject subject = createSubject(request, response);

  12.  
  13. // 通过Subject对象执行过滤器链,

  14. subject.execute(new Callable() {

  15. public Object call() throws Exception {

  16. // 更新会话最后访问时间,用于计算会话超时

  17. updateSessionLastAccessTime(request, response);

  18. // 执行过滤器链

  19. executeChain(request, response, chain);

  20. return null;

  21. }

  22. });

  23. } catch (ExecutionException ex) {

  24. t = ex.getCause();

  25. } catch (Throwable throwable) {

  26. t = throwable;

  27. }

  28.  
  29. // 省略一些代码...

  30. }


先看一下,Subject如果是如何创建的:

 
  1. protected WebSubject createSubject(ServletRequest request, ServletResponse response) {

  2. return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();

  3. }


跟踪代码最终调用DefaultWebSubjectFactory.createSubject方法:

 
  1. public Subject createSubject(SubjectContext context) {

  2. if (!(context instanceof WebSubjectContext)) {

  3. return super.createSubject(context);

  4. }

  5. WebSubjectContext wsc = (WebSubjectContext) context;

  6. SecurityManager securityManager = wsc.resolveSecurityManager();

  7. Session session = wsc.resolveSession();

  8. boolean sessionEnabled = wsc.isSessionCreationEnabled();

  9. PrincipalCollection principals = wsc.resolvePrincipals();

  10. // 判断是已经认证,如果是在没有登录之前,明显返回是false

  11. boolean authenticated = wsc.resolveAuthenticated();

  12. String host = wsc.resolveHost();

  13. ServletRequest request = wsc.resolveServletRequest();

  14. ServletResponse response = wsc.resolveServletResponse();

  15.  
  16. return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,

  17. request, response, securityManager);

  18. }


接下来看看过滤器链是如何创建与执行的:

 
  1. protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)

  2. throws IOException, ServletException {

  3. // 获取当前URL匹配的过滤器链

  4. FilterChain chain = getExecutionChain(request, response, origChain);

  5. // 执行过滤器链中的过滤器

  6. chain.doFilter(request, response);

  7. }

  8.  
  9. protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {

  10. FilterChain chain = origChain;

  11. // 获取过滤器链解析器,即上面创建的PathMatchingFilterChainResolver对象

  12. FilterChainResolver resolver = getFilterChainResolver();

  13. if (resolver == null) {

  14. log.debug("No FilterChainResolver configured. Returning original FilterChain.");

  15. return origChain;

  16. }

  17.  
  18. // 调用其getChain方法,根据URL匹配相应的过滤器链

  19. FilterChain resolved = resolver.getChain(request, response, origChain);

  20. if (resolved != null) {

  21. log.trace("Resolved a configured FilterChain for the current request.");

  22. chain = resolved;

  23. } else {

  24. log.trace("No FilterChain configured for the current request. Using the default.");

  25. }

  26.  
  27. return chain;

  28. }


根据上述Spring配置,假设现在第一次访问URL: "/authenticated.jsp",则会应用上名为authc的Filter,即FormAuthenticationFilter,根据FormAuthenticationFilter的继承体系,先执行dviceFilter.doFilterInternal方法:

 
  1. public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)

  2. throws ServletException, IOException {

  3. Exception exception = null;

  4.  
  5. try {

  6. // 执行preHandle

  7. boolean continueChain = preHandle(request, response);

  8. if (log.isTraceEnabled()) {

  9. log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]");

  10. }

  11. // 如果preHandle返回false则过滤器链不再执行

  12. if (continueChain) {

  13. executeChain(request, response, chain);

  14. }

  15.  
  16. postHandle(request, response);

  17. if (log.isTraceEnabled()) {

  18. log.trace("Successfully invoked postHandle method");

  19. }

  20.  
  21. } catch (Exception e) {

  22. exception = e;

  23. } finally {

  24. cleanup(request, response, exception);

  25. }

  26. }


接下来执行:PathMatchingFilter.preHandle方法:

 
  1. protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

  2.  
  3. if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {

  4. if (log.isTraceEnabled()) {

  5. log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately.");

  6. }

  7. return true;

  8. }

  9.  
  10. for (String path : this.appliedPaths.keySet()) {

  11. // 根据配置,访问URL:"/authenticated.jsp"时,会匹配上FormAuthenticationFilter,

  12. // 而FormAuthenticationFilter继承自PathMatchingFilter,所以返回true

  13. if (pathsMatch(path, request)) {

  14. log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path);

  15. Object config = this.appliedPaths.get(path);

  16. // 执行isFilterChainContinued方法,该方法调用onPreHandle方法

  17. return isFilterChainContinued(request, response, path, config);

  18. }

  19. }

  20.  
  21. //no path matched, allow the request to go through:

  22. return true;

  23. }


接着执行AccessControlFilter.onPreHandle方法:

 
  1. public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {

  2. // 如果isAccessAllowed方法返回false,则会执行onAccessDenied方法

  3. return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);

  4. }


接着执行AuthenticatingFilter.isAccessAllowed方法:

 
  1. @Override

  2. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

  3. return super.isAccessAllowed(request, response, mappedValue) ||

  4. (!isLoginRequest(request, response) && isPermissive(mappedValue));

  5. }

  6. super.isAccessAllowed方法,即AuthenticationFilter.isAccessAllowed方法:

  7. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

  8. Subject subject = getSubject(request, response);

  9. return subject.isAuthenticated();

  10. }


由以上代码可知,由于是第一次访问URL:"/authenticated.jsp",所以isAccessAllowed方法返回false,所以接着执行FormAuthenticationFilter.onAccessDenied方法:

 
  1. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {

  2. // 第一次访问自然不是登录请求

  3. if (isLoginRequest(request, response)) {

  4. // 判断是否是POST请求

  5. if (isLoginSubmission(request, response)) {

  6. if (log.isTraceEnabled()) {

  7. log.trace("Login submission detected. Attempting to execute login.");

  8. }

  9. return executeLogin(request, response);

  10. } else {

  11. if (log.isTraceEnabled()) {

  12. log.trace("Login page view.");

  13. }

  14. //allow them to see the login page ;)

  15. return true;

  16. }

  17. } else {

  18. if (log.isTraceEnabled()) {

  19. log.trace("Attempting to access a path which requires authentication. Forwarding to the " +

  20. "Authentication url [" + getLoginUrl() + "]");

  21. }

  22. // 所以执行该方法

  23. saveRequestAndRedirectToLogin(request, response);

  24. return false;

  25. }

  26. }

  27.  
  28. protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {

  29. // 将request对象保存在session中,以便登录成功后重新转至上次访问的URL

  30. saveRequest(request);

  31. // 重定向至登录页面,即:"/login.jsp"

  32. redirectToLogin(request, response);

  33. }


根据配置,访问URL:"/login.jsp"时也会应用上FormAuthenticationFilter,由于是重定向所以发起的是GET请求,所以isLoginSubmission()返回false,所以没有执行executeLogin方法,所以能够访问/login.jsp页面。在登录表单中应该设置action="",这样登录请求会提交至/login.jsp,这时为POST请求,所以会执行executeLogin方法:

 
  1. protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {

  2. // 根据表单填写的用户名密码创建AuthenticationToken

  3. AuthenticationToken token = createToken(request, response);

  4. if (token == null) {

  5. String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +

  6. "must be created in order to execute a login attempt.";

  7. throw new IllegalStateException(msg);

  8. }

  9. try {

  10. // 获取Subject对象

  11. Subject subject = getSubject(request, response);

  12. // 执行Subject.login方法进行登录

  13. subject.login(token);

  14. // 如果登录成功,重定向至上次访问的URL

  15. return onLoginSuccess(token, subject, request, response);

  16. } catch (AuthenticationException e) {

  17. // 如果登录失败,则设置错误信息至request,并重新返回登录页面

  18. return onLoginFailure(token, e, request, response);

  19. }

  20. }

  21.  
  22. protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,

  23. ServletRequest request, ServletResponse response) throws Exception {

  24. // 重定向至上次访问的URL

  25. issueSuccessRedirect(request, response);

  26. // 由于返回false,所以过滤器链不再执行

  27. return false;

  28. }

  29.  
  30. protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,

  31. ServletRequest request, ServletResponse response) {

  32. // 设置错误信息至request

  33. setFailureAttribute(request, e);

  34. // 由于返回true,所以过滤器链继续执行,所以又返回了登录页面

  35. return true;

  36. }


至此,认证流程大致流程就是这样了,限于篇幅,登录的流程具体,请期待下篇博文。

猜你喜欢

转载自blog.csdn.net/m0_38053538/article/details/81282204