企业级会话管理概述
在一套完整的企业级开发架构设计过程之中你会发现以下几个部分是其核心的组成:
-
一套完整的RPC业务中心,利用业务中心可以有效的实现业务分割,同时利用微架构的概念也可以更好地去进行业务的设计(微架构本身是存在有缺陷,多个业务之间有可能出现互相调用的依赖问题,允许忽略)
-
在整个的企业的项目开发之中认证和授权是一个非常重要的话题,如果没有严格的认证和授权的控制机制,会感觉到你的系统像在裸奔一样(WEB端);
-
数据的缓存设计非常重要,对于缓存可以是用单主机的缓存控制,也可以采用缓存数据库进行分布式的缓存处理;
-
在一个企业之中应该提供有一个统一的用户认证服务,即:所有的服务都可以依赖于此认证机制实现处理;
所以现在对于任何一个企业级的架构设计来讲,都需要去努力解决以上的问题, 之所以现在的开发框架之中Shiro非常的流程是因为它可以帮助所有的开发者快速的构建一个符合于现在企业级的认证管理处理方案,其中也包括了它自己的缓存方案和单点登录解决方案。
以一个当前的企业设计开发框架为例说明其发展的过程,如果按照一个最为传统的项目设计,那么此时在真个项目之中,应该包含有如下的几个组成部分:
-
RPC业务端(Dubbo):实现业务层的定义和数据层的操作:
-
经典方案:Spring + Hibernate,处理速度很慢;
-
后续方案:Spring + MyBatis、Spring+JPA;
-
-
PRC消费端(WEB):实现业务调用以及认证授权的检测处理操作:
-
第一代方案:Spring + SpringSecurity;
-
第二代方案:Spring + Shiro;
-
当前的架构之中并没有过多的去考虑到WEB端的优化(缓存配置以及单点登录设计)。
在整个Shiro之中realm为了避免重复执行,往往采用的形式是:将认证于授权数据直接通过EHCache进行缓存。
但是在这之中实际上还存在有两个情况:用户的密码会存在有尝试失败的次数限制,重复登录问题;
如果一个项目之中可能有若干个子系统出现,那么这些子系统不可能都做各自的登录处理,应该有一个统一的登录控制端提供认证服务(只提供认证服务,授权是不同的)。
所以在一个标准的企业架构设计过程之中,会有无数个子系统,那么对于这些子系统都需要有一个完整的登录认证解决方案,所以需要使用Redis来做单点登录处理,同时还需要考虑到用户数据的缓存问题,那么就涉及到了分布式缓存处理操作。
RedisCache缓存控制工具
对于项目之中的缓存,如果现在只是进行了单机的配置,那么现在可以使用的方案就是EHCache解决方案,可是如果放在集群之中,EHCache会有明显的性能问题,那么就需要单独创建Redis缓存。
既然要进行Redis的缓存处理操作,那么首先就需要提供有Redis的相关处理类,而这个处理类需要一个重要的说明:要利用SpringData提供的相关的JedisConnectionPool的处理操作来完成吗,但是不建议使用RedisTemplate来处理,因为这个类如果要进行一些系统序列化的时候有可能会出现无法反序列化的情况。那么最好的做法就是进行自定义的redis工具类的编写。
1、【enterpriseauth-ssm-cache】在项目之中配置好需要使用到的Redis、Spring-Data相关依赖支持包;
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
2、【enterpriseauth-ssm-cache】配置src/main/profiles/dev/config/redis.properties配置文件,进行Redis连接的相关配置
-
启动Redis服务:/usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf
-
进入到Redis客户端:/usr/local/redis/bin/redis-cli -h 192.168.136.128 -p 6379 -a mldnjava
# Redis的连接主机地址
redis.host=192.168.136.128
# Redis连接端口号
redis.port=6379
# Redis的认证信息,认证信息密码
redis.password=mldnjava
# Redis连接的超时时间
redis.timeout=2000
# 设置最大的可用连接数
redis.pool.maxTotal=100
# 最新维持的可用连接数
redis.pool.maxIdle=20
# 最大等待时间
redis.pool.maxWaitMillis=2000
# 是否要返回可用的连接
redis.pool.testOnBorrow=true
# 配置要使用的数据库的编号(不配置默认就是0)
redis.db.0=0
3、【enterpriseauth-ssm-cache】配置src/main/resources/spring/spring-redis.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"
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-4.3.xsd">
<!-- 首先进行Jedis连接池的相关配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxTotal" value="${redis.pool.maxTotal}"/> <!-- 最大可用连接数 -->
<property name="maxIdle" value="${redis.pool.maxIdle}"/> <!-- 最小维持连接数 -->
<property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}"/> <!-- 最大等待时间 -->
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}"/> <!-- 确保取得可用连接 -->
</bean>
<!-- 进行ConnectionFactory的配置 —— 配置第一个连接池 -->
<bean id="connectionFactory0" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="poolConfig" ref="jedisPoolConfig"/> <!-- 引用进行连接池的配置项 -->
<property name="hostName" value="${redis.host}"/> <!-- Redis的连接地址 -->
<property name="port" value="${redis.port}"/> <!-- Redis的连接端口 -->
<property name="password" value="${redis.password}"/> <!-- 定义的是连接密码,认证密码 -->
<property name="timeout" value="${redis.timeout}"/> <!-- 连接的超时时间 -->
<property name="database" value="${redis.db.0}"/>
</bean>
</beans>
4、【enterpriseauth-ssm-cache】如果要想与Shiro进行结合处理,强烈建议此时定义一个单独的程序类,这个程序类实现Cache接口,但是考虑到后续的重用的问题,建议这个类设置为一个抽象类;
package cn.mldn.enterpriseauth.ssm.util.cache.abs;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.util.SerializationUtils;
public abstract class AbstractRedisCache<K, V> implements Cache<K, V> {
private JedisConnectionFactory connectionFactory ; // 定义一个Jedis连接工厂
/**
* 由于RedisConnection的所有操作都是以字节数组的形式出现的,所以建议直接设置一个工具方法转换
* @param obj 要转换的任意对象
* @return 对象的字节数组
*/
protected byte [] objectToArray(Object obj) {
return SerializationUtils.serialize(obj) ;
}
/**
* 将字节数组重新变为Object对象
* @param data 要转变的字节数组
* @return 目标处理对象
*/
protected Object byteArrayToObject(byte data[]) {
return SerializationUtils.deserialize(data) ;
}
@Override
public V get(K key) throws CacheException {
V obj = null ;
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
obj = (V) this.byteArrayToObject(connection.get(this.objectToArray(key))) ;
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
return obj;
}
@Override
public V put(K key, V value) throws CacheException {
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
connection.set(this.objectToArray(key), this.objectToArray(value)) ;
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
return value;
}
/**
* 设置一个Redis的数据操作,其本身需要一个失效时间
* @param key 要设置的key
* @param value 要设置的value
* @param expire 失效时间
* @return 返回Value
* @throws CacheException 缓存异常
*/
public V putEx(K key, V value,Long expire) throws CacheException {
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
connection.setEx(this.objectToArray(key), expire, this.objectToArray(value)) ;
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
return value;
}
/**
* 设置一个Redis的数据操作,其本身需要一个失效时间
* @param key 要设置的key
* @param value 要设置的value
* @param expire 失效时间
* @return 返回Value
* @throws CacheException 缓存异常
*/
public V putEx(K key, V value,String expire) throws CacheException {
return this.putEx(key, value, Long.parseLong(expire)) ;
}
@Override
public V remove(K key) throws CacheException {
V obj = null ;
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
obj = (V) this.byteArrayToObject(connection.get(this.objectToArray(key))) ;
connection.del(this.objectToArray(key)) ;
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
return obj;
}
@Override
public void clear() throws CacheException {
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
connection.flushDb(); // 删除当前数据库之中的全部数据
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
}
@Override
public int size() {
int size = 0 ;
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
Set<byte[]> keys = connection.keys(this.objectToArray("*")) ;
size = keys.size() ;
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
return size ;
}
@Override
public Set<K> keys() {
Set<K> allKeys = new HashSet<K>() ; // 返回全部的Key信息
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
Set<byte[]> keys = connection.keys(this.objectToArray("*")) ;
for (byte[] key : keys) {
allKeys.add((K) this.byteArrayToObject(key));
}
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
return allKeys;
}
@Override
public Collection<V> values() {
Set<V> allValues = new HashSet<V>() ; // 返回全部的Key信息
RedisConnection connection = this.connectionFactory.getConnection() ;
try {
Set<byte[]> keys = connection.keys(this.objectToArray("*")) ;
for (byte[] key : keys) {
allValues.add((V) this.byteArrayToObject(connection.get(key)));
}
} catch (Exception e) {}
connection.close(); // 将连接交回到连接池之中
return allValues;
}
public void setConnectionFactory(JedisConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
}
5、【enterpriseauth-ssm-cache】建立RedisCache的具体处理类:
package cn.mldn.enterpriseauth.ssm.util.cache;
import cn.mldn.enterpriseauth.ssm.util.cache.abs.AbstractRedisCache;
public class RedisCache<K, V> extends AbstractRedisCache<K, V> {
}
6、【enterpriseauth-ssm-cache】建立一个专门用于进行测试的spring配置文件:
<?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:dubbo="http://code.alibabatech.com/schema/dubbo"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd
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-4.1.xsd">
<!-- 定义配置的Annotation扫描包 -->
<context:component-scan base-package="cn.mldn" />
<!-- 定义所有要导入的属性文件的路径 -->
<context:property-placeholder location="classpath:config/redis.properties"/>
<!-- 定义要读取的其它配置的Spring文件 -->
<import resource="classpath:spring/spring-redis.xml"/>
<!-- 配置RedisCache的类定义 -->
<bean id="redisCache" class="cn.mldn.enterpriseauth.ssm.util.cache.RedisCache">
<property name="connectionFactory" ref="connectionFactory0"/>
</bean>
</beans>
7、【enterpriseauth-ssm-cache】编写一个测试类,来测试是否可用正常实现Redis数据操作:
package cn.mldn.enterpriseauth.test;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import cn.mldn.enterpriseauth.ssm.util.cache.RedisCache;
@ContextConfiguration(locations= {"classpath:config/test-spring.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class TestRedisCache {
@Resource
private RedisCache<Object,Object> redisCache ;
@Test
public void testPut() {
this.redisCache.put("mldn", "www.mldn.cn") ;
}
@Test
public void testGet() {
System.err.println(this.redisCache.get("mldn"));
}
}
当缓存配置完成之后,那么后续的问题就在于如何将此缓存组件与Shiro的CacheManager混合在一起使用了。
Redis缓存认证与授权信息
在整个的Shiro里面核心的问题就是认证与授权的管理操作,而对于认证与授权的管理操作为了避免重复性的去执行数据库或者是远程RPC端的服务调用,所以需要利用缓存来完成。
在最初的时候可用发现在项目之中spring-redis.xml配置文件里面使用了如下的一个缓存操作:
<!-- 进行Shiro中缓存管理器的配置,现在就实现了EhCache-Shiro组件的配置 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<!-- 配置EHCache组件要使用的配置文件的路径 -->
<property name="cacheManagerConfigFile" value="classpath:shiro/ehcache.xml"/>
</bean>
在Shiro中的EHCache配置文件里面会发现有如下的重要的缓存信息配置项:
- 【REALM缓存】authorizationCache:进行授权信息缓存处理操作;
- 【REALM缓存】authenticationCache:进行认证信息缓存处理操作;
- 【SESSIONDAO】shiro-activeSessionCache:所有当前活跃的session的缓存信息;
实际上现在唯一使用的就是"shiro-activeSessionCache"缓存的信息,其在SessionDAO中配置有所体现:
<!-- 所有的SessionID最终一定要进行数据的存储操作,那么既然要进行存储,现在就需要定义一个Session的数据处理操作 -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<!-- 定义在进行Session管理之中所使用的缓存策略 -->
* <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
<!-- 获得生成的SessionID数据 -->
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
</bean>
但是考虑到实际开发之中的应用,此时的缓存配置不再使用EHCache配置了,应该采用Redis实现处理操作,首先来观察CacheManager接口定义:
<K, V> Cache<K, V> getCache(String var1) throws CacheException;
该接口之中只有一个getCache()方法返回一个Cache接口对象;
1、【enterpriseauth-ssm-cache】建立一个RedisCacheManager程序类,该类一定要实现CacheManager处理接口:
<!-- 进行Shiro中缓存管理器的配置,现在就实现了Redis-Shiro组件的配置 -->
* <bean id="cacheManager" class="cn.mldn.enterpriseauth.ssm.util.cache.manager.RedisCacheManager">
<!-- 配置EHCache组件要使用的配置文件的路径 -->
<property name="connectionFactory" ref="connectionFactory0"/>
* </bean>
<!-- 定义项目之中要使用的认证与授权处理的Realm对象,该对象一定要配置到安全管理器之中 -->
<bean id="memberRealm" class="cn.mldn.enterpriseauth.ssm.realm.MemberRealm">
* <property name="cachingEnabled" value="true"/> <!-- 启用缓存 -->
* <property name="authenticationCachingEnabled" value="true"/><!-- 启用认证缓存 -->
<!-- 设置认证缓存的名字,该缓存的操作统一通过RedisCacheManager管理 -->
* <property name="authenticationCacheName" value="authenticationCache"/>
* <property name="authorizationCachingEnabled" value="true"/> <!--启用授权缓存-->
<!-- 设置授权缓存的名字,该缓存的操作统一通过RedisCacheManager管理 -->
* <property name="authorizationCacheName" value="authorizationCache"/>
<property name="credentialsMatcher">
<bean class="cn.mldn.enterpriseauth.ssm.realm.matcher.DefaultCredentialsMatcher"/>
</property>
</bean>
当项目重新启动之后将可用获得如下的几个信息:authenticationCache、authorizationCache。
2、【enterpriseauth-ssm-cache】但是对于缓存需要特别注意的是,除了认证和授权之外还需要处理一下session缓存,那么这个缓存就需要修改sessionDAO的配置项
<!-- 所有的SessionID最终一定要进行数据的存储操作,那么既然要进行存储,现在就需要定义一个Session的数据处理操作 -->
<bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
<!-- 定义在进行Session管理之中所使用的缓存策略 -->
* <property name="activeSessionsCacheName" value="activeSessionCache"/>
<!-- 获得生成的SessionID数据 -->
<property name="sessionIdGenerator" ref="sessionIdGenerator"/>
<!-- 将SessionDAO缓存也统一交给CacheManager负责管理 -->
* <property name="cacheManager" ref="cacheManager"/>
</bean>
当配置完成之后启动时可用发现又会出现一个缓存信息名称:“activeSessionCache”,这个信息是在创建Session的时候才会出现。
3、【enterpriseauth-ssm-cache】现在有这么多缓存操作,再实际的开发之中肯定要将这些信息保存再不同的缓存数据库之中,这样一来RedisCacheManager就需要做更多的处理了,那么为了可用管理所有的缓存,利用Map来完成处理(ConnectionFactory),修改RedisCacheManager程序类,进行缓存管理的处理。
package cn.mldn.enterpriseauth.ssm.util.cache.manager;
import cn.mldn.enterpriseauth.ssm.util.cache.RedisCache;
import cn.mldn.enterpriseauth.ssm.util.cache.abs.AbstractRedisCache;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class RedisCacheManager implements CacheManager {
// 建立一个负责管理所有缓存处理类的集合操作,要求保证线程安全
private final ConcurrentHashMap<String, Cache<Object, Object>> CACHES = new ConcurrentHashMap<>();
private Map<String, JedisConnectionFactory> connectionFactoryMap;
@Override
public Cache<Object, Object> getCache(String name) throws CacheException {
Cache<Object, Object> cache = CACHES.get(name);
if (cache == null){ //还没有创建该缓存管理对象就需要进行对象的创建处理
AbstractRedisCache<Object, Object> abstractRedisCache = null;
if ("authenticationCache".equals(name)){ //要获取的是认证缓存
abstractRedisCache = new RedisCache<>();
abstractRedisCache.setConnectionFactory(this.connectionFactoryMap.get("authenticationCache"));
}else if("authorizationCache".equals(name)){ //获得授权缓存
abstractRedisCache = new RedisCache<>();
abstractRedisCache.setConnectionFactory(this.connectionFactoryMap.get("authorizationCache"));
}else if ("activeSessionCache".equals(name)){ //获得session缓存activeSessionCache
abstractRedisCache = new RedisCache<>();
abstractRedisCache.setConnectionFactory(this.connectionFactoryMap.get("activeSessionCache"));
}
cache = abstractRedisCache;
CACHES.put(name, cache); //防止随后重复取出
}
return cache;
}
public void setConnectionFactoryMap(Map<String, JedisConnectionFactory> connectionFactoryMap) {
this.connectionFactoryMap = connectionFactoryMap;
}
}
4、【enterpriseauth-ssm-cache】本次预计使用三台Redis数据库,但是只是通过一台主机的不同数据库做出模拟,模拟清单。
192.168.136.128、db-0、authentication
192.168.136.128、db-1、authorization
192.168.136.128、db-2、activeSessionCache
5、【enterpriseauth-ssm-cache】修改redis.properties配置文件,追加多个数据库的配置项:
# 配置要使用的数据库的编号(不配置默认就是0)
redis.db.authentication=0
redis.db.authorization=1
redis.db.activeSessionCache=2
6、【enterpriseauth-ssm-cache】修改spring-redis.xml配置文件,追加多个连接工厂配置:
<!-- 配置认证缓存的数据库的连接 -->
<bean id="connectionFactoryAuthentication" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="poolConfig" ref="jedisPoolConfig"/> <!-- 引用进行连接池的配置项 -->
<property name="hostName" value="${redis.host}"/> <!-- Redis的连接地址 -->
<property name="port" value="${redis.port}"/> <!-- Redis的连接端口 -->
<property name="password" value="${redis.password}"/> <!-- 定义的是连接密码,认证密码 -->
<property name="timeout" value="${redis.timeout}"/> <!-- 连接的超时时间 -->
<property name="database" value="${redis.db.authentication}"/>
</bean>
<!-- 配置授权缓存的数据库的连接 -->
<bean id="connectionFactoryAuthorization" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="poolConfig" ref="jedisPoolConfig"/> <!-- 引用进行连接池的配置项 -->
<property name="hostName" value="${redis.host}"/> <!-- Redis的连接地址 -->
<property name="port" value="${redis.port}"/> <!-- Redis的连接端口 -->
<property name="password" value="${redis.password}"/> <!-- 定义的是连接密码,认证密码 -->
<property name="timeout" value="${redis.timeout}"/> <!-- 连接的超时时间 -->
<property name="database" value="${redis.db.authorization}"/>
</bean>
<!-- 配置Session缓存的数据库的连接 -->
<bean id="connectionFactoryActiveSessionCache" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="poolConfig" ref="jedisPoolConfig"/> <!-- 引用进行连接池的配置项 -->
<property name="hostName" value="${redis.host}"/> <!-- Redis的连接地址 -->
<property name="port" value="${redis.port}"/> <!-- Redis的连接端口 -->
<property name="password" value="${redis.password}"/> <!-- 定义的是连接密码,认证密码 -->
<property name="timeout" value="${redis.timeout}"/> <!-- 连接的超时时间 -->
<property name="database" value="${redis.db.activeSessionCache}"/>
</bean>
7、【enterpriseauth-ssm-cache】修改CacheManager定义
<bean id="cacheManager" class="cn.mldn.enterpriseauth.ssm.util.cache.manager.RedisCacheManager">
<property name="connectionFactoryMap">
<map>
<!-- 配置认证的缓存连接池 -->
<entry key="authenticationCache" value-ref="connectionFactoryAuthentication"/>
<!-- 配置授权的缓存连接池 -->
<entry key="authorizationCache" value-ref="connectionFactoryAuthorization"/>
<!-- 配置Session的缓存连接池 -->
<entry key="activeSessionCache" value-ref="connectionFactoryActiveSessionCache"/>
</map>
</property>
</bean>
正常启动之后可以按照之前的模式进行登录的处理操作。
密码尝试次数控制
在实际的项目开发过程之中,尤其是大规模的系统架构之中为了保证账户的安全性,往往是不可能让用户无限制的去尝试密码的。所以往往会设置一个尝试的次数,而且关键性的问题,这种操作需要应付恶意的破解操作,验证码可以保证操作的性能,同时如果现在要是有很多的用户尝试密码登录也应该考虑保证这些用户的处理速度,那么最好的做法是将尝试的信息记录保存在Redis数据库里面。
1、【enterpriseauth-ssm-retry】修改redis.properties配置文件,追加一个新的数据库配置项:
redis.db.retryCount=3
2、【enterpriseauth-ssm-retry】修改spring-redis.xml配置文件,进行数据库连接的配置项:
<!-- 配置密码尝试次数的Redis连接池 -->
<bean id="connectionFactoryRetryCount" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="poolConfig" ref="jedisPoolConfig"/> <!-- 引用进行连接池的配置项 -->
<property name="hostName" value="${redis.host}"/> <!-- Redis的连接地址 -->
<property name="port" value="${redis.port}"/> <!-- Redis的连接端口 -->
<property name="password" value="${redis.password}"/> <!-- 定义的是连接密码,认证密码 -->
<property name="timeout" value="${redis.timeout}"/> <!-- 连接的超时时间 -->
<property name="database" value="${redis.db.retryCount}"/>
</bean>
3、【enterpriseauth-ssm-retry】在Shiro里面所有的缓存操作都应该通过CacheManager来获得,那么就需要追加新的配置:
修改spring-shiro.xml配置文件,在CacheManager之中配置新的数据库连接池:
<bean id="cacheManager" class="cn.mldn.enterpriseauth.ssm.util.cache.manager.RedisCacheManager">
<property name="connectionFactoryMap">
<map>
<!-- 配置认证的缓存连接池 -->
<entry key="authenticationCache" value-ref="connectionFactoryAuthentication"/>
<!-- 配置授权的缓存连接池 -->
<entry key="authorizationCache" value-ref="connectionFactoryAuthorization"/>
<!-- 配置Session的缓存连接池 -->
<entry key="activeSessionCache" value-ref="connectionFactoryActiveSessionCache"/>
<!-- 配置密码尝试次数的数据库连接池 -->
<entry key="retryCount" value-ref="connectionFactoryRetryCount"/>
</map>
</property>
</bean>
4、【enterpriseauth-ssm-retry】处理CacheManager对象的定义:
@Override
public Cache<Object, Object> getCache(String name) throws CacheException {
Cache<Object, Object> cache = CACHES.get(name);
if (cache == null){ //还没有创建该缓存管理对象就需要进行对象的创建处理
AbstractRedisCache<Object, Object> abstractRedisCache = null;
if ("authenticationCache".equals(name)){ //要获取的是认证缓存
abstractRedisCache = new RedisCache<>();
abstractRedisCache.setConnectionFactory(this.connectionFactoryMap.get("authenticationCache"));
}else if("authorizationCache".equals(name)){ //获得授权缓存
abstractRedisCache = new RedisCache<>();
abstractRedisCache.setConnectionFactory(this.connectionFactoryMap.get("authorizationCache"));
}else if ("activeSessionCache".equals(name)){ //获得session缓存activeSessionCache
abstractRedisCache = new RedisCache<>();
abstractRedisCache.setConnectionFactory(this.connectionFactoryMap.get("activeSessionCache"));
}else if ("retryCount".equals(name)){
abstractRedisCache = new RedisCache<>();
abstractRedisCache.setConnectionFactory(this.connectionFactoryMap.get("retryCount"));
}
cache = abstractRedisCache;
}
return cache;
}
5、【enterpriseauth-ssm-retry】密码的验证时在Realm之后进行的,是在密码匹配器之中进行处理,程序类为:cn.mldn.enterpriseauth.ssm.realm.matcher.DefaultCredentialsMatcher;
package cn.mldn.enterpriseauth.ssm.realm.matcher;
import cn.mldn.enterpriseauth.ssm.util.cache.RedisCache;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import cn.mldn.util.enctype.PasswordUtil;
import org.apache.shiro.cache.CacheManager;
import java.util.concurrent.atomic.AtomicInteger;
// 定义一个密码加密处理的密码匹配其
public class DefaultCredentialsMatcher extends SimpleCredentialsMatcher {
private RedisCache<Object, Object> cache; // 整体的操作一定需要有一个Redis缓存配置
private String expire = "50"; // 默认是50秒失效
private int maxRetryCount = 5; // 最多可以试验5次
/**
* 实现登录计数处理,如果要进行用户的登录计数,那么就应该将用户名作为数据的key
* @param mid 要保存的数据库中的key信息
*/
private void retry(String mid){
// 之所以使用AtomicInteger原子操作类是为了防止多个用户并发操作时候的不同步问题
AtomicInteger num = (AtomicInteger) this.cache.get(mid); //获取保存的对象
if (num == null){ //现在还没有相关的数据,用户没有登录过或者登录成功
num = new AtomicInteger(0); //设置一个登录次数
this.cache.put(mid, num); //保存信息
}else { //如果现在以及保存有登录次数(你至少已经失败过一次)
if (num.incrementAndGet() > this.maxRetryCount){ // 超过了最大尝试次数
this.cache.putEx(mid, num, this.expire); //设置一个失效时间
throw new ExcessiveAttemptsException("用户“"+mid+"”密码尝试次数过多,请稍后再试!");
}else { //如果现在不够最大次数
this.cache.put(mid, num); //保存当前尝试次数
}
}
}
/**
* 当用户成功登录之后所有的数据应该被释放(删除)
* @param mid
*/
private void unlock(String mid){
this.cache.remove(mid);
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String mid = (String)token.getPrincipal(); //获取mid数据
this.retry(mid); //进行登录次数控制
// 在父类之中提供有toString()方法可以自动将传递的字符数组密码变为字符串的密码
Object tokenCredentials = PasswordUtil.encoder(super.toString(token.getCredentials())) ;
Object accountCredentials = super.getCredentials(info) ; // 获取认证处理后的密码
boolean flag = super.equals(tokenCredentials, accountCredentials); //密码检测
if (flag){ //登录成功
this.unlock(mid);
}
return flag;
}
public void setExpire(String expire) { //设置失效时间
this.expire = expire;
}
public void setMaxRetryCount(int maxRetryCount) { //设置最多的尝试次数
this.maxRetryCount = maxRetryCount;
}
public void setCacheManager(CacheManager cacheManager){
// 由于需要设置一个数据的有效时间,所以不能使用Cache标准操作,要利用其子类处理
this.cache = (RedisCache<Object, Object>) cacheManager.getCache("retryCount");
}
}
6、【enterpriseauth-ssm-retry】定义shiro.properties配置文件,设置失效时间
#定义尝试登录的最大失效时间
shiro.retry.expire=50
# 定义尝试登录的最大尝试次数
shiro.retry.max=5
7、【enterpriseauth-ssm-retry】修改MemberRealm里的定义操作,多注入一些信息
<!-- 定义项目之中要使用的认证与授权处理的Realm对象,该对象一定要配置到安全管理器之中 -->
<bean id="memberRealm" class="cn.mldn.enterpriseauth.ssm.realm.MemberRealm">
<property name="cachingEnabled" value="true"/> <!-- 启用缓存 -->
<property name="authenticationCachingEnabled" value="true"/><!-- 启用认证缓存 -->
<!-- 设置认证缓存的名字,该缓存的操作统一通过RedisCacheManager管理 -->
<property name="authenticationCacheName" value="authenticationCache"/>
<property name="authorizationCachingEnabled" value="true"/> <!--启用授权缓存-->
<!-- 设置授权缓存的名字,该缓存的操作统一通过RedisCacheManager管理 -->
<property name="authorizationCacheName" value="authorizationCache"/>
<property name="credentialsMatcher">
<bean class="cn.mldn.enterpriseauth.ssm.realm.matcher.DefaultCredentialsMatcher">
* <property name="cacheManager" ref="cacheManager"/>
* <property name="expire" value="${shiro.retry.expire}"/>
* <property name="maxRetryCount" value="${shiro.retry.max}"/>
</bean>
</property>
</bean>
这个时候就可以对账户信息进行有效的保存,当然你可以再做一些更高级的算法,例如:第一次进行临时锁定,第二次长时间锁定,第三次进行更长时间锁定。(算数求模)
并发Session访问控制
在很多的时候为了保证账号的安全性,往往会出现同一个账号不允许重复登录的问题,例如:你在使用QQ的时候如果有人用了你的账户登录你会被迫下线(后来的时候腾讯改变方案,如果你手机、电脑同时在线是允许的),对于此类的操作实际上也是可以利用Redis缓存来完成的,但是这个时候就需要做一个考虑:对于踢出的方案有两种,踢出前一个,另外是踢出后一个,肯定是踢出前一个会比较合理。
如果要想确定出要踢出的人员,则首先要解决的问题就是顺序问题,而顺序问题自然可以想到要使用队列,但是如果使用Quene队列只是实现了先进先出的功能,那么此时肯定是需要双向弹出,那么建议使用它的子接口:Deque(LinkedList处理)。现在在Redis里面应该保存的就是一个双端队列的集合信息,而redis之中的key的名称应该使用的是用户名。
1、【enterpriseauth-ssm-kickout】修改redis.properties配置文件,追加一个新的连接
redis.db.kickout=4
2、【enterpriseauth-ssm-kickout】修改spring-redis.xml配置文件,追加一个新的连接控制:
<!-- 配置保存的当前用户信息的队列缓存 -->
<bean id="connectionFactoryKickout" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="poolConfig" ref="jedisPoolConfig"/> <!-- 引用进行连接池的配置项 -->
<property name="hostName" value="${redis.host}"/> <!-- Redis的连接地址 -->
<property name="port" value="${redis.port}"/> <!-- Redis的连接端口 -->
<property name="password" value="${redis.password}"/> <!-- 定义的是连接密码,认证密码 -->
<property name="timeout" value="${redis.timeout}"/> <!-- 连接的超时时间 -->
<property name="database" value="${redis.db.kickout}"/>
</bean>
3、【enterpriseauth-ssm-kickout】修改CacheManager的配置,配置新的连接项:
4、【enterpriseauth-ssm-kickout】修改RedisCacheManager程序类,追加一个新的缓存配置项:
package cn.mldn.enterpriseauth.ssm.util.cache.manager;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import cn.mldn.enterpriseauth.ssm.util.cache.RedisCache;
import cn.mldn.enterpriseauth.ssm.util.cache.abs.AbstractRedisCache;
public class RedisCacheManager implements CacheManager {
// 建立一个负责管理所有缓存处理类的集合操作,要求保证线程安全
private final ConcurrentMap<String, Cache<Object,Object>> CACHES = new ConcurrentHashMap<>() ;
private Map<String,JedisConnectionFactory> connectionFactoryMap ;
@Override
public Cache<Object, Object> getCache(String name) throws CacheException {
Cache<Object,Object> cache = CACHES.get(name) ; // 获得已经保存的缓存对象
if (cache == null) { // 没有缓存对象
AbstractRedisCache<Object, Object> abstractCache = null ;
if ("authentication".equals(name)) { // 获得的是认证缓存
abstractCache = new RedisCache<Object,Object>() ;
abstractCache.setConnectionFactory(this.connectionFactoryMap.get("authentication"));
} else if ("authorization".equals(name)) { // 授权缓存
abstractCache = new RedisCache<Object,Object>() ;
abstractCache.setConnectionFactory(this.connectionFactoryMap.get("authorization"));
} else if ("activeSessionCache".equals(name)) {
abstractCache = new RedisCache<Object,Object>() ;
abstractCache.setConnectionFactory(this.connectionFactoryMap.get("activeSessionCache"));
} else if ("retryCount".equals(name)) {
abstractCache = new RedisCache<Object,Object>() ;
abstractCache.setConnectionFactory(this.connectionFactoryMap.get("retryCount"));
* } else if ("kickout".equals(name)) {
* abstractCache = new RedisCache<Object,Object>() ;
* abstractCache.setConnectionFactory(this.connectionFactoryMap.get("kickout"));
* }
cache = abstractCache ; // 获得缓存对象
}
return cache ;
}
// 此时需要管理多个Redis的连接控制,所以所有的连接通过外部配置
public void setConnectionFactoryMap(Map<String, JedisConnectionFactory> connectionFactoryMap) {
this.connectionFactoryMap = connectionFactoryMap;
}
}
5、【enterpriseauth-ssm-kickout】这个时候就需要采用自定义过滤器来实现该处理操作了。
package cn.mldn.enterpriseauth.ssm.filter;
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import cn.mldn.enterpriseauth.ssm.util.cache.RedisCache;
public class KickoutSessionControlFilter extends AccessControlFilter {
private String kickoutUrl ; // 剔出之后需要跳转的页面
// kickoutAfter = true:表示踢出之后的
// kickoutAfter = false:表示保留之后进来的,踢出之前的
private boolean kickoutAfter = false ; // 之前还是之后剔出
private int maxSessionCount = 1 ; // 最大的session保存个数
// 在Shiro里面注销的管理由SessionManager负责,所以需要得到SessionManager对象
private SessionManager sessionManager ;
private Cache<Object,Object> cache ; // 数据需要缓存到Redis数据库之中
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
return false; // 该操作要返回false,否则onAccessDenied()方法不执行
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 能够踢出的用户一定是在已经成功登录后的用户,如果未登录认证的用户不应该操作
Subject subject = super.getSubject(request, response) ;// Shiro的Subject
if (!subject.isAuthenticated() && !subject.isRemembered()) { // 没有登录
return true ; // 后续正常访问
} // Shiro自己维持了一套自己的Session管理
// 此时用户已经认证过了,需要接收这个Session对象,SessionManager需要做强制性注销处理
// 同时该session的数据也需要保存在Redis集合里面
Session session = subject.getSession() ; // 获得当前的Session对象
// 如果要进行Redis集合保存,那么一定需要提供有一个mid的数据信息
String mid = (String) subject.getPrincipal() ; // 当前用户id信息
// 对于登录的核心控制,需要设置有一个 保存队列,而这个保存队列在Redis里面
Deque<Serializable> allSessions = (Deque<Serializable>) this.cache.get(mid) ; // 获取已经存在的集合信息
if (allSessions == null) { // 当前没有存储过数据
allSessions = new LinkedList<Serializable>() ; // 创建新的集合
} // 判断当前的session是否存在于集合之中,因为多个路径都可以使用到此过滤器,数据为空,表示该数据未存储
if (!allSessions.contains(session.getId()) && session.getAttribute("kickout") == null) {
allSessions.push(session.getId()); // 将当前的session保存到集合之中
this.cache.put(mid, allSessions) ; // 将数据重新保存到Redis集合之中
}
try { // 判断是否已经达到了最大的session保存量
if (allSessions.size() > this.maxSessionCount) { // 已经超过了最大保存个数
Serializable kickoutSessionId = null ; // 保存要强制注销的SessionID
if(this.kickoutAfter) { // 如果是踢出后者
kickoutSessionId = allSessions.removeFirst() ; // 踢出第一个
} else { // 踢出前一个
kickoutSessionId = allSessions.removeLast() ; // 踢出后面的一个
}
this.cache.put(mid, allSessions) ;// 重新保存一次session
// 获得即将被提出的SessionID的对应的Session对象信息
Session kickoutSession = this.sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (kickoutSession != null) { // 这个用户还在呢
kickoutSession.setAttribute("kickout", true); // 你被踢出
}
}
} catch (Exception e) {}
if (session.getAttribute("kickout") != null) { // 当前操作的session需要被踢出
try {
subject.logout(); // 踢出指定的一个Session数据
} catch (Exception e) {}
super.saveRequest(request); // 记录下本次的请求操作
WebUtils.issueRedirect(request, response, this.kickoutUrl + "?kickmsg=out");
return false ; // 停止掉后续的访问服务
}
return true; // 正确只想你个后续访问服务
}
public void setMaxSessionCount(int maxSessionCount) {
this.maxSessionCount = maxSessionCount;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public void setCacheManager(CacheManager cacheManager) {
// 由于需要设置一个数据的有效时间,所以不能够使用Cache标准操作,要利用其子类处理
this.cache = (RedisCache<Object,Object>) cacheManager.getCache("kickout") ;
}
}
6、【enterpriseauth-ssm-kickout】修改spring-shiro.xml配置文件,实现踢出处理过滤的配置
<!-- 定义剔出的过滤器 -->
<bean id="kickoutFilter" class="cn.mldn.enterpriseauth.ssm.filter.KickoutSessionControlFilter">
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="kickoutUrl" value="/loginForm.action"/> <!-- 踢出后的路径 -->
<property name="maxSessionCount" value="2"/> <!-- 只允许一个用户访问 -->
</bean>
7、【enterpriseauth-ssm-kickout】在ShiroFilter之中追加此过滤器配置:
<!-- 配置文件里面已经将所有的配置交由Spring负责了,所以对于过滤的处理操作一定要交给指定的类完成配置 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 在此过滤管理之中定义要使用的安全管理器 -->
<property name="securityManager" ref="securityManager"/>
<!-- 当认证或授权出现了错误之后应该跳转到登录页面,要由Action跳转到JSP页面 -->
<property name="loginUrl" value="/loginForm.action"/>
<!-- 当授权失败之后跳转的页面路径,也是通过Action跳转 -->
<property name="unauthorizedUrl" value="/unauth.action"/>
<!-- 当登录成功之后跳转到的指定页面 -->
<property name="successUrl" value="/pages/back/welcome.action"/>
<property name="filters"> <!-- 由于现在重新定义了过滤,所以要重新追加 -->
<map>
<!-- 当执行了authc的检测的时候,明确的找到新的过滤配置类 -->
<entry key="authc" value-ref="formAuthenticationFilter"/>
<!-- 当执行了注销的操作过滤时,会执行此项配置 -->
<entry key="logout" value-ref="logoutFilter"/>
<!-- 增加踢出重复的session过滤器配置 -->
* <entry key="kickout" value-ref="kickoutFilter"/>
</map>
</property>
<!-- 定义所有访问路径处理规则 -->
<property name="filterChainDefinitions">
<value>
/logout.page=logout
/loginForm.action=authc
* /pages/**=authc,kickout
</value>
</property>
</bean>