Shiro source code analysis ③: certification process

I. Introduction

Since I haven't used Shiro before, I started using it recently, so I read some of its processes and source code, and summarized some of the content and recorded it. This series will not completely analyze all Shiro's code, but only briefly analyze the main (I) need (use) flow (to) process (of) . Since most of this series is for personal understanding and personal learning skills are not good, it is inevitable that there will be "unjust and false confusion". If you find anything, thank you for your corrections. I am very grateful.


Shiro source code analysis complete works:

  1. Shiro source code analysis①: Simple project construction
  2. Shiro source code analysis ②: AbstractShiroFilter
  3. Shiro source code analysis ③: certification process
  4. Shiro source code analysis ④: authentication process

When we http://localhost:8081/shiro/login?userName=张三&password=123456request will first go through AbstractShiroFilter filter, after AbstractShiroFilter filter, we will reach the login request of the substance of the request.

Second, the certification process

As follows, the login interface is as follows:

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

Let's take a look directly subject.login(usernamePasswordToken);. The code is implemented in DelegatingSubject#login, and the detailed code is as follows:

    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();

Before executing the method, clear the previously cached user information because it needs to be re-verified. If the cache exists, clear it, otherwise do nothing.

	// 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);

Here is the core logic of the entire login verification. If this step ends, the verification is passed. securityManager.login(this, token);The concrete realization DefaultSecurityManager#loginin. The detailed code is as follows:

    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;
    }

The above code is quite concise, let’s analyze the content step by step

2.1 authenticate(token);

authenticate(token);Will call this.authenticator.authenticate(token);, we are here to look at the direct this.authenticator.authenticate(token);realization of 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;
    }

Here we can see that Shiro uses different processing methods for single Realm and multiple Realm.

In the above code, we see that the key core sentence is realm.getAuthenticationInfo(token)that its implementation is AuthenticatingRealm#getAuthenticationInfoas follows:

	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);

When the login verification is over, AuthenticationInfo will be returned, and we need to create a new Subject based on AuthenticationInfo. When creating this Subject, a Session will be created, and the principals and authenticated (recording that the session state of the current Session has been authenticated, which is true) will be saved in the Session.


createSubject(token, info, subject);The detailed implementation is DefaultSecurityManager#createSubject(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo, org.apache.shiro.subject.Subject)in, the code is as follows:

	// 创建 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;
    }

In AbstractShiroFilterthe call will be createSubject(SubjectContext subjectContext)to create the Subject.
The difference between the two is: AbstractShiroFilter is called to create a Subject to bind the current thread. If there is a Session, it will extract the information cached in the Session and fill it into the Subject. If there is no Session, it will not Actively create Session, but return to Subject directly.
In the authentication process, when the authentication is successful, the authenticated information will be cached in the Session. If the Session exists, the information will be merged. If the Session does not exist, a Session will be created and the information will be cached.
In general, AbstractShiroFilter will read data from the Session, and the authentication process will write data to the Session.

For a detailed explanation of the content of createSubject(context);, please refer to Shiro source code analysis ②: AbstractShiroFilter

At this point, the entire login authentication process has ended.


A brief summary of the entire login process:

  1. http://localhost:8081/shiro/login?userName=张三&password=123456When a request is sent, it will go through first AbstractShiroFilter. AbstractShiroFilter will create a Subject to bind the current thread, and will distribute the request to the appropriate filter. Here we distribute it to AnonymousFilter, that is, let it go directly.
  2. After AbstractShiroFilterthen, we came directly ShiroDemoController#loginmethod, there is a direct call subject.login(usernamePasswordToken);. Among them, the authentication method in our custom Realm will be called for authentication. If the authentication is created Session, will principals, and authenticatedthe state will be saved to the Session.

Above: The content part refers to the network
https://blog.csdn.net/dgh112233/article/details/100083287
https://www.zhihu.com/pin/1105962164963282944
http://www.muzhuangnet.com/show/771.
If the html is intrusive, please contact to delete it. The content is only used for self-record learning. If there is any error, please correct me

Guess you like

Origin blog.csdn.net/qq_36882793/article/details/113344923