Java-Shiro(七):Shiro認證、授權信息緩存


聲明:本證項目基於《Java-Shiro(六):Shiro Realm講解(三)Realm的自定義及應用》構建項目為基礎。

本文代碼:https://github.com/478632418/springmv_without_web_xml/tree/master/mybaits-test-dynamic-sql-02

Shiro內部提供了對認證信息和授權信息的緩存,但是shiro默認是關閉認證信息緩存,對於授權信息的緩存shiro默認開啟的。一般情況下,使用shiro緩存時,只需要關注授權信息緩存,因為認證信息只是一次驗證查詢,而授權信息需要在每次認證都會執行(訪問量大),且一般情況下授權的數據量大。

但是,當用戶信息被修改時,我們希望理解看到認證信息也被同步時,需要關注認證信息清空同步問題。

Shiro授權緩存的使用

配置

 

在授權信息緩存的方案包含以下三種:

1)使用Ehcache(系統混合緩存方案);
2)使用本地內存緩存方案;
3)自定義CacheManager(比如Redis用來作為緩存)。

1)使用Ehcache(系統混合緩存方案)

1)在pom.xml中引入shiro-ehcache依賴;

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>${shiro.version}</version>
        </dependency>

2)在applicationContext-shiro.xml中添加ehcache配置並被securityManager的cacheManage引用:

    <!--cacheManager-->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:shiro-ehcache.xml"/>
    </bean>
    
    <!-- securityManager安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
        <property name="cacheManager" ref="cacheManager"></property>
    </bean>

3)在src/main/resources下添加shiro-ehcache.xml ehcache配置文件。

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
    <!-- 數據緩存地址,如,F:/develop/ehcache -->
    <diskStore path="java.io.tmpdir/shiro-spring-sample"/>
    <!-- 
    name="authorizationCache" //緩存名稱
    maxEntriesLocalHeap="2000" //緩存最大條目數
    eternal="false" //對象是否永久有效,true則timeout失效
    timeToIdleSeconds="3600" //對象在失效前的閑置時間(單位:s),僅eternal=false時有效;默認為0,即可閑置時間無窮大。
    timeToLiveSeconds="0" //緩存數據的生成時間(單位:s),介於創建時間和失效時間之間;僅eternal=false有效;默認為0,即對象存活時間無窮大。
    overflowToDisk="false" //內存中對象數量達到maxElementInMemory時,是否將對象寫到磁盤
    diskSpoolBufferSizeMB:設置diskStore磁盤緩存的緩存區大小,默認30MB。每個Cache都應該有自己的一個緩存區。
    maxElementOnDisk:磁盤最大緩存個數。
    diskPersistent:是否緩存虛擬機重啟期數據,默認false。
    diskExpiryThreadIntervalSeconds: 磁盤失效線程運行時間間隔,默認120s。
    memoryStoreEvictionPolicy:達到maxElementInMemory時,Ehcache將會根據此策略去清理內存,默認策略是LRU(最近最少使用),可設為FIFO(先進先出)或LFU(較少使用)。
    clearOnFlush: 內存數量最大時是否清除。
     -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            />
    
    <cache name="shiro-activeSessionCache"
           maxElementsInMemory="10000"
           eternal="true"
           overflowToDisk="true"
           diskPersistent="true"
           diskExpiryThreadIntervalSeconds="600"/>

    <cache name="org.apache.shiro.realm.SimpleAccountRealm.authorization"
           maxElementsInMemory="100"
           eternal="false"
           timeToLiveSeconds="600"
           overflowToDisk="false"/>

</ehcache>

備注:ehcache中cache標簽屬性意義:

name="authorizationCache" //緩存名稱
maxEntriesLocalHeap="2000" //緩存最大條目數
eternal="false" //對象是否永久有效,true則timeout失效
timeToIdleSeconds="3600" //對象在失效前的閑置時間(單位:s),僅eternal=false時有效;默認為0,即可閑置時間無窮大。
timeToLiveSeconds="0" //緩存數據的生成時間(單位:s),介於創建時間和失效時間之間;僅eternal=false有效;默認為0,即對象存活時間無窮大。
overflowToDisk="false" //內存中對象數量達到maxElementInMemory時,是否將對象寫到磁盤
diskSpoolBufferSizeMB:設置diskStore磁盤緩存的緩存區大小,默認30MB。每個Cache都應該有自己的一個緩存區。
maxElementOnDisk:磁盤最大緩存個數。
diskPersistent:是否緩存虛擬機重啟期數據,默認false。
diskExpiryThreadIntervalSeconds: 磁盤失效線程運行時間間隔,默認120s。
memoryStoreEvictionPolicy:達到maxElementInMemory時,Ehcache將會根據此策略去清理內存,默認策略是LRU(最近最少使用),可設為FIFO(先進先出)或LFU(較少使用)。
clearOnFlush: 內存數量最大時是否清除。

2)使用本地內存緩存方案

applicationContext-shiro.xml中配置cacheManager,該cacheManager並被securityManager引用。

    <!-- // 采用本地內存方式緩存 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"/>
        
    <!-- securityManager安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
        <property name="cacheManager" ref="cacheManager"></property>
     </bean>

其他不需要配置即可。

3)自定義CacheManager

比如Redis用來作為緩存。

1)需要添加redis依賴到pom.xml

        <!-- redis依賴包 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-redis -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>

2)在src/main/resources下新建applicaitonContext-redis.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
    http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
    http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
      <!-- 加載配置文件 -->
    <context:property-placeholder location="classpath:jedis.properties" ignore-unresolvable="true" />
    
    <!-- 連接池配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <!-- 最大連接數 -->
        <property name="maxTotal" value="${redis.maxTotal}" />
        <!-- 最大空閑連接數 -->
        <property name="maxIdle" value="${redis.maxIdle}" />
        <!-- 每次釋放連接的最大數目 -->
        <property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
        <!-- 釋放連接的掃描間隔(毫秒) -->
        <property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
        <!-- 連接最小空閑時間 -->
        <property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
        <!-- 連接空閑多久后釋放, 當空閑時間>該值 且 空閑連接>最大空閑連接數 時直接釋放 -->
        <property name="softMinEvictableIdleTimeMillis" value="${redis.softMinEvictableIdleTimeMillis}" />
        <!-- 獲取連接時的最大等待毫秒數,小於零:阻塞不確定的時間,默認-1 -->
        <property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
        <!-- 在獲取連接的時候檢查有效性, 默認false -->
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />
        <!-- 在空閑時檢查有效性, 默認false -->
        <property name="testWhileIdle" value="${redis.testWhileIdle}" />
        <!-- 連接耗盡時是否阻塞, false報異常,ture阻塞直到超時, 默認true -->
        <property name="blockWhenExhausted" value="${redis.blockWhenExhausted}" />
    </bean>
 
    <bean id="jedisPool" class="redis.clients.jedis.JedisPool">
        <constructor-arg name="host" value="${redis.host}"></constructor-arg>
        <constructor-arg name="port" value="${redis.port}"></constructor-arg>
        <constructor-arg name="poolConfig" ref="jedisPoolConfig"></constructor-arg>
    </bean>

    <!-- 需要密碼 -->
    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
          p:host-name="${redis.host}"
          p:port="${redis.port}"
          p:password="${redis.pass}"
          p:pool-config-ref="jedisPoolConfig"/>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
        <property name="connectionFactory"     ref="connectionFactory" />
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
        </property>
    </bean>
</beans>

配置中需要依賴資源文件jedis.properties

redis.maxTotal=2000
redis.maxIdle=50
redis.numTestsPerEvictionRun=1024
redis.timeBetweenEvictionRunsMillis=30000
redis.minEvictableIdleTimeMillis=1800000
redis.softMinEvictableIdleTimeMillis=10000
redis.maxWaitMillis=15000
redis.testOnBorrow=false
redis.testWhileIdle=false
redis.testOnReturn=false
redis.blockWhenExhausted=true
redis.host=127.0.0.1
redis.port=6379
redis.pass=

3)修改web.xml,ContextLoaderListener加載文件添加applicationContext-redis.xml,修改后為:

    <!-- 加載spring容器 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:applicationContext-redis.xml,
            classpath:applicationContext-shiro.xml,
            classpath:applicationContext-mybatis.xml
        </param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

4)新建redis緩存管理類RedisCache.java和RedisCacheManager.java

RedisCache.java

public class RedisCache<K, V> implements Cache<K, V> {
    @Autowired
    private RedisTemplate redisTemplate;
    private Logger logger;
    private String keyPrefix;

    public String getKeyPrefix() {
        return this.keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public RedisCache(RedisTemplate redisTemplate) {
        this.logger = LoggerFactory.getLogger(this.getClass());
        this.keyPrefix = "shiro_redis_cache:";
        this.redisTemplate = redisTemplate;
    }

    public RedisCache(RedisTemplate redisTemplate, String prefix) {
        this(redisTemplate);
        this.keyPrefix = prefix;
    }

    /**
     * 獲得byte[]型的key
     *
     * @param key
     * @return
     */
    private byte[] getByteKey(Object key) {
        if (key instanceof String) {
            String preKey = this.keyPrefix + key;
            return preKey.getBytes();
        } else {
            return SerializeUtils.serialize((Serializable) key);
        }
    }

    private RedisConnection getRedisConnect() {
        return redisTemplate.getConnectionFactory().getConnection();
    }

    @Override
    public Object get(Object key) throws CacheException {
        byte[] bytes = getByteKey(key);
        byte[] value = getRedisConnect().get(bytes);
        if (value == null) {
            return null;
        }
        return SerializeUtils.deserialize(value);
    }

    /**
     * 將shiro的緩存保存到redis中
     */
    @Override
    public Object put(Object key, Object value) throws CacheException {
        RedisConnection redisConnection = getRedisConnect();
        byte[] bytesKey = getByteKey(key);
        byte[] bytesValue = SerializeUtils.serialize((Serializable) value);

        redisConnection.set(bytesKey, bytesValue);

        byte[] bytes = redisConnection.get(getByteKey(key));
        Object object = SerializeUtils.deserialize(bytes);

        return object;

    }

    @Override
    public Object remove(Object key) throws CacheException {
        RedisConnection redisConnection = getRedisConnect();

        byte[] bytes = redisConnection.get(getByteKey(key));

        redisConnection.del(getByteKey(key));

        return SerializeUtils.deserialize(bytes);
    }

    /**
     * 清空所有緩存
     */
    @Override
    public void clear() throws CacheException {
        RedisConnection redisConnection = getRedisConnect();
        redisConnection.flushDb();
    }

    /**
     * 緩存的個數
     */
    @Override
    public int size() {
        RedisConnection redisConnection = getRedisConnect();
        Long size = redisConnection.dbSize();
        return size.intValue();
    }

    /**
     * 獲取所有的key
     */
    @Override
    public Set keys() {
        RedisConnection redisConnection = getRedisConnect();
        Set<byte[]> keys = redisConnection.keys(new String("*").getBytes());
        Set<Object> set = new HashSet<Object>();
        for (byte[] bs : keys) {
            set.add(SerializeUtils.deserialize(bs));
        }
        return set;
    }


    /**
     * 獲取所有的value
     */
    @Override
    public Collection values() {
        RedisConnection redisConnection = getRedisConnect();
        Set keys = this.keys();

        List<Object> values = new ArrayList<Object>();
        for (Object key : keys) {
            byte[] bytes = redisConnection.get(getByteKey(key));
            values.add(SerializeUtils.deserialize(bytes));
        }
        return values;
    }

}
View Code

RedisCacheManager.java 

public class RedisCacheManager implements CacheManager {
    @Resource
    private RedisTemplate<Serializable, Object> redisTemplate;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        return new RedisCache<K, V>(redisTemplate);
    }
}

5)修改applicationContext-shiro.xml配置文件,配置securityManager的cacheManager對象。

    <!-- 自定義cacheManager -->
    <bean id="cacheManager" class="com.dx.test.shiro.RedisCacheManager"/>

    <!-- securityManager安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"></property>
        <property name="cacheManager" ref="cacheManager"></property>
        <property name="sessionManager" ref="sessionManager"></property>
     </bean>

執行效果

執行效果:

1)進入登錄頁面http://localhost:8080/mybaits-test-dynamic-sql/login/,填寫用戶&密碼點擊登錄按鈕,用戶認證通過;

2)該用戶第一次授權,調用realm#doGetAuthorizationInfo(...)方法查詢數據庫。

3)該用戶第二次授權,不調用realm#doGetAuthorizationInfo(...)方法查詢數據庫,直接從緩存中取出授權信息(授權標識符)。 

清除緩存

如果用戶正常退出,緩存自動清空;

如果用戶非正常退出,緩存也自動清空;

如果修改了用戶權限,而用戶不退出系統,修改的權限無法立即生效。需要開發者變成實現:

1)在權限修改后,調用MyRealm的clearCache方法清除緩存。

在MyRealm類方法最下邊添加方法clearCache方法:

    /**
     * 清空已經放入緩存的授權信息。
     * */
    public void clearCache() {
        PrincipalCollection principals=SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }

2)添加模擬編輯角色SysRoleController.java類:

@Controller
@RequestMapping(value = "/role")
public class SysRoleController {
    @Autowired
    private MyRealm myRealm;

    @RequestMapping(value = "/update", method = RequestMethod.GET)
    public String updateRole(SysRole sysRole, Map<String, Object> map) {
        BaseResult baseResult = null;
        ResultEnum enu = null;
        
        // 模擬:在這里做了以下業務:
        // 1)修改了角色下的資源信息;
        // 2)刪除了角色;
        // 3)修改了用戶的角色信息。

        myRealm.clearCache();

        enu = ResultEnum.Success;
        baseResult = new BaseResult(enu.getCode(), enu.getMessage(), enu.getDesc());
        map.put("result", baseResult);

        return "role/list.jsp";
    }
}

備注:

1)上邊代碼只是模擬測試代碼,模擬修改了授權信息,實際上做了以下操作都會修改授權信息:

1.1)修改了角色下的資源信息;

1.2)刪除了角色;

1.3)修改了用戶的角色信息。

2)思考:如果修改了授權信息,實際系統已經修改了,但是認證的用戶授權已經被緩存到了緩存中,假設不再修改授權信息的地方清理授權緩存,什么時候用戶會獲取修改后的授權信?

2.1)用戶退出系統時會清理緩存,重新后在第一次授權時,會調用realm#doGetAuthorizationInfo(...)方法查詢數據庫,將最新的授權信息緩存;

2.2)緩存過期,在shiro-ehcache.xml配置的cache標簽項中有配置自動過期時間,當然如果配置為0,永不過期。

3)測試

認證后,訪問授權,然后修改授權信息,重新授權是否調用realm#doGetAuthorizationInfo(...)方法查詢數據庫。

備注:

1)認證:訪問登錄頁面,輸入賬戶密碼點擊‘登錄’按鈕,登錄后台;

2)授權(url攔截、訪問<shiro:標簽>、@RequiresPermission、@RequiresRole等):訪問后台頁面,第一次會調用realm#doGetAuthorizationInfo(...)方法查詢數據庫,然后ehcache會緩存該授權吸信息到緩存中。第二次以后都不會再調用realm#doGetAuthorizationInfo(...)方法查詢數據庫;

3)修改授權信息,訪問修改授權頁面(http://localhost:8080/mybaits-test-dynamic-sql/role/update

  執行了以下三種操作都可能修改授權信息:

  • 1)修改了角色下的資源信息;
  • 2)刪除了角色;
  • 3)修改了用戶的角色信息。

4)重新訪問授權(url攔截、訪問<shiro:標簽>、@RequiresPermission、@RequiresRole等)信息:訪問后台頁面,如果緩存被情況的情況下會再次調用realm#doGetAuthorizationInfo(...)方法查詢數據庫,然后ehcache會緩存該授權信息到緩存中。

遇到問題:java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource

在使用Redis作為Shiro的緩存管理器的時候,在將SimpleAuthenticationInfo信息進行序列化的時候報異常:java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource

原因是:SimpleByteSource沒有實現Serializable接口

解決辦法:

1)重新定義SimpleByteSource(MySimpleByteSource.java)

/**
 * 解決:shiro 使用緩存時出現:java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource
 * 序列化后,無法反序列化的問題
 */
public class MySimpleByteSource implements ByteSource, Serializable {
    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MySimpleByteSource() {
    }

    public MySimpleByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MySimpleByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MySimpleByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MySimpleByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MySimpleByteSource(File file) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
    }

    public MySimpleByteSource(InputStream stream) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public void setBytes(byte[] bytes) {
        this.bytes = bytes;
    }

    @Override
    public byte[] getBytes() {
        return this.bytes;
    }

    @Override
    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }
        return this.cachedHex;
    }

    @Override
    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    @Override
    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    @Override
    public String toString() {
        return this.toBase64();
    }

    @Override
    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource) o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }

}
View Code

2)修改MyRealm#doGetAuthenticationInfo()中使用salt代碼:

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("‘MyRealm’執行認證操作:");
        if (token == null) {
            throw new UnsupportedTokenException();
        }

        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        if (userToken == null) {
            throw new UnsupportedTokenException();
        }

        // 獲取當前需要登錄的用戶
        String username = userToken.getUsername();
        String userPwd = String.valueOf(userToken.getPassword());
        if (StringUtils.isBlank(username) || userPwd == null) {
            throw new IncorrectCredentialsException("用戶名或密碼不正確");
        }

        SysUser sysUser = this.sysUserMapper.getByUsername(username);
        if (sysUser == null) {
            throw new UnknownAccountException("用戶名不存在");
        }

        Byte locked = Byte.valueOf("1");
        if (sysUser.getStatus().equals(locked)) {
            throw new LockedAccountException("用戶已鎖定");
        }
        Date now = new Date();
        if (sysUser.getExpireTime().before(now)) {
            throw new ExpiredCredentialsException("用戶過期");
        }

        // 從數據庫中取出密碼,密碼是加過鹽的
        String password = sysUser.getPassword();
        // 從數據庫中取出鹽
        String salt=sysUser.getSalt();

        SysUser simpleSysUser=new SysUser();
        simpleSysUser.setUserName(sysUser.getUsername());
        simpleSysUser.setEmail(sysUser.getEmail());
        simpleSysUser.setPhone(sysUser.getPhone());
        simpleSysUser.setNickName(sysUser.getNickName());
        simpleSysUser.setSignature(sysUser.getSignature());
        
        // 該信息會提交給SecurityManager,在SecurityManager內部會進行驗證:
        // 用戶輸入的password+salt+md5+hashIterations 是否等於 db password? 等於則通過認證,否則不通過認證。
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                simpleSysUser,
                password.toCharArray(),
                new MySimpleByteSource(salt),
                this.getName());
        return authenticationInfo;
    }

 配置認證緩存

指定緩存認證的緩存對象,以及是否開啟緩存,這些配置都需要在realm屬性上配置。
1)自定義緩存對象:
1)首先,開啟realm的緩存控制開關屬性;
2)其次,需要指定 realm.authenticationCache 屬性(也即是指定緩存類對象)。

配置

主要兩種實現方式:

1)采用自定義authenticationCache的cache對象;

2)集成ehcache,在ehcache中配置authenticationCache對象。

1)自定義緩存類對象

    <!-- 配置自定義Realm -->
    <bean id="myRealm" class="com.dx.test.shiro.MyRealm">
        <!-- 將憑證匹配器設置到realm中,realm按照憑證匹配器的要求進行散列 -->
        <property name="credentialsMatcher" ref="credentialsMatcher"></property>
        <!--啟用緩存,默認SimpleAccountRealm關閉,默認AuthenticatingRealm、AuthorizingRealm、CachingRealm開啟-->
        <property name="cachingEnabled" value="true"/>
        <!--啟用身份驗證緩存,即緩存AuthenticationInfo,默認false-->
        <property name="authenticationCachingEnabled" value="true"/>
        <!--啟用授權緩存,即緩存AuthorizationInfo的信息,默認為true-->
        <property name="authorizationCachingEnabled" value="true"/>
        <property name="authenticationCache" ref="redisCache"></property>
    </bean>
    <bean id="redisCache" class="com.dx.test.shiro.RedisCache">
        <constructor-arg ref="redisTemplate"></constructor-arg>
    </bean>

備注:

1)其中RedisCache需要依賴redisTemplate bean,因此需要在ContextLoaderListener中添加applicationContext-redis.xml(上邊有提過)。

2)cachingEnabled:啟用緩存,默認SimpleAccountRealm關閉,默認AuthenticatingRealm、AuthorizingRealm、CachingRealm開啟;
3)authenticationCachingEnabled:啟用身份驗證(認證)緩存,即緩存AuthenticationInfo,默認false;
4)authorizationCachingEnabled:啟用授權緩存,即緩存AuthorizationInfo的信息,默認為true;
5)authenticationCache:進行認證信息緩存使用的緩存類對象。

根據上邊配置中注釋,我們可以得知:

當我們的自定義 MyRealm 繼承自 AuthorizingRealm 時,cachingEnabled 屬性值一定為 true。那么,主要控制是否開啟認證的開關是 authenticationCachingEnabled。

2)集成ehcache實現認證緩存

除了上邊自定義方式,實際上還可以集成Ehcache實現認證、授權緩存。這種方式,不僅僅可以配置認證緩存,對授權緩存也可以采用該方案。

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="myRealm"/>       
    <property name="cacheManager" ref="ehCacheManager"/>   
</bean>  
<bean id="ehCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">     
    <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> 
<bean/>       
<bean id="myRealm" class="com.dx.test.shiro.MyRealm">         
    <property name="credentialsMatcher" ref="credentialsMatcher"/>         
    <property name="cachingEnabled" value="true"/>         
    <property name="authenticationCachingEnabled" value="true"/>         
    <property name="authenticationCacheName" value="authenticationCache"/>         
    <property name="authorizationCachingEnabled" value="true"/>         
    <property name="authorizationCacheName" value="authorizationCache"/>     
</bean>

ehcache.xml配置內容:

<cache name="authorizationCache"            
     maxEntriesLocalHeap="2000"            
     eternal="false"            
     timeToIdleSeconds="3600"            
     timeToLiveSeconds="0"            
     overflowToDisk="false"            
     statistics="true">     
</cache>      
<cache name="authenticationCache"            
     maxEntriesLocalHeap="2000"            
     eternal="false"            
     timeToIdleSeconds="3600"            
     timeToLiveSeconds="0"            
     overflowToDisk="false"            
     statistics="true">     
</cache>

測試

主要針對自定authenticationCache方案進行測試:

1)進入登錄頁面,輸入賬戶密碼,點擊登錄;

2)此時會發現redis中多了一條redis_cache:信息。

 

清除緩存

如果賬戶正常退出,會自動清空redis中緩存;

如果非正常退出,不會自動清空redis中緩存。(因此,這也是不推薦配置的一個原因);

當修改了用戶基本信息時,如果強制執行清空認證信息,需要在MyRealm中定義clearXXX先關類。

MyRealm.java清空認證、授權信息方法定義:

    //重寫以下方法並改為public,否則測試無法調用這些Protected的方法
    @Override
    public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(principals);
    }
    @Override
    public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }
    @Override
    public void clearCache(PrincipalCollection principals) {
        //同時調用以上倆方法,清空兩個Info
        super.clearCache(principals); 
    }
     
    public void clearAllCachedAuthorizationInfo(){
        getAuthorizationCache().clear();
    }
    public void clearAllCachedAuthenticationInfo() {
        getAuthenticationCache().clear();
    }
    public void clearAllCache() {
        clearAllCachedAuthenticationInfo();
        clearAllCachedAuthorizationInfo();
    }

注意:

1)如果采用了redis緩存認證方案,當執行clearAllCachedAuthenticationInfo()方法,會清退所有在線用戶,用戶需要重新登錄才能繼續操作;

2)如果采用ehcache緩存認證方案,當執行clearAllCachedAuthenticationInfo()方法,不會清空任何在線用戶,用戶不需要重新登錄;

3)不管采用什么緩存授權信息,當執行了clearAllCachedAuthorizationInfo()方法,都會清空授權信息,用戶再次操作調用授權時,會重新執行MyRealm#doGetAuthorizationInfo()方法。這就有缺點,也許只修改涉及到授權改變的用戶授權緩存清空才合理,以下清空所有用戶有點粗暴。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM