• Apache Shiro 学习记录1


      最近几天在学习Apache Shiro......看了一些大神们的教程.....感觉收获不少.....但是毕竟教程也只是指引一下方向....即使是精品教程,仍然有很多东西都没有说明....所以自己也稍微研究了一下...记录了一下我的研究发现....教程点这里

      这篇教程的最后提到了strategy.....然后给出了4个方法.....但是并没有怎么详细说明.....我想说说我的理解.....(我的理解可能会有很多错误)


      我想先说说登陆验证的大致流程....大致......

    Subject

    从用户那里收集完用户名密码以后我们会调用subject.login(token)这个方法去登陆.....Subject是一个接口,没有定义login的具体实现.....Shiro里只有一个类实现了这个接口,是DelegatingSubject这个类.这个类里的方法login方法如下:

     1    public void login(AuthenticationToken token) throws AuthenticationException {
     2         clearRunAsIdentitiesInternal();
     3         Subject subject = securityManager.login(this, token);
     4 
     5         PrincipalCollection principals;
     6 
     7         String host = null;
     8 
     9         if (subject instanceof DelegatingSubject) {
    10             DelegatingSubject delegating = (DelegatingSubject) subject;
    11             //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
    12             principals = delegating.principals;
    13             host = delegating.host;
    14         } else {
    15             principals = subject.getPrincipals();
    16         }
    17 
    18         if (principals == null || principals.isEmpty()) {
    19             String msg = "Principals returned from securityManager.login( token ) returned a null or " +
    20                     "empty value.  This value must be non null and populated with one or more elements.";
    21             throw new IllegalStateException(msg);
    22         }
    23         this.principals = principals;
    24         this.authenticated = true;
    25         if (token instanceof HostAuthenticationToken) {
    26             host = ((HostAuthenticationToken) token).getHost();
    27         }
    28         if (host != null) {
    29             this.host = host;
    30         }
    31         Session session = subject.getSession(false);
    32         if (session != null) {
    33             this.session = decorate(session);
    34         } else {
    35             this.session = null;
    36         }
    37     }

    代码各种复杂=.= ..............我也看不懂....但是我看到第三行,明白了subject其实也是让securityManager来执行login操作的......那我们去看看securityManager吧......

    SecurityManager

    SecurityManager也是一个接口,各种继承,实现其他接口......还好实现类只有一个DefaultSecurityManager类...并且login方法直到这个类才被实现....

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

    我觉得这里最重要的就是第四行的authenticate方法.......

    这个方法是在DefaultSecurityManager的N层父类AuthenticatingSecurityManager里实现的....

    1     public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    2         return this.authenticator.authenticate(token);
    3     }

    这里又可以看出SecurityManager的login方法其实也是委托给认证器authenticator调用authenticate(token)方法来实现的.

    public abstract class AuthenticatingSecurityManager extends RealmSecurityManager {
    
        /**
         * The internal <code>Authenticator</code> delegate instance that this SecurityManager instance will use
         * to perform all authentication operations.
         */
        private Authenticator authenticator;
    
        /**
         * Default no-arg constructor that initializes its internal
         * <code>authenticator</code> instance to a
         * {@link org.apache.shiro.authc.pam.ModularRealmAuthenticator ModularRealmAuthenticator}.
         */
        public AuthenticatingSecurityManager() {
            super();
            this.authenticator = new ModularRealmAuthenticator();
        }
        ..................
    }

    认证器authenticator的默认实现是ModularRealmAuthenticator....this.authenticator = new ModularRealmAuthenticator();...所以我们再来看看ModularRealmAuthenticator..........

    ModularRealmAuthenticator

    ModularRealmAuthenticator的authenticate方法是继承自父类AbstractAuthenticator的..这个方法还是final的...

     1     public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
     2 
     3         if (token == null) {
     4             throw new IllegalArgumentException("Method argumet (authentication token) cannot be null.");
     5         }
     6 
     7         log.trace("Authentication attempt received for token [{}]", token);
     8 
     9         AuthenticationInfo info;
    10         try {
    11             info = doAuthenticate(token);
    12             if (info == null) {
    13                 String msg = "No account information found for authentication token [" + token + "] by this " +
    14                         "Authenticator instance.  Please check that it is configured correctly.";
    15                 throw new AuthenticationException(msg);
    16             }
    17         } catch (Throwable t) {
    18             AuthenticationException ae = null;
    19             if (t instanceof AuthenticationException) {
    20                 ae = (AuthenticationException) t;
    21             }
    22             if (ae == null) {
    23                 //Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more
    24                 //severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate:
    25                 String msg = "Authentication failed for token submission [" + token + "].  Possible unexpected " +
    26                         "error? (Typical or expected login exceptions should extend from AuthenticationException).";
    27                 ae = new AuthenticationException(msg, t);
    28             }
    29             try {
    30                 notifyFailure(token, ae);
    31             } catch (Throwable t2) {
    32                 if (log.isWarnEnabled()) {
    33                     String msg = "Unable to send notification for failed authentication attempt - listener error?.  " +
    34                             "Please check your AuthenticationListener implementation(s).  Logging sending exception " +
    35                             "and propagating original AuthenticationException instead...";
    36                     log.warn(msg, t2);
    37                 }
    38             }
    39 
    40 
    41             throw ae;
    42         }
    43 
    44         log.debug("Authentication successful for token [{}].  Returned account [{}]", token, info);
    45 
    46         notifySuccess(token, info);
    47 
    48         return info;
    49     }

    从中我们可以看出比较重要的是第11行 info = doAuthenticate(token);

    doAuthenticate(token)方法在ModularRealmAuthenticator之中被override过...

    1     protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    2         assertRealmsConfigured();
    3         Collection<Realm> realms = getRealms();
    4         if (realms.size() == 1) {
    5             return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    6         } else {
    7             return doMultiRealmAuthentication(realms, authenticationToken);
    8         }
    9     }

    这里还是比较明显的,根据定义的Realm数量来决定是调用doSingleRealmAuthentication方法还是调用doMultiRealmAuthentication方法...

    我们来看看doMultiRealmAuthentication方法里到底做了些什么呢..这个方法是定义在ModularRealmAuthenticator里的...

     1     protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
     2 
     3         AuthenticationStrategy strategy = getAuthenticationStrategy();
     4 
     5         AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
     6 
     7         if (log.isTraceEnabled()) {
     8             log.trace("Iterating through {} realms for PAM authentication", realms.size());
     9         }
    10 
    11         for (Realm realm : realms) {
    12 
    13             aggregate = strategy.beforeAttempt(realm, token, aggregate);
    14 
    15             if (realm.supports(token)) {
    16 
    17                 log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
    18 
    19                 AuthenticationInfo info = null;
    20                 Throwable t = null;
    21                 try {
    22                     info = realm.getAuthenticationInfo(token);
    23                 } catch (Throwable throwable) {
    24                     t = throwable;
    25                     if (log.isDebugEnabled()) {
    26                         String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
    27                         log.debug(msg, t);
    28                     }
    29                 }
    30 
    31                 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
    32 
    33             } else {
    34                 log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
    35             }
    36         }
    37 
    38         aggregate = strategy.afterAllAttempts(token, aggregate);
    39 
    40         return aggregate;
    41     }

    虽然很多看不明白....但是我大致流程能看懂:

    在调用每个Realm之前,先执行strategy.beforeAllAttempts(realms, token);

    然后再进入 for (Realm realm : realms),就是准备去调用每个realm来认证..不过每个Realm认证之前还要调用aggregate = strategy.beforeAttempt(realm, token, aggregate);

    然后得到单个Realm的认证结果 info = realm.getAuthenticationInfo(token);

    再调用aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);这里可能会把info的认证信息和aggregate合并,就是把principle合并....不过还是要看具体的strategy的.

    最后再调用 aggregate = strategy.afterAllAttempts(token, aggregate);

    也就是说strategy.beforeAllAttempts(realms, token);调用1次,然后根据realm的数量调用N次strategy.beforeAttempt(realm, token, aggregate);N次realm.getAuthenticationInfo(token);N次 strategy.afterAttempt(realm, token, info, aggregate, t);再调用1次strategy.afterAllAttempts(token, aggregate);

    另外,不是说你单个Realm里认证失败,比如抛出了UnknownAccountException..所有认证就终止了....仔细看第23,24行会发现Realm里抛出的异常是会被拦截下来的...然后传给了 strategy.afterAttempt(realm, token, info, aggregate, t);....发现了这个异常以后到底是算认证失败了还是继续看其他的Realm是根据strategy来的...比如AllSuccessfulStrategy的afterAttempt方法...发现了任意1个realm抛出的异常,就直接抛出异常,终止认证....但是AtLeastOneSuccessfulStrategy就不一样...它就会无视这个异常...继续下面realm来认证...然后根据afterAllAttempts里的AuthenticationInfo的汇总信息来判断到底是认证失败了要抛出异常还是认证是成功的....

    Shiro默认的策略是AtLeastOneSuccessfulStrategy


    各种Strategy

    我觉得看到这里我们可以再来看看Shiro已经定义的三种Strategy了...教程里提到过:

    FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;

    AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,返回所有Realm身份验证成功的认证信息;

    AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。

    这3种策略都继承自AbstractAuthenticationStrategy类..这个抽象类里定义了前面提到的2个after方法和2个before方法还有1个merge方法...

    1     protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
    2         if( aggregate instanceof MergableAuthenticationInfo ) {
    3             ((MergableAuthenticationInfo)aggregate).merge(info);
    4             return aggregate;
    5         } else {
    6             throw new IllegalArgumentException( "Attempt to merge authentication info from multiple realms, but aggregate " +
    7                       "AuthenticationInfo is not of type MergableAuthenticationInfo." );
    8         }
    9     }

    merge方法会调用AuthenticationInfo的merge方法....大致过程是:

      public void merge(AuthenticationInfo info) {
            if (info == null || info.getPrincipals() == null || info.getPrincipals().isEmpty()) {
                return;
            }
    
            if (this.principals == null) {
                this.principals = info.getPrincipals();
            } else {
                if (!(this.principals instanceof MutablePrincipalCollection)) {
                    this.principals = new SimplePrincipalCollection(this.principals);
                }
                ((MutablePrincipalCollection) this.principals).addAll(info.getPrincipals());
            }
        ......................
    }

    大致意思就是把info的principals合并到aggregate的principals上去....

    AbstractAuthenticationStrategy的merge方法主要是在afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t)里被调用,因为经过一个Realm的认证以后会得到info对象...这个时候不同的认证策略就需要考虑是不是要把当前得到的info合并到已经有的前面几个Realm积累下来的aggregateInfo里去了...

    举例:

    AllSuccessfulStrategy类

     1     public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
     2         if (!realm.supports(token)) {
     3             String msg = "Realm [" + realm + "] of type [" + realm.getClass().getName() + "] does not support " +
     4                     " the submitted AuthenticationToken [" + token + "].  The [" + getClass().getName() +
     5                     "] implementation requires all configured realm(s) to support and be able to process the submitted " +
     6                     "AuthenticationToken.";
     7             throw new UnsupportedTokenException(msg);
     8         }
     9 
    10         return info;
    11     }

    第2行,如果realm不支持token,显然这个realm是认证失败的...而AllSuccessfulStrategy策略需要所有Realm都认证成功才行...果断抛出异常...

     1     public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t)
     2             throws AuthenticationException {
     3         if (t != null) {
     4             if (t instanceof AuthenticationException) {
     5                 //propagate:
     6                 throw ((AuthenticationException) t);
     7             } else {
     8                 String msg = "Unable to acquire account data from realm [" + realm + "].  The [" +
     9                         getClass().getName() + " implementation requires all configured realm(s) to operate successfully " +
    10                         "for a successful authentication.";
    11                 throw new AuthenticationException(msg, t);
    12             }
    13         }
    14         if (info == null) {
    15             String msg = "Realm [" + realm + "] could not find any associated account data for the submitted " +
    16                     "AuthenticationToken [" + token + "].  The [" + getClass().getName() + "] implementation requires " +
    17                     "all configured realm(s) to acquire valid account data for a submitted token during the " +
    18                     "log-in process.";
    19             throw new UnknownAccountException(msg);
    20         }
    21 
    22         log.debug("Account successfully authenticated using realm [{}]", realm);
    23 
    24         // If non-null account is returned, then the realm was able to authenticate the
    25         // user - so merge the account with any accumulated before:
    26         merge(info, aggregate);
    27 
    28         return aggregate;
    29     }

    Throwable t, t是从Realm里抛出来的,如果Realm抛出了异常,不管什么原因,认证肯定是失败了...所以Strategy也要抛出异常,即认证失败了......info = null的道理也是一样的....

    AtLeastOneSuccessfulStrategy类:

     1     public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
     2         //we know if one or more were able to succesfully authenticate if the aggregated account object does not
     3         //contain null or empty data:
     4         if (aggregate == null || CollectionUtils.isEmpty(aggregate.getPrincipals())) {
     5             throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " +
     6                     "could not be authenticated by any configured realms.  Please ensure that at least one realm can " +
     7                     "authenticate these tokens.");
     8         }
     9 
    10         return aggregate;
    11     }

    所有Realm的认证都执行完毕之后,如果AuthenticationInfo 的合并结果aggregate还是null或者空的话那么说明所有Realm的认证都失败的...那么就应该抛出异常,说明认证失败了....

    FirstSuccessfulStrategy类:

     1 public class FirstSuccessfulStrategy extends AbstractAuthenticationStrategy {
     2 
     3     /**
     4      * Returns {@code null} immediately, relying on this class's {@link #merge merge} implementation to return
     5      * only the first {@code info} object it encounters, ignoring all subsequent ones.
     6      */
     7     public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
     8         return null;
     9     }
    10 
    11     /**
    12      * Returns the specified {@code aggregate} instance if is non null and valid (that is, has principals and they are
    13      * not empty) immediately, or, if it is null or not valid, the {@code info} argument is returned instead.
    14      * <p/>
    15      * This logic ensures that the first valid info encountered is the one retained and all subsequent ones are ignored,
    16      * since this strategy mandates that only the info from the first successfully authenticated realm be used.
    17      */
    18     protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
    19         if (aggregate != null && !CollectionUtils.isEmpty(aggregate.getPrincipals())) {
    20             return aggregate;
    21         }
    22         return info != null ? info : aggregate;
    23     }
    24 }

    为什么要override beforeAllAttempts方法返回null我也不懂....父类是返回new SimpleAuthenticationInfo();.....

    override merge方法是因为FirstSuccessfulStrategy策略只返回第一个验证成功的Realm的AuthenticationInfo ...

    如果aggregate不是空的或者null,说明前面的Realm有成功过,那么根据策略,这个时候不应该把本次realm得到的info合并进去.....而是直接返回前面验证成功的AuthenticationInfo ......

  • 相关阅读:
    Mac下Selenium无法最大化Chrome解决方案
    Mac环境配置好ant后提示Permission denied
    禁止Chrome浏览器自动升级
    Selenium滚动条window.scrollTo和window.scrollBy
    Windows和Linux如何使用Java代码实现关闭进程
    自动化测试框架Selenium工作原理
    Mac下用SSH连接远程Linux或Mac服务器
    mac显示隐藏文件
    Selenium自动化测试脚本中三种等待时间简介
    Java虚拟机之栈
  • 原文地址:https://www.cnblogs.com/abcwt112/p/4582877.html
Copyright © 2020-2023  润新知