• Shiro源码(五)-多realm 的应用


      之前在研究认证授权的过程中,简单研究过可以有多个realm,下面研究其多个realm 多种认证鉴权方式以及使用。

    1. 单Reaml 认证鉴权过程

    0. realm 认证过程:

       可以看出,其本身是一个授权器Authorizer。 其作为认证器使用是需要作为认证器 Authenticator 内部的成员属性调用。

    1. 自定义Realm

    import com.beust.jcommander.internal.Lists;
    import com.zd.bx.bean.user.User;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class CustomRealm extends AuthorizingRealm {
    
        private static final Logger log = LoggerFactory.getLogger(CustomRealm.class);
    
        /**
         * 鉴权
         *
         * @param principalCollection
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            // getPrimaryPrincipal获取到的是doGetAuthenticationInfo方法最后存进去的user对象
            Object primaryPrincipal = principalCollection.getPrimaryPrincipal();
            if (primaryPrincipal == null) {
                return null;
            }
    
            SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
            User currentUser = (User) primaryPrincipal;
            // 添加角色
            authorizationInfo.addRoles(Lists.newArrayList("管理员"));
            // 添加权限
            authorizationInfo.addStringPermissions(Lists.newArrayList("user:manage:*", "dept:manage:*"));
    
            log.debug("authorizationInfo roles: {}, permissions: {}", authorizationInfo.getRoles(),
                    authorizationInfo.getStringPermissions());
            return authorizationInfo;
        }
    
        /**
         * 认证
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
                throws AuthenticationException {
    
            if (authenticationToken == null || !(authenticationToken instanceof UsernamePasswordToken)) {
                return null;
            }
    
            User user = new User();
            user.setPassword("111222");
            return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
        }
    
        @Override
        public boolean supports(AuthenticationToken token) {
            log.info("token: {}", token);
            return token != null && UsernamePasswordToken.class.isAssignableFrom(token.getClass());
        }
    }

     2. 注入到SecurityManager 中

        // 权限管理,配置主要是Realm的管理认证
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中
            securityManager.setRealms(Lists.newArrayList(new CustomRealm()));
            return securityManager;
        }

    查看其 org.apache.shiro.mgt.RealmSecurityManager#setRealms:

        public void setRealms(Collection<Realm> realms) {
            if (realms == null) {
                throw new IllegalArgumentException("Realms collection argument cannot be null.");
            }
            if (realms.isEmpty()) {
                throw new IllegalArgumentException("Realms collection argument cannot be empty.");
            }
            this.realms = realms;
            afterRealmsSet();
        }

      主要的操作包括:设置到SecutityManager 自己的属性内部; 调用 afterRealmsSet() 方法进行后续处理。调用到: org.apache.shiro.mgt.AuthenticatingSecurityManager#afterRealmsSet

        protected void afterRealmsSet() {
            super.afterRealmsSet();
            if (this.authenticator instanceof ModularRealmAuthenticator) {
                ((ModularRealmAuthenticator) this.authenticator).setRealms(getRealms());
            }
        }

      可以看到是调用父类方法,然后设置到 authenticator 认证器内部。org.apache.shiro.mgt.RealmSecurityManager#afterRealmsSet: 是设置到缓存器和发布事件

        protected void afterRealmsSet() {
            applyCacheManagerToRealms();
            applyEventBusToRealms();
        }

    3. 调用链查看:

    (1) 认证方法 doGetAuthenticationInfo 认证方法调用链:

    (2) 授权方法 doGetAuthornizationInfo() 方法调用链:

    2. 多realm 认证认证过程

      在一个普通的web 工程中,一个realm 针对usernamePasswordToken 验证方式足够使用。有的时候需要多种认证方式。 假设我们需要根据微信的uniquecode 进行认证。

    1. 新增token

    package com.zd.bx.config.shiro;
    
    
    import org.apache.shiro.authc.AuthenticationToken;
    
    public class WechatToken implements AuthenticationToken {
    
        private String wechatUniqueName;
    
        public WechatToken(String wechatUniqueName) {
            this.wechatUniqueName = wechatUniqueName;
        }
    
        @Override
        public Object getPrincipal() {
            return wechatUniqueName;
        }
    
        @Override
        public Object getCredentials() {
            return wechatUniqueName;
        }
    
        public String getWechatUniqueName() {
            return wechatUniqueName;
        }
    }

    2. 新增第二种realm, 验证WechatToken 

    package com.zd.bx.config.shiro;
    
    import com.zd.bx.bean.user.User;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class WechatRealm extends AuthorizingRealm {
    
        private static final Logger log = LoggerFactory.getLogger(WechatRealm.class);
    
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            return null;
        }
    
        /**
         * 认证
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
                throws AuthenticationException {
    
            if (authenticationToken == null || !(authenticationToken instanceof WechatToken)) {
                return null;
            }
    
            WechatToken wechatToken = (WechatToken) authenticationToken;
            User user = new User();
            user.setPassword(wechatToken.getWechatUniqueName());
            return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
        }
    
        @Override
        public boolean supports(AuthenticationToken token) {
            log.info("token: {}", token);
            return token != null && token instanceof WechatToken;
        }
    }

    3. 设置到SecurityManager 中

        // 权限管理,配置主要是Realm的管理认证
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中
            securityManager.setRealms(Lists.newArrayList(new CustomRealm(), new WechatRealm()));
            return securityManager;
        }

    4. 新增第二种登录方式

        @GetMapping("/login2")
        public String login2() {
            Subject subject = SecurityUtils.getSubject();
            AuthenticationToken generateToken = new UsernamePasswordToken("zs", "111222");
            subject.login(generateToken);
            return "success";
        }
    
        @GetMapping("/login3")
        public String login3() {
            Subject subject = SecurityUtils.getSubject();
            WechatToken wechatToken = new WechatToken("qiaozhi");
            subject.login(wechatToken);
            return "success";
        }

    5. shiro 配置放开登录地址

            /**
             *  路径 -> 过滤器名称1[参数1,参数2,参数3...],过滤器名称2[参数1,参数2...]...
             * 自定义配置(前面是路径, 后面是具体的过滤器名称加参数,多个用逗号进行分割,过滤器参数也多个之间也是用逗号分割))
             * 有的过滤器不需要参数,比如anon, authc, shiro 在解析的时候接默认解析一个数组为 [name, null]
             */
            FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // 测试地址
            FILTER_CHAIN_DEFINITION_MAP.put("/login2", "anon"); // 登陆地址
            FILTER_CHAIN_DEFINITION_MAP.put("/login3", "anon"); // 登陆地址
            FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[系统管理员,用户管理员],perms[user:manager:*]");
            FILTER_CHAIN_DEFINITION_MAP.put("/dept/**", "perms[dept:manage:*]");
            FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // 所有资源都需要经过验证

    6. 测试

      访问 /login 和 /login3 都可以进行认证成功,则证明生效。

    7. 原理查看

    1. org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate 获取认证信息

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

    2. org.apache.shiro.authc.pam.ModularRealmAuthenticator#doMultiRealmAuthentication

        protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    
            AuthenticationStrategy strategy = getAuthenticationStrategy();
    
            AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
    
            if (log.isTraceEnabled()) {
                log.trace("Iterating through {} realms for PAM authentication", realms.size());
            }
    
            for (Realm realm : realms) {
    
                try {
                    aggregate = strategy.beforeAttempt(realm, token, aggregate);
                } catch (ShortCircuitIterationException shortCircuitSignal) {
                    // Break from continuing with subsequnet realms on receiving 
                    // short circuit signal from strategy
                    break;
                }
    
                if (realm.supports(token)) {
    
                    log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
    
                    AuthenticationInfo info = null;
                    Throwable t = null;
                    try {
                        info = realm.getAuthenticationInfo(token);
                    } catch (Throwable throwable) {
                        t = throwable;
                        if (log.isDebugEnabled()) {
                            String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
                            log.debug(msg, t);
                        }
                    }
    
                    aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
    
                } else {
                    log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
                }
            }
    
            aggregate = strategy.afterAllAttempts(token, aggregate);
    
            return aggregate;
        }

      可以看到核心逻辑是在这里。

    1》 getAuthenticationStrategy() 获取认证策略, 默认是AtLeastOneSuccessfulStrategy 至少有一个成功策略,总共的策略有:

     2》 strategy.beforeAllAttempts(realms, token); 调用到 org.apache.shiro.authc.pam.AbstractAuthenticationStrategy#beforeAllAttempts 创建了一个SimpleAuthenticationInfo 对象。

        public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
            return new SimpleAuthenticationInfo();
        }

    3》遍历Realm 进行处理:

    (1) 如果支持调用org.apache.shiro.authc.pam.AbstractAuthenticationStrategy#beforeAttempt 进行处理之前逻辑:

        public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
            return aggregate;
        }

    (2)  首先调用 realm.supports(token) 判断是否支持验证指定的token, 不支持直接进行下一个realm。也就是重复 3》 过程

    (3) realm.getAuthenticationInfo(token) 获取认证信息,这里会调用到realm, 先从缓存获取,获取不到调用doGetAuthenticationInfo 方法

    (4) 调用afterAttempt 重置 aggregate 对象。 会调用到:org.apache.shiro.authc.pam.AbstractAuthenticationStrategy#afterAttempt

        public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
            AuthenticationInfo info;
            if (singleRealmInfo == null) {
                info = aggregateInfo;
            } else {
                if (aggregateInfo == null) {
                    info = singleRealmInfo;
                } else {
                    info = merge(singleRealmInfo, aggregateInfo);
                }
            }
    
            return info;
        }

      这里实际就是调用 org.apache.shiro.authc.SimpleAuthenticationInfo#merge 合并两个 info。 实际就是将单个realm 获取到的认证信息合并到aggregate 属性中

    4》 最后的realm 处理完之后调用 strategy.afterAllAttempts(token, aggregate);, 这里调用到 org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy#afterAllAttempts 重写了父类的方法:

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

      也就是验证认证是否成功,如果走完所有的realm 都不成功则抛出异常。

      这里可以看到针对org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy 策略的多realm 认证的方式是: 遍历所有的realm, 如果其 supports 返回true, 也就是支持验证该token。 进行token 的认证, 认证完之后将认证的信息合并到一个统一的SimpleAuthenticationInfo 对象aggregate 内部。 如果最后的aggregate  为空,或者其内部的认证对象Principals 为空则抛出异常。

    3. 多Realm 授权过程

    1.授权过程会调用到: org.apache.shiro.authz.ModularRealmAuthorizer#hasRole

        public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
            assertRealmsConfigured();
            for (Realm realm : getRealms()) {
                if (!(realm instanceof Authorizer)) continue;
                if (((Authorizer) realm).hasRole(principals, roleIdentifier)) {
                    return true;
                }
            }
            return false;
        }

    2. 这里实际是调用多个realm, 判断其是否包含指定的角色, 对于权限验证也是类似的机制。

    4. 切换多Realm 的认证策略

      上面看到默认的认证策略是 AtLeastOneSuccessfulStrategy, 也就是多个realm 轮询进行认证判断,根据其是否支持指定的token 进行认证处理,最后合并认证结果。 如果想改成所有的认证都必须成功,也就是将认证策略改为:AllSuccessfulStrategy。

    默认的三种认证策略是:

    1. 源码查看

    (1) org.apache.shiro.authc.pam.AbstractAuthenticationStrategy 

    package org.apache.shiro.authc.pam;
    
    import org.apache.shiro.authc.*;
    import org.apache.shiro.realm.Realm;
    
    import java.util.Collection;
    
    
    /**
     * Abstract base implementation for Shiro's concrete <code>AuthenticationStrategy</code>
     * implementations.
     *
     * @since 0.9
     */
    public abstract class AbstractAuthenticationStrategy implements AuthenticationStrategy {
    
        /**
         * Simply returns <code>new {@link org.apache.shiro.authc.SimpleAuthenticationInfo SimpleAuthenticationInfo}();</code>, which supports
         * aggregating account data across realms.
         */
        public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
            return new SimpleAuthenticationInfo();
        }
    
        /**
         * Simply returns the <code>aggregate</code> method argument, without modification.
         */
        public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
            return aggregate;
        }
    
        /**
         * Base implementation that will aggregate the specified <code>singleRealmInfo</code> into the
         * <code>aggregateInfo</code> and then returns the aggregate.  Can be overridden by subclasses for custom behavior.
         */
        public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
            AuthenticationInfo info;
            if (singleRealmInfo == null) {
                info = aggregateInfo;
            } else {
                if (aggregateInfo == null) {
                    info = singleRealmInfo;
                } else {
                    info = merge(singleRealmInfo, aggregateInfo);
                }
            }
    
            return info;
        }
    
        /**
         * Merges the specified <code>info</code> argument into the <code>aggregate</code> argument and then returns an
         * aggregate for continued use throughout the login process.
         * <p/>
         * This implementation merely checks to see if the specified <code>aggregate</code> argument is an instance of
         * {@link org.apache.shiro.authc.MergableAuthenticationInfo MergableAuthenticationInfo}, and if so, calls
         * <code>aggregate.merge(info)</code>  If it is <em>not</em> an instance of
         * <code>MergableAuthenticationInfo</code>, an {@link IllegalArgumentException IllegalArgumentException} is thrown.
         * Can be overridden by subclasses for custom merging behavior if implementing the
         * {@link org.apache.shiro.authc.MergableAuthenticationInfo MergableAuthenticationInfo} is not desired for some reason.
         */
        protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
            if( aggregate instanceof MergableAuthenticationInfo ) {
                ((MergableAuthenticationInfo)aggregate).merge(info);
                return aggregate;
            } else {
                throw new IllegalArgumentException( "Attempt to merge authentication info from multiple realms, but aggregate " +
                          "AuthenticationInfo is not of type MergableAuthenticationInfo." );
            }
        }
    
        /**
         * Simply returns the <code>aggregate</code> argument without modification.  Can be overridden for custom behavior.
         */
        public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
            return aggregate;
        }
    }

    (2) AtLeastOneSuccessfulStrategy   主要重写了afterAllAttempts 验证是否认证成功,认证失败抛出异常

    package org.apache.shiro.authc.pam;
    
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.subject.PrincipalCollection;
    
    /**
     * <tt>AuthenticationStrategy</tt> implementation that requires <em>at least one</em> configured realm to
     * successfully process the submitted <tt>AuthenticationToken</tt> during the log-in attempt.
     * <p/>
     * <p>This means any number of configured realms do not have to support the submitted log-in token, or they may
     * be unable to acquire <tt>AuthenticationInfo</tt> for the token, but as long as at least one can do both, this
     * Strategy implementation will allow the log-in process to be successful.
     * <p/>
     * <p>Note that this implementation will aggregate the account data from <em>all</em> successfully consulted
     * realms during the authentication attempt. If you want only the account data from the first successfully
     * consulted realm and want to ignore all subsequent realms, use the
     * {@link FirstSuccessfulStrategy FirstSuccessfulAuthenticationStrategy} instead.
     *
     * @see FirstSuccessfulStrategy FirstSuccessfulAuthenticationStrategy
     * @since 0.2
     */
    public class AtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrategy {
    
        private static boolean isEmpty(PrincipalCollection pc) {
            return pc == null || pc.isEmpty();
        }
    
        /**
         * Ensures that the <code>aggregate</code> method argument is not <code>null</code> and
         * <code>aggregate.{@link org.apache.shiro.authc.AuthenticationInfo#getPrincipals() getPrincipals()}</code>
         * is not <code>null</code>, and if either is <code>null</code>, throws an AuthenticationException to indicate
         * that none of the realms authenticated successfully.
         */
        public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
            //we know if one or more were able to successfully authenticate if the aggregated account object does not
            //contain null or empty data:
            if (aggregate == null || isEmpty(aggregate.getPrincipals())) {
                throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " +
                        "could not be authenticated by any configured realms.  Please ensure that at least one realm can " +
                        "authenticate these tokens.");
            }
    
            return aggregate;
        }
    }
    View Code

    (3) AllSuccessfulStrategy   

    package org.apache.shiro.authc.pam;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.realm.Realm;
    
    
    /**
     * <tt>AuthenticationStrategy</tt> implementation that requires <em>all</em> configured realms to
     * <b>successfully</b> process the submitted <tt>AuthenticationToken</tt> during the log-in attempt.
     * <p/>
     * <p>If one or more realms do not support the submitted token, or one or more are unable to acquire
     * <tt>AuthenticationInfo</tt> for the token, this implementation will immediately fail the log-in attempt for the
     * associated subject (user).
     *
     * @since 0.2
     */
    public class AllSuccessfulStrategy extends AbstractAuthenticationStrategy {
    
        /** Private class log instance. */
        private static final Logger log = LoggerFactory.getLogger(AllSuccessfulStrategy.class);
    
        /**
         * Because all realms in this strategy must complete successfully, this implementation ensures that the given
         * <code>Realm</code> {@link org.apache.shiro.realm.Realm#supports(org.apache.shiro.authc.AuthenticationToken) supports} the given
         * <code>token</code> argument.  If it does not, this method throws an
         * {@link UnsupportedTokenException UnsupportedTokenException} to end the authentication
         * process immediately. If the realm does support the token, the <code>info</code> argument is returned immediately.
         */
        public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
            if (!realm.supports(token)) {
                String msg = "Realm [" + realm + "] of type [" + realm.getClass().getName() + "] does not support " +
                        " the submitted AuthenticationToken [" + token + "].  The [" + getClass().getName() +
                        "] implementation requires all configured realm(s) to support and be able to process the submitted " +
                        "AuthenticationToken.";
                throw new UnsupportedTokenException(msg);
            }
    
            return info;
        }
    
        /**
         * Merges the specified <code>info</code> into the <code>aggregate</code> argument and returns it (just as the
         * parent implementation does), but additionally ensures the following:
         * <ol>
         * <li>if the <code>Throwable</code> argument is not <code>null</code>, re-throws it to immediately cancel the
         * authentication process, since this strategy requires all realms to authenticate successfully.</li>
         * <li>neither the <code>info</code> or <code>aggregate</code> argument is <code>null</code> to ensure that each
         * realm did in fact authenticate successfully</li>
         * </ol>
         */
        public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo info, AuthenticationInfo aggregate, Throwable t)
                throws AuthenticationException {
            if (t != null) {
                if (t instanceof AuthenticationException) {
                    //propagate:
                    throw ((AuthenticationException) t);
                } else {
                    String msg = "Unable to acquire account data from realm [" + realm + "].  The [" +
                            getClass().getName() + " implementation requires all configured realm(s) to operate successfully " +
                            "for a successful authentication.";
                    throw new AuthenticationException(msg, t);
                }
            }
            if (info == null) {
                String msg = "Realm [" + realm + "] could not find any associated account data for the submitted " +
                        "AuthenticationToken [" + token + "].  The [" + getClass().getName() + "] implementation requires " +
                        "all configured realm(s) to acquire valid account data for a submitted token during the " +
                        "log-in process.";
                throw new UnknownAccountException(msg);
            }
    
            log.debug("Account successfully authenticated using realm [{}]", realm);
    
            // If non-null account is returned, then the realm was able to authenticate the
            // user - so merge the account with any accumulated before:
            merge(info, aggregate);
    
            return aggregate;
        }
    }

       重写了 beforeAttempt 方法, 如果该realm 不支持该token 抛出异常; afterAttempt 也是合并加验证是否认证成功。 确保必须所有realm 都认证成功。

    (4) org.apache.shiro.authc.pam.FirstSuccessfulStrategy 第一个成功

    public class FirstSuccessfulStrategy extends AbstractAuthenticationStrategy {
    
        private boolean stopAfterFirstSuccess;
    
        public void setStopAfterFirstSuccess (boolean stopAfterFirstSuccess ) {
    
            this.stopAfterFirstSuccess  = stopAfterFirstSuccess ;
        }
    
        public boolean getStopAfterFirstSuccess() {
            return stopAfterFirstSuccess ;
        }
    
        /**
         * Returns {@code null} immediately, relying on this class's {@link #merge merge} implementation to return
         * only the first {@code info} object it encounters, ignoring all subsequent ones.
         */
        public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
            return null;
        }
    
    
        /**
         * Throws ShortCircuitIterationException if stopAfterFirstSuccess is set and authentication is 
         * successful with a previously consulted realm. 
         * Returns the <code>aggregate</code> method argument, without modification
         * otherwise.
         */
        public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
            if (getStopAfterFirstSuccess() && aggregate != null && !isEmpty(aggregate.getPrincipals())) {
                throw new ShortCircuitIterationException();
            }
            return aggregate;
        }
    
        
    
        private static boolean isEmpty(PrincipalCollection pc) {
            return pc == null || pc.isEmpty();
        }
    
        /**
         * Returns the specified {@code aggregate} instance if is non null and valid (that is, has principals and they are
         * not empty) immediately, or, if it is null or not valid, the {@code info} argument is returned instead.
         * <p/>
         * This logic ensures that the first valid info encountered is the one retained and all subsequent ones are ignored,
         * since this strategy mandates that only the info from the first successfully authenticated realm be used.
         */
        protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
            if (aggregate != null && !isEmpty(aggregate.getPrincipals())) {
                return aggregate;
            }
            return info != null ? info : aggregate;
        }
    }

      重写 beforeAllAttempts 返回一个空对象; beforeAttempt 判断如果认证,抛出异常 ShortCircuitIterationException, 上面org.apache.shiro.authc.pam.ModularRealmAuthenticator#doMultiRealmAuthentication 捕捉到异常则结束后续realm 认证; merge 方法只返回单个对象, 不进行merge。

    2. 切换为AllSuccessfulStrategy

      切换思路主要就是重新设置SecurityManager的authenticator。 其中在设置authenticator的过程中需要注意, authenticator 需要设置在设置realm 之前,否则重新设置authenticator 之后不会应用给securityManager 设置的realms, 原因是org.apache.shiro.mgt.RealmSecurityManager#setRealms 设置完调用 afterRealmsSet 给authenticator 设置realms。

      下面两种方法原理一样, 只是代码写的不同而已。

    方法一:

        @Bean
        public Authenticator authenticator() {
            ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
            authenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
            authenticator.setRealms(Lists.newArrayList(new CustomRealm(), new WechatRealm()));
            return authenticator;
        }
        
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setAuthenticator(authenticator());
            return securityManager;
        }

     方法二:

        @Bean
        public Authenticator authenticator() {
            ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
            authenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
            return authenticator;
        }
    
        // 权限管理,配置主要是Realm的管理认证
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setAuthenticator(authenticator());
    //         注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中
            securityManager.setRealms(Lists.newArrayList(new CustomRealm(), new WechatRealm()));
            return securityManager;
        }

    总结:对于认证和授权的机制还不是太一样。 授权是遍历所有的realm, 判断其是否有指定的角色或者权限; 认证的时候会有一个多realm的认证策略,默认是最少一个成功, 然后根据不同的策略对认证进行不同的处理。

    【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
  • 相关阅读:
    rbac 授权
    docker overlayfs k8s overlayfs 目录文件太大
    pod oom
    文档怎么写
    fabric
    nacos 报错 Unknown column 'encrypted_data_key' in 'field list'
    【Oracle】RAC在启动时ohasd超时导致启动失败
    【SQLServer】查看sqlserver中的历史查询记录
    【Oracle】High CPU (%sys) Usage On Oracle Linux 6 UEK3 RAC Node
    【MySQL】MySQL 5.7中过滤复制和部分复制的变化
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/15487483.html
Copyright © 2020-2023  润新知