• Shiro的认证原理(Subject#login的背后故事)


    登录操作一般都是我们触发的:

    Subject subject = SecurityUtils.getSubject();
    AuthenticationToken authenticationToken = new ...
    subject.login(authenticationToken);

    Subject的登录将委托给SecurityManager,SecurityManager的login方法实际上是产生了一个新的Subject,然后将相关属性赋予当前调用者Subject:

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

    DefaultSecurityManager实现了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;
    }

    image

    父类AuthenticatingSecurityManager实现authenticate方法:

    public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
    
        private Authenticator authenticator;
    
        public AuthenticatingSecurityManager() {
            super();
            this.authenticator = new ModularRealmAuthenticator();
        }
    
        public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
            return this.authenticator.authenticate(token);
        }
    
        //......
    }

    利用一个ModularRealmAuthenticator类型的authenticator来实现:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

    然后根据realms集合是单个还是多个分别处理,最终无非是这样:

    AuthenticationInfo info = realm.getAuthenticationInfo(token);

    父类AuthenticatingRealm的getAuthenticationInfo方法实现了info的获取和身份的校验,仅仅调用自己实现的realm的doGetAuthenticationInfo方法,初步验证后构造一个SimpleAuthenticationInfo:

    SimplePrincipalCollection principalCollection = new SimplePrincipalCollection(principals, this.getName());
    return new SimpleAuthenticationInfo(principalCollection, authenticationInfo.getCredentials());

    AuthenticatingRealm的getAuthenticationInfo方法逻辑如下:

    首先去缓存找info:

    AuthenticationInfo info = getCachedAuthenticationInfo(token);

    缓存没有则调用子类实现的方法:

    info = doGetAuthenticationInfo(token);

    info不为null的时候就要验证了(这里还可以加密验证):

    assertCredentialsMatch(token, info);

    两次创建Subject

    进入AbstractShiroFilter的时候,会默认创建一个Subject,这个是在Subject接口中的内部类实现的,但是同样也是调用了DefaultSecurityManager中的createSubject方法:

    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);
    
        //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
        //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
        //process is often environment specific - better to shield the SF from these details:
        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);
    
        //save this subject for future reference if necessary:
        //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
        //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
        //Added in 1.2:
        save(subject);
    
        return subject;
    }

    初次进入shiroFilter,会创建一个Subject,这个Subject没有验证通过,保留了三个属性:request,response,securityManager。

    当我们调用subject.login的时候,我们在DefaultSecurityManager中为subjectContext设置了相关属性:

    protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
        SubjectContext context = createSubjectContext();
        context.setAuthenticated(true);
        context.setAuthenticationToken(token);
        context.setAuthenticationInfo(info);
        if (existing != null) {
            context.setSubject(existing);
        }
        // 这个方法的具体实现在上面
        return createSubject(context);
    }

    一个save方法为我们构造了session:

    save(subject);

    这个session是根据我们的principals(放在登录成功返回的那个AuthenticationInfo中)构造的。

    下一次进入的时候,依然是:

    final Subject subject = createSubject(request, response);

    但是下面的方法为我们找回了session,通过request.getSession(false)就可以取到。

    context = resolveSession(context);

    有了session后其他的东西都可以恢复,这样就可以识别并维持一个subject的状态,即使每次都重新创建了Subject对象。

    具体是在DefaultWebSubjectFactory这个方法里恢复的,并且通过构造器赋值给下一个新的subject了:

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

    举两个例子:

    PrincipalCollection principals = wsc.resolvePrincipals();
    -> principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);
    
    boolean authenticated = wsc.resolveAuthenticated();
    -> Session session = resolveSession();
    if (session != null) {
        Boolean sessionAuthc = (Boolean) session.getAttribute(AUTHENTICATED_SESSION_KEY);
        authc = sessionAuthc != null && sessionAuthc;
    }

    通过上面两个方法就恢复了Subject的两个属性,其实都是存放在session中。

    其实只要你使用了Shiro,不管你是否登录,核心过滤器都会为我们构造Subject实例,当我们主动调用subject.login方法时,会间接调用我们自己实现的realm的doGetAuthenticationInfo,根据我们在数据库中获取的信息(存放在info中)和调用login方法时传递的AuthenticationToken中的信息对比。

    当info为null或者抛出了AuthenticationException异常,都视为登录失败。

  • 相关阅读:
    LinkList
    hadoop记录篇3-namenode水平拓展Federation+viewfs
    hadoop记录篇2-namenode高可用(HA)之QJM+NFS
    hadoop记录篇1-hdfs集群安装
    springcloud记录篇5-zuul路由
    springcloud记录篇4-断路器Circuit Breaker
    springcloud记录篇3-springcloud客户端ribbon和feign
    springboot心得笔记-常用配置
    springboot心得笔记-入门
    springcloud记录篇2-服务注册和发现
  • 原文地址:https://www.cnblogs.com/lucare/p/8679132.html
Copyright © 2020-2023  润新知