• 详解Shiro认证流程


    详解Shiro认证流程

    通过前面对shiro源码的解读,我们知道,只要我们在拦截器里面配置了所有请求都经过FormAuthenticationFilter,那么我们就不用自己写login方法,shiro会自己帮我们处理登录逻辑。

    isAccessAllowed

    shiro中的判断一个请求是否允许通过的条件是当前得到的subject是否已经认证过,或者当前请求是否是登录请求。
    如何判断Subject是否认证过?

    org.apache.shiro.subject.support.DelegatingSubject#isAuthenticated
    public boolean isAuthenticated() {
      return authenticated;
     }
    
    • 1
    • 2
    • 3
    • 4

    接来下依次追寻subject.isAuthenticated的结果:

    Subject在如何得到?

    org.apache.shiro.SecurityUtils#getSubject

        public static Subject getSubject() {
            Subject subject = ThreadContext.getSubject();
            if (subject == null) {
                subject = (new Subject.Builder()).buildSubject();
                ThreadContext.bind(subject);
            }
            return subject;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    从当前线程的上下文中获取,如果没取到就用Builder创建一个,然后把subject绑定到线程上下文。那么取的过程就可以不看了,看下build过程。
    org.apache.shiro.subject.Subject.Builder#Builder()

    public static class Builder {
    	private final SubjectContext subjectContext;
    	private final SecurityManager securityManager;
    	public Builder() {
    		this(SecurityUtils.getSecurityManager());
    	}
        public Builder(SecurityManager securityManager) {
           if (securityManager == null) {
               throw new NullPointerException("SecurityManager method argument cannot be null.");
           }
           this.securityManager = securityManager;
           this.subjectContext = newSubjectContextInstance();
           if (this.subjectContext == null) {
               throw new IllegalStateException("Subject instance returned from 'newSubjectContextInstance' " +
                       "cannot be null.");
           }
           this.subjectContext.setSecurityManager(securityManager);
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    org.apache.shiro.subject.Subject.Builder#buildSubject

    public Subject buildSubject() {
    	return this.securityManager.createSubject(this.subjectContext);
    }
    
    • 1
    • 2
    • 3

    shiro中的createSubject的实现只有org.apache.shiro.mgt.DefaultSecurityManager#createSubject(org.apache.shiro.subject.SubjectContext)

        public Subject createSubject(SubjectContext subjectContext) {
            //create a copy so we don't modify the argument's backing map:
            SubjectContext context = copy(subjectContext);
    
            //ensure that the context has a SecurityManager instance, and if not, add one:
            context = ensureSecurityManager(context);
    
            //解析关联的session(通常基于引用的session ID),并将其放置在之前的上下文中发送到SubjectFactory。
            //SubjectFactory不需要知道如何获取session作为过程通常是特定于环境的-更好地保护SF不受以下细节的影响:
            context = resolveSession(context);
    
            //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
            //if possible before handing off to the SubjectFactory:
            context = resolvePrincipals(context);
    
            Subject subject = doCreateSubject(context);
    
            //如有必要,请保存此subject以备将来参考:(此处需要此subject,以防rememberMe principals 已解决,
            //并且需要将其存储在session中,因此我们不会在每次操作中不断地对rememberMe PrincipalCollection进行rehydrate)。
            //在1.2中添加的;
            save(subject);
    
            return subject;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    重点看下resolveSession、doCreateSubject和save(subject)三个部分:

    resolveSession

    org.apache.shiro.mgt.DefaultSecurityManager#resolveSession

        protected SubjectContext resolveSession(SubjectContext context) {
            if (context.resolveSession() != null) {
                log.debug("Context already contains a session.  Returning.");
                return context;
            }
            try {
                //Context couldn't resolve it directly, let's see if we can since we have direct access to 
                //the session manager:
                Session session = resolveContextSession(context);
                if (session != null) {
                    context.setSession(session);
                }
            } catch (InvalidSessionException e) {
                log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
                        "(session-less) Subject instance.", e);
            }
            return context;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这里太细了就不贴代码了,大意就是从当前的上下文取session,没取到就通过上下文来生成session,然后设置到上下文中,debug看了下,这个地方的session最开始都是用HttpServletSession包装来的。所以session跟是从客户端拿来的。

    doCreateSubject

    org.apache.shiro.mgt.DefaultSecurityManager#doCreateSubject

    protected Subject doCreateSubject(SubjectContext context) {
       return getSubjectFactory().createSubject(context);
    }
    
    • 1
    • 2
    • 3

    org.apache.shiro.web.mgt.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();
            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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这个地方其实可以看到subject的创建过程也没干什么,就是上下文中获取认证信息,session等相关配置来创建一个委派对象WebDelegatingSubject,所以我们在shiro里面看到的subject都是WebDelegatingSubject。

    save(Subject subject)

    org.apache.shiro.mgt.DefaultSecurityManager#save

        protected void save(Subject subject) {
            this.subjectDAO.save(subject);
        }
    
    • 1
    • 2
    • 3

    org.apache.shiro.mgt.DefaultSubjectDAO#save

        public Subject save(Subject subject) {
            if (isSessionStorageEnabled(subject)) {
                saveToSession(subject);
            } else {
                log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " +
                        "authentication state are expected to be initialized on every request or invocation.", subject);
            }
    
            return subject;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    save过程就是把subject里面的Principals和认证状态缓存到session的过程。

    isAuthenticated

    这里我们回归下主题,我们是想看subject.authenticated 是在什么时候赋值的。所以我们先看看authenticated 的判断:
    org.apache.shiro.subject.support.DefaultSubjectContext#resolveAuthenticated

        public boolean resolveAuthenticated() {
            Boolean authc = getTypedValue(AUTHENTICATED, Boolean.class);
            if (authc == null) {
                //see if there is an AuthenticationInfo object.  If so, the very presence of one indicates a successful
                //authentication attempt:
                AuthenticationInfo info = getAuthenticationInfo();
                authc = info != null;
            }
            if (!authc) {
                //fall back to a session check:
                Session session = resolveSession();
                if (session != null) {
                    Boolean sessionAuthc = (Boolean) session.getAttribute(AUTHENTICATED_SESSION_KEY);
                    authc = sessionAuthc != null && sessionAuthc;
                }
            }
    
            return authc;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    从上下文里面拿的,先从上下文拿AUTHENTICATED,再拿AUTHENTICATION_INFO,如果两个都没有,那就从session里面拿AUTHENTICATED_SESSION_KEY,只要有一个拿到了就算已经认证过了,authenticated的赋值也是这个时候赋值的。

    onAccessDenied

    isAccessAllowed的结果是false后,就将执行onAccessDenied方法。我们仍然研究FormAuthenticationFilter的onAccessDenied,看他是怎么处理的。

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    	if (isLoginRequest(request, response)) {
    	    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;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    很简单,如果是login请求就执行login操作,如果不是就直接重定向到login页面。

    执行登录

        protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
           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 = getSubject(request, response);
               subject.login(token);
               return onLoginSuccess(token, subject, request, response);
           } catch (AuthenticationException e) {
               return onLoginFailure(token, e, request, response);
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    从org.apache.shiro.web.filter.authc.AuthenticatingFilter#executeLogin的源码来看,shiro的执行登录的过程大致分成下面这4步:

    1. 创建token
    2. 获取subject
    3. subject.login
    4. 登录结果的回调

    创建token

        protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
            String username = getUsername(request);
            String password = getPassword(request);
            return createToken(username, password, request, response);
        }
        protected String getUsername(ServletRequest request) {
        	return WebUtils.getCleanParam(request, getUsernameParam());
        }
        protected String getPassword(ServletRequest request) {
         	return WebUtils.getCleanParam(request, getPasswordParam());
        }
        protected AuthenticationToken createToken(String username, String password,
                                               ServletRequest request, ServletResponse response) {
    	     boolean rememberMe = isRememberMe(request);
    	     String host = getHost(request);
    	     return createToken(username, password, rememberMe, host);
        }
        protected AuthenticationToken createToken(String username, String password,
                                                boolean rememberMe, String host) {
          	return new UsernamePasswordToken(username, password, rememberMe, host);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    从上面贴出的源码看,shiro通过配置的username和password参数名从request中获取到对应的username和password来构建了一个简单的UsernamePasswordToken。

    获取subject

        protected Subject getSubject(ServletRequest request, ServletResponse response) {
            return SecurityUtils.getSubject();
        }
         public static Subject getSubject() {
            Subject subject = ThreadContext.getSubject();
            if (subject == null) {
                subject = (new Subject.Builder()).buildSubject();
                ThreadContext.bind(subject);
            }
            return subject;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看到这个时候获取的subject其实在一开始就已经创建好了,这个地方只是从线程的上下文中取出来就好了。

    subject.login

        public void login(AuthenticationToken token) throws AuthenticationException {
            clearRunAsIdentitiesInternal();
            Subject subject = securityManager.login(this, token);
    
            PrincipalCollection principals;
    
            String host = null;
    
            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()) {
                String msg = "Principals returned from securityManager.login( token ) returned a null or " +
                        "empty value.  This value must be non null and populated with one or more elements.";
                throw new IllegalStateException(msg);
            }
            this.principals = principals;
            this.authenticated = true;
            if (token instanceof HostAuthenticationToken) {
                host = ((HostAuthenticationToken) token).getHost();
            }
            if (host != null) {
                this.host = host;
            }
            Session session = subject.getSession(false);
            if (session != null) {
                this.session = decorate(session);
            } else {
                this.session = null;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    看下securityManager.login:

        public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
            AuthenticationInfo info;
            try {
                info = authenticate(token);
            } catch (AuthenticationException ae) {
                try {
                    onFailedLogin(token, ae, subject);
                } catch (Exception e) {
                    if (log.isInfoEnabled()) {
                        log.info("onFailedLogin method threw an " +
                                "exception.  Logging and propagating original AuthenticationException.", e);
                    }
                }
                throw ae; //propagate
            }
    
            Subject loggedIn = createSubject(token, info, subject);
    
            onSuccessfulLogin(token, info, loggedIn);
    
            return loggedIn;
        }
          public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
            return this.authenticator.authenticate(token);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    SecurityManager首先需要从authenticator中去认证token,并且返回一个AuthenticationInfo,所以我们登录的用户凭证校验其实是交个了这个authenticator;
    从SecurityManager的构造方法里面可以知道,authenticator是一个ModularRealmAuthenticator实例。

        public AuthenticatingSecurityManager() {
            super();
            this.authenticator = new ModularRealmAuthenticator();
        }
    
    • 1
    • 2
    • 3
    • 4

    关于ModularRealmAuthenticator这里就不贴代码了。简单描述下就是ModularRealmAuthenticator有一个Realm集合,默认情况下AuthenticationStrategy需要至少一个Realm。
    看下authenticator的authenticate(token)实现:
    org.apache.shiro.authc.AbstractAuthenticator#authenticate

        public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    
            if (token == null) {
                throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
            }
    
            log.trace("Authentication attempt received for token [{}]", token);
    
            AuthenticationInfo info;
            try {
                info = doAuthenticate(token);
                if (info == null) {
                    String msg = "No account information found for authentication token [" + token + "] by this " +
                            "Authenticator instance.  Please check that it is configured correctly.";
                    throw new AuthenticationException(msg);
                }
            } catch (Throwable t) {
                AuthenticationException ae = null;
                if (t instanceof AuthenticationException) {
                    ae = (AuthenticationException) t;
                }
                if (ae == null) {
                    //Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more
                    //severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate:
                    String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +
                            "error? (Typical or expected login exceptions should extend from AuthenticationException).";
                    ae = new AuthenticationException(msg, t);
                    if (log.isWarnEnabled())
                        log.warn(msg, t);
                }
                try {
                    notifyFailure(token, ae);
                } catch (Throwable t2) {
                    if (log.isWarnEnabled()) {
                        String msg = "Unable to send notification for failed authentication attempt - listener error?.  " +
                                "Please check your AuthenticationListener implementation(s).  Logging sending exception " +
                                "and propagating original AuthenticationException instead...";
                        log.warn(msg, t2);
                    }
                }
    
    
                throw ae;
            }
    
            log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
    
            notifySuccess(token, info);
    
            return info;
        }
        protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
            assertRealmsConfigured();
            Collection realms = getRealms();
            if (realms.size() == 1) {
                return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
            } else {
                return doMultiRealmAuthentication(realms, authenticationToken);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    1. 要求token不能为空
    2. authenticator里面的Realm取出来对token进行认证。
    3. 如果Realm只有一个,那么就直接进行认证即可。否则进行多个Realm的逻辑处理,我们一般情况下都是单个Realm,所以就不过多分析多Realm的情况了。

    Realm认证:
    org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo

        public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
            AuthenticationInfo info = getCachedAuthenticationInfo(token);
            if (info == null) {
                //otherwise not cached, perform the lookup:
                info = doGetAuthenticationInfo(token);
                log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
                if (token != null && info != null) {
                    cacheAuthenticationInfoIfPossible(token, info);
                }
            } else {
                log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
            }
    
            if (info != null) {
                assertCredentialsMatch(token, info);
            } else {
                log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}].  Returning null.", token);
            }
    
            return info;
        }
         private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) {
            AuthenticationInfo info = null;
    
            Cache cache = getAvailableAuthenticationCache();
            if (cache != null && token != null) {
                log.trace("Attempting to retrieve the AuthenticationInfo from cache.");
                Object key = getAuthenticationCacheKey(token);
                info = cache.get(key);
                if (info == null) {
                    log.trace("No AuthorizationInfo found in cache for key [{}]", key);
                } else {
                    log.trace("Found cached AuthorizationInfo for key [{}]", key);
                }
            }
    
            return info;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    首先会从缓存里面获取AuthenticationInfo,如果缓存中没有,就会执行doGetAuthenticationInfo来获取。而这个方法就是我们自己实现realm的时候需要实现的方法。
    得到AuthenticationInfo后会进行缓存(如果需要的话,通过org.apache.shiro.realm.AuthenticatingRealm#isAuthenticationCachingEnabled(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo)控制。)。
    得到的AuthenticationInfo还会通过org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch与传进来的token一起进行校验,只有校验通过后才会成功返回。

        protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
            CredentialsMatcher cm = getCredentialsMatcher();
            if (cm != null) {
                if (!cm.doCredentialsMatch(token, info)) {
                    //not successful - throw an exception to indicate this:
                    String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
                    throw new IncorrectCredentialsException(msg);
                }
            } else {
                throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " +
                        "credentials during authentication.  If you do not wish for credentials to be examined, you " +
                        "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看到CredentialsMatcher是配置到Realm中的。

    登录后的回调

    1.authenticatorh会将登录结果告知各监听者。

    SecurityManager会做一些rememberMe的登录成功或失败的回调。同时会用当前的token和info创建一个新的Subject返回。

    如果登录成功,subject会记录principals并把authenticated标记为true,如果能获取到session还会对session做部分调整。

    最后就是执行org.apache.shiro.web.filter.authc.AuthenticatingFilter的成功和失败回调。

  • 相关阅读:
    拥抱Spring全新OAuth解决方案
    实在智能牵手埃林哲,“TARS-RPA-Agent+云时通”双剑合璧共推企业数字化转型
    命令行工具集合busybox编译
    JAVA 笔试面试题(一)
    IDEA JAVA项目 导入JAR包,打JAR包 和 JAVA运行JAR命令提示没有主清单属性
    malloc和new的本质区别
    在线实时监测离子风机的功能
    Spring源码:SpringBean 的注册-XML源码解析
    2022年全球市场聚碳酸酯天窗玻璃总体规模、主要生产商、主要地区、产品和应用细分研究报告
    你不知道的下划线属性-text-decoration
  • 原文地址:https://blog.csdn.net/jiey0407/article/details/126617181