侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

安全/权限框架Shiro(9)之多Realm认证

zze
zze
2018-06-11 / 0 评论 / 0 点赞 / 405 阅读 / 13623 字

实现

其实在前面【登录功能实现】这节查看源码的过程中我们有看到这个方法:

// org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    // <1>
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

<1> 处实际上是从当前 ModularRealmAuthenticator 实例中获取一个 realms 属性,而这个属性是一个集合,该属性是用来存放我们所注册的 Realm 实例。

显然,Realm 实例是可以有多个的,我们只需要将它们注入给 ModularRealmAuthenticator 实例,然后将 ModularRealmAuthenticator 实例交给安全管理器即可。如下:

1、创建第二个 Realm 类,密码使用 SHA1 加密:

// com.zze.shiro.realms.SecondRealm
package com.zze.shiro.realms;

import org.apache.shiro.authc.*;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;

public class SecondRealm extends AuthenticatingRealm {

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // authenticationToken 实际就是 Controller 中通过 Subject.Login 方法传入的 token,保存了前台输入的密码
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        // 获取页面传入的用户名
        String username = usernamePasswordToken.getUsername();
        // 根据用户名从数据库获取密码,假如获取到的是 123456 加密后的密码
        Object credentials = "123456";  // 要加密的对象
        String hashAlgorithmName = "SHA1"; // 加密方式
        ByteSource salt = ByteSource.Util.bytes(username); // 以用户名当做盐值
        int hashIterations = 1024; // 加密次数,与 Spring 中配置一致
        credentials = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);

        // 假如 unknown 用户名不存在
        if ("unknown".equals(username)) {
            throw new UnknownAccountException("用户名不存在");
        }
        // 假如 monster 用户名已经被锁定
        if ("monster".equals(username)) {
            throw new LockedAccountException("用户已被锁定");
        }
        // 根据用户情况,构建 AuthenticationInfo 对象返回,通常使用的实现类为 SimpleAuthenticationInfo
        // a. principal : 认证的实体信息,可以是 username,也可以是用户对应的实体对象
        Object principal = username;
        // b. credentials : 密码
        // c. realmName : 当前 realm 对象的 name,调用父类的 getName() 方法获得
        String realmName = getName();

        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, credentials, salt, realmName);

        return simpleAuthenticationInfo; // 返回认证信息,交给 Shiro 完成比对

    }
}

2、在 Spring 核心配置文件中配置多 Realm

<!-- applicationContext.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--
    1、配置安全管理器 SecurityManager
    -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="cacheManager" ref="cacheManager"/>
        <property name="realm" ref="jdbcRealm"/>
        <property name="authenticator" ref="authenticator"/>
    </bean>

    <!--
    2、配置缓存管理器 CacheManager
        a、需加入 ehcache jar 和 配置文件
    -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>
    <!--
    4、配置 lifecycleBeanPostProcessor,可以自动调用配置在 Spring IOC 容器中 Shiro bean 的生命周期方法
    -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!--
    5、启用 IOC 容器中使用 Shiro 注解,必须在配置 lifecycleBeanPostProcessor 之后才可使用
    -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>

    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="successUrl" value="/list.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
        <property name="filterChainDefinitions">
            <value>
                /logout = anon
                /login* = anon
                /** = authc
            </value>
        </property>
    </bean>
    <bean id="jdbcRealm" class="com.zze.shiro.realms.LoginRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <!--加密方式-->
                <property name="hashAlgorithmName" value="MD5"/>
                <!--加密次数-->
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>

    <bean id="secondRealm" class="com.zze.shiro.realms.SecondRealm">
        <property name="credentialsMatcher">
            <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
                <!--加密方式-->
                <property name="hashAlgorithmName" value="SHA1"/>
                <!--加密次数-->
                <property name="hashIterations" value="1024"/>
            </bean>
        </property>
    </bean>
    <!--配置多 Realm-->
    <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
        <property name="realms">
            <list>
                <ref bean="jdbcRealm"/>
                <ref bean="secondRealm"/>
            </list>
        </property>
    </bean>
</beans>

此时,我们再登录就会使用多 Realm 认证。

认证策略

上面我们已经实现了多 Realm 认证,但肯定还会有一个疑问。我们现在使用的是多个 Realm 认证,那如何才算认证成功呢?对于这个问题的解决,Shiro 为我们提供了认证策略,要使用认证策略我们需要了解下面几个类,这几个类都实现了 org.apache.shiro.authc.pam.AuthenticationStrategy 接口:

  • org.apache.shiro.authc.pam.FirstSuccessfulStrategy :只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其它的忽略。
  • org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy :只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有 Realm 身份验证成功的认证信息。
  • org.apache.shiro.authc.pam.AllSuccessfulStrategy :所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。

org.apache.shiro.authc.pam.ModularRealmAuthenticator 默认使用的是 org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy 策略。

那我们如何配置认证策略呢,还是从源码看。使用多 Realm 认证时会经过这个方法:

// org.apache.shiro.authc.pam.ModularRealmAuthenticator#doMultiRealmAuthentication
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    // <1>
    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) {

        aggregate = strategy.beforeAttempt(realm, token, aggregate);

        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.isWarnEnabled()) {
                    String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
                    log.warn(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() 就是在获取认证策略,而认证策略也是 ModularRealmAuthenticator 的一个属性,所以我们只需要将我们要使用的认证策略类注入给认证器 ModularRealmAuthenticator 实例即可:

<!--认证器配置多 Realm-->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    <!--配置多 Realm-->
    <property name="realms">
        <list>
            <ref bean="jdbcRealm"/>
            <ref bean="secondRealm"/>
        </list>
    </property>
    <!--配置认证策略-->
    <property name="authenticationStrategy">
        <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
    </property>
</bean>

在上面我们是把 realms 属性配置给 ModularRealmAuthenticator ,但我们通常是直接把 realms 直接配置给安全管理器,如下:

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="cacheManager" ref="cacheManager"/>
    <property name="authenticator" ref="authenticator"/>
    <!--配置多 Realm-->
    <property name="realms">
        <list>
            <ref bean="jdbcRealm"/>
            <ref bean="secondRealm"/>
        </list>
    </property>
</bean>

<!--认证器配置多 Realm-->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    <!--配置认证策略-->
    <property name="authenticationStrategy">
        <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"/>
    </property>
</bean>

但我们知道 org.apache.shiro.authc.pam.ModularRealmAuthenticator#doMultiRealmAuthentication 方法中是要通过 ModularRealmAuthenticator 实例的属性拿到 realms 使用的。为什么这样配置依然好使呢?查看 DefaultWebSecurityManagersetRealms 方法:

// 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;
    // <2>
    afterRealmsSet();
}

接着查看 <2> 处的 afterRealmsSet() 方法:

// org.apache.shiro.mgt.AuthorizingSecurityManager#afterRealmsSet
protected void afterRealmsSet() {
    super.afterRealmsSet();
    if (this.authorizer instanceof ModularRealmAuthorizer) {
        ((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms());
    }
}

看到这里就可以明了,其实在把 realms 注入给安全管理器 RealmSecurityManager 时,安全管理器同时也把 realms 的引用给了 ModularRealmAuthenticatorrealms 属性。

0

评论区