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. 当调用登录接口时首先会经过 Shiro 的拦截器 SpringShiroFilter
  2. SpringShiroFilter拦截器会创建一个Subject并绑定当前线程,随后将请求分发给合适的 Filter来处理
  3. 随后请求到达登录接口,调用 SubjectUtils.getSubject 获取 Subject 主体类,这个对象即SpringShiroFilter 中创建的 Subject。
  4. 通过执行 Subject#login 来完成登录验证流程。
  5. Subject#login 会通过 AbstractAuthenticator#authenticate方法 来调用 Realm 验证登录流程,获取到验证信息 AuthenticationInfo,经过 CredentialsMatcher 密码校验通过后返回。
  6. 如果认证通过,通过创建一个新的 Subject 来保存 AuthenticationInfo 信息,并创建一个 Session将 principalsauthenticated 状态会保存到Session 中 (此时principals保存着当前用户的信息,而 authenticated 为true,代表登录验证通过),然后 SessionId 写到 Cookies 中
  7. 开始完成登录后的后置操作。如rememberMe功能的回调。
  8. 至此整个登录功能完成,此时登录验证成功。Session已创建,SessionId 保留到了 Cookies 中。
  9. 当用户访问其他接口时,会创建Subject,随后通过Cookies 中的SessionId 获取到Session,并将 Session 中的信息合并到当前Subject中,此时当前线程的 Subject#authenticated = true, Subject#principals 保存了用户信息。
  10. 随后经过过滤器验证,如指定的是 authc 过滤器,则认为是 FormAuthenticationFilter 来执行此处请求的验证。而 FormAuthenticationFilter 的判断条件是 Subject#authenticated 是否为true。故校验通过。

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

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猫吻鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值