Shiro源码分析③ :认证流程

一、前言

由于之前没有使用过 Shiro,最近开始使用,故对其部分流程和源码进行了阅读,大体总结了一些内容记录下来。本系列并不会完完全全分析 Shiro 的全部代码,仅把主(我)要(用)流(到)程(的) 简单分析一下。由于本系列大部分为个人内容理解 并且 个人学艺实属不精,故难免出现 “冤假错乱”。如有发现,感谢指正,不胜感激。


Shiro 源码分析全集:

  1. Shiro源码分析① :简单项目搭建
  2. Shiro源码分析② :AbstractShiroFilter
  3. Shiro源码分析③ :认证流程
  4. Shiro源码分析④ :鉴权流程

当我们 进行 http://localhost:8081/shiro/login?userName=张三&password=123456 请求时,首先会经过 AbstractShiroFilter 过滤器,在经过 AbstractShiroFilter 过滤器之后,就到达我们登录请求的实质请求了。

二、认证流程

如下,登录接口如下:

    @PostMapping("login")
    public String login() {
    
    
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("张三", "123456");
        Subject subject = SecurityUtils.getSubject();
        subject.login(usernamePasswordToken);
        return "login";
    }

我们直接来看 subject.login(usernamePasswordToken);。其代码实现在 DelegatingSubject#login,详细代码如下:

    public void login(AuthenticationToken token) throws AuthenticationException {
    
    
        // 1. 清空之前会话缓存
        clearRunAsIdentitiesInternal();
        // 2. 交由安全管理器来进行验证操作,通过这一步,则说明验证通过了
        Subject subject = securityManager.login(this, token);

        PrincipalCollection principals;

        String host = null;
		// 获取 Subject 中的的 principals 
        if (subject instanceof DelegatingSubject) {
    
    
            DelegatingSubject delegating = (DelegatingSubject) subject;
            //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
            principals = delegating.principals;
            host = delegating.host;
        } else {
    
    
            principals = subject.getPrincipals();
        }

        if (principals == null || principals.isEmpty()) {
    
    
          //  	... 抛出异常
        }
        // 记录下 principals, 并将 authenticated  = true,表示当前 Subject 验证通过
     
        this.principals = principals;
        this.authenticated = true;
        // 针对 HostAuthenticationToken 保存其 host
        if (token instanceof HostAuthenticationToken) {
    
    
            host = ((HostAuthenticationToken) token).getHost();
        }
        if (host != null) {
    
    
            this.host = host;
        }
        // 获取session
        Session session = subject.getSession(false);
        if (session != null) {
    
    
        	// 装饰 session, 将 session 包装成  StoppingAwareProxiedSession 类型
            this.session = decorate(session);
        } else {
    
    
            this.session = null;
        }
    }

1. clearRunAsIdentitiesInternal();

在执行方法前,将之前缓存的用户信息清空,因为要重新验证。如果存在缓存则清除,否则什么也不做。

	// org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY
    private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
            DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";
            
    private void clearRunAsIdentities() {
    
    
        Session session = getSession(false);
        if (session != null) {
    
    
        	// 清除 session 中的 org.apache.shiro.subject.support.DelegatingSubject.RUN_AS_PRINCIPALS_SESSION_KEY 属性
            session.removeAttribute(RUN_AS_PRINCIPALS_SESSION_KEY);
        }
    }

2. securityManager.login(this, token);

这里是整个登录验证的核心逻辑,这一步如果结束,则说明验证通过。securityManager.login(this, token); 的具体实现在 DefaultSecurityManager#login 中 。详细代码如下 :

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    
    
        AuthenticationInfo info;
        try {
    
    
        	// 1. 进行验证
            info = authenticate(token);
        } catch (AuthenticationException ae) {
    
    
            try {
    
    
            	// 登录错误时的回调。可供扩展。默认是针对 RememberMeManager 的操作,登录失败则使之前的记住状态失效
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
    
    
               // ... 异常日志
            }
            throw ae; //propagate
        }
		// 2. 创建 subject
        Subject loggedIn = createSubject(token, info, subject);
		// 登录成功时的回调。基本同错误时相同
        onSuccessfulLogin(token, info, loggedIn);

        return loggedIn;
    }

上面的代码还是比较简洁的,下面我们分步解析内容

2.1 authenticate(token);

authenticate(token); 中会调用 this.authenticator.authenticate(token);,我们这里直接来看 this.authenticator.authenticate(token); 的具体实现 AbstractAuthenticator#authenticate

 	public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    
    
		// ... 日志异常校验
        AuthenticationInfo info;
        try {
    
    
        	// 带do的方法才是真正做事的方法,这里开始了验证工作
            info = doAuthenticate(token);
            // 验证返回的是null则说明验证失败
            if (info == null) {
    
    
        	// ...抛出异常
            }
        } catch (Throwable t) {
    
    
            AuthenticationException ae = null;
            if (t instanceof AuthenticationException) {
    
    
                ae = (AuthenticationException) t;
            }
            if (ae == null) {
    
    
             // ...抛出异常
            }
            try {
    
    
            	// 通知 AuthenticationListener 监听器验证失败
                notifyFailure(token, ae);
            } catch (Throwable t2) {
    
    
              	// ...抛出异常
            }
            throw ae;
        }

		... 
		// 通知 AuthenticationListener 监听器验证成功
        notifySuccess(token, info);

        return info;
    }

	...
	
	// doAuthenticate 的实现在其子类 ModularRealmAuthenticator#doAuthenticate
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        assertRealmsConfigured();
        // 获取 所有的 Realm
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
    
    
        	// 如果只有一个,单独处理即可
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
    
    
        	// 多个则按照多个处理的策略
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }
	
	...    
		// 单一Realm处理 
	protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
    
    
		// 如果当前Realm不支持 当前token,则抛出异常
        if (!realm.supports(token)) {
    
    
         	//  ... 抛出异常
        }
        // 调用 realm 的 getAuthenticationInfo 方法,这里可以看到,如果 getAuthenticationInfo 方法返回为null会直接抛出异常,下面详解
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
    
    
          	//  ... 抛出异常
        }
        return info;
    }

...
	// 多Realm的处理
	 protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    
    
		// 获取认证策略
        AuthenticationStrategy strategy = getAuthenticationStrategy();
		// 在所有Realm进行前 进行回调
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);

        for (Realm realm : realms) {
    
    
			// 在每个Realm进行前 进行回调
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
			
            if (realm.supports(token)) {
    
    
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
    
    
					// 调用 realm 的 getAuthenticationInfo 方法,下面详解
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable throwable) {
    
    
                    t = throwable;
                   
                }
				// 在每个Realm进行后 进行回调
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);

            } else {
    
    
               // ... 打印日志
            }
        }
        // 在所有Realm进行后 进行回调
        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }

这里我们可以看到,针对单一Realm 和 多Realm, Shiro使用了不同的处理方法。

上面代码中,我们看到关键的核心一句就是 realm.getAuthenticationInfo(token),其实现AuthenticatingRealm#getAuthenticationInfo 如下:

	public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
		// 1. 获取缓存的 AuthenticationInfo  信息,如果没有缓存,则返回null。解析成功将在第三步将其缓存
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
    
    
            //otherwise not cached, perform the lookup:
            // 2. 调用我们自己定义的逻辑处理 重新处理一遍
            info = doGetAuthenticationInfo(token);
            if (token != null && info != null) {
    
    
            	// 3. 解析结束如果 AuthenticationInfo 不为空则进行结果缓存
                cacheAuthenticationInfoIfPossible(token, info);
            }
        } else {
    
    
      		// ... 日志打印
        }

        if (info != null) {
    
    
        	// 调用 CredentialsMatcher 进行密码校验
            assertCredentialsMatch(token, info);
        } else {
    
    
            log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
        }

        return info;
    }

	... 
	// 进行凭证验证
 	protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    
    
 		// 获取指定的 CredentialsMatcher (凭证匹配器)
        CredentialsMatcher cm = getCredentialsMatcher();
        if (cm != null) {
    
    
        	// 调用 指定的凭证匹配器进行匹配
            if (!cm.doCredentialsMatch(token, info)) {
    
    
              // 抛出异常
            }
        } else {
    
    
          // ... 抛出异常
        }
    }


2.2 createSubject(token, info, subject);

当登录验证结束后,会返回 AuthenticationInfo,我们则需要根据 AuthenticationInfo 来创建一个新的Subject。在创建这个Subject的时候,会创建一个Session,同时将 principals 和 authenticated (记录当前Session的会话状态已经通过认证,即为true) 状态会被保存到Session 中。


createSubject(token, info, subject); 其详细实现在DefaultSecurityManager#createSubject(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo, org.apache.shiro.subject.Subject)中,代码如下:

	// 创建 Subject
 	protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
    
    
 		// 根据不同的 SecurityManager 可能创建 DefaultSubjectContext 和  DefaultWebSubjectContext 。
 		// 这里由于是  DefaultWebSecurityManager 所以创建的类型是 DefaultWebSubjectContext
        SubjectContext context = createSubjectContext();
        // 设置标识,表明已经认证通过,在  save(subject); 中 mergeAuthenticationState(subject); 保存的就是该状态,表明验证通过,在之后的请求中会使用到
        context.setAuthenticated(true);
        // 将token保存到上下文中
        context.setAuthenticationToken(token);
        // 将认证的返回信息保存到上下文中
        context.setAuthenticationInfo(info);
        if (existing != null) {
    
    
        	// 将 Subject 信息保存到上下文中
            context.setSubject(existing);
        }
        // 开始创建Subject
        return createSubject(context);
    }

	...	
	// 这里的方法和 AbstractShiroFilter 文章中介绍的相同,这里就不会详细介绍了,会着重说明两处创建Subject的不同
    public Subject createSubject(SubjectContext subjectContext) {
    
    
        //create a copy so we don't modify the argument's backing map:
        // 拷贝一个  SubjectContext  副本
        SubjectContext context = copy(subjectContext);

        //ensure that the context has a SecurityManager instance, and if not, add one:
        // 1. 确保上下文具有SecurityManager实例,如果没有,添加一个
        context = ensureSecurityManager(context);
		// 2. 解析session。
        context = resolveSession(context);
		// 3. 解析 Principals
        context = resolvePrincipals(context);
		// 4. 创建 一个全新的 Subject
        Subject subject = doCreateSubject(context);
		// 5. 保存(缓存) subject 。与AbstractShiroFilter 不同的是,这里会去创建Session,并会写入Cookies 中
        save(subject);

        return subject;
    }

AbstractShiroFilter 中也会调用 createSubject(SubjectContext subjectContext) 来创建Subject。
二者的不同之处在于:AbstractShiroFilter 在调用时目的是为了创建一个Subject来绑定当前线程,如果存在Session,则将Session中缓存的信息提取出来,填充至Subject,如果不存在Session,并不会主动创建Session,而是直接返回Subject。
而在认证流程,当认证成功后,这里会将认证后的信息缓存到Session中,如果Session存在,则进行信息合并,如果Session不存在,则会创建一个Session,再将信息进行缓存。
总的来说,AbstractShiroFilter 会从Session中读取数据,认证流程会向Session中写入数据。

关于 createSubject(context); 内容的具体讲解,请参考 Shiro源码分析② :AbstractShiroFilter

至此,整个登录认证流程就已经结束了。


简单总结整个登录过程:

  1. http://localhost:8081/shiro/login?userName=张三&password=123456 请求发送过来时,会先经过 AbstractShiroFilter 。AbstractShiroFilter 会创建一个Subject绑定当前线程,同时会将 该请求分发给合适的过滤器,我们这里是分发给 AnonymousFilter,也就是直接放行。
  2. 经过 AbstractShiroFilter 之后,我们直接来到了 ShiroDemoController#login 方法,这里面直接调用了 subject.login(usernamePasswordToken);。而在这其中,会调用我们自定义的 Realm 中的认证方法来进行认证。如果认证通过,则会创建Session,将 principalsauthenticated 状态会被保存到Session 中。

以上:内容部分参考网络
https://blog.csdn.net/dgh112233/article/details/100083287
https://www.zhihu.com/pin/1105962164963282944
http://www.muzhuangnet.com/show/771.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

猜你喜欢

转载自blog.csdn.net/qq_36882793/article/details/113344923