shiro安全控制目录
session一般是存储在单机的内存中,但是在集群环境中,session可以存储在公用的缓存服务器上。本文主要介绍通过Shiro管理session。Shiro下的session管理。并将session缓存到redis中。这样就可以在集群环境中共享session。
1. XML配置
在web.xml配置DelegatingFilterProxy拦截器代理,将拦截器交由Spring容器管理,Spring bean默认使用<filter-name>的名称。
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2. shiro配置
这个就是由spring管理的拦截器。其中的会话管理器sessionManager
,由安全管理器securityManager
进行配置
<!-- Shiro主过滤器本身功能十分强大,其强大之处就在于它支持任何基于URL路径表达式的、自定义的过滤器的执行 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口,这个属性是必须的 -->
<property name="securityManager" ref="securityManager"/>
<!-- 要求登录时的链接,非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="/login"/>
<!-- 用户访问未对其授权的资源时,所显示的连接 -->
<property name="unauthorizedUrl" value="/views/error/403.jsp"/>
<property name="filterChainDefinitions">
<value>
<!-- Shiro 过滤链的定义 -->
/login/** = anon <!-- 对于登录相关不进行鉴权 -->
/static/** = anon <!-- 静态资源不进行鉴权 -->
/** = user
</value>
</property>
3. securityManager配置
自定义Realm的理解中Realm实现了认证和授权,而sessionManager实现了用户自定义的会话管理。一般在web中,有两种会话管理器:
(1)DefaultWebSessionManager(由shiro维护会话)
(2)ServletContainerSessionManager(由servlet维护会话)
无论采取哪种会话管理,需要注意的是subject和request获取的session是同一个session。
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--自定义Realm,实现授权和认证-->
<property name="realm" ref="myRealm"/>
<property name="rememberMeManager" ref="rememberMeManager"/>
<!-- sessionManager管理配置 -->
<property name="sessionManager" ref="defaultWebSessionManager"/>
</bean>
3. defaultWebSessionManager配置
用户自定义的sessionManager对象,用户可以自定义sessionDAO
对象,完成session对象的持久化。
<bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!--全局session过期时间:30分钟操作会覆盖web.xml文件中的超时时间配置 -->
<property name="globalSessionTimeout" value="1800000"/>
<!---【本编重点】session保存到redis中-->
<property name="sessionDAO" ref="customShiroSessionDAO"/>
<!-- 所有的session一定要将id设置到Cookie之中,需要提供有Cookie的操作模版 -->
<property name="sessionIdCookie" ref="simpleCookie"/>
<!--定时扫描超时session-->
<!-- session 监听,可以多个。 -->
<property name="sessionListeners">
<list>
<ref bean="customSessionListener"/>
</list>
</property>
<!--session关闭调度器-->
<property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
<!-- 需要让此session可以使用该定时调度器进行检测 -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<!--删除过期的session-->
<property name="deleteInvalidSessions" value="true"/>
</bean>
需要注意的参数
4. redisShiroSessionDAO配置
我们要将Redis和Session进行整合,就要实现SessionDAO接口。实现SessionDAO和AbstractSessionDAO的方法。
<!--定制的SessionDao,继承AbstractSessionDAO接口-->
<bean id="customShiroSessionDAO" class="com.exam.CustomShiroSessionDAO">
<property name="shiroSessionRepository" ref="jedisShiroSessionRepository"/>
</bean>
<bean id="jedisShiroSessionRepository" class="com.exam.JedisShiroSessionRepository">
<property name="jedisManager" ref="jedisManager"/>
</bean>
<bean id="jedisManager" class="com.exam.common.redis.JedisManager">
<property name="myJedisCluster" ref="jedisCluster"/>
</bean>
<!-- redis client集群配置,初始化Redis集群 -->
<bean id="jedisCluster" class="com.exam.common.redis.MyJedisCluster" scope="singleton" init-method="init">
<property name="maxRedirections" value="${core.redis.maxRedirections}"/>
<property name="timeout" value="${core.redis.timeout}"/>
<property name="dataTimeOut" value="${core.redis.dataTimeOut}"/>
<property name="poolMaxTotal" value="${core.redis.pool.maxTotal}"/>
<property name="poolMaxIdle" value="${core.redis.pool.maxIdle}"/>
<property name="poolMinIdle" value="${core.redis.pool.minIdle}"/>
<property name="soTimeout" value="${core.redis.pool.soTimeout}"/>
<property name="password" value="${core.redis.password}"/>
<property name="usePwd" value="${core.redis.usePwd}"/>
<property name="jedisClusterNode">
<list>
<map>
<entry key="host" value="${core.redis.host1}"></entry>
<entry key="port" value="${core.redis.port1}"></entry>
</map>
<map>
<entry key="host" value="${core.redis.host2}"></entry>
<entry key="port" value="${core.redis.port2}"></entry>
</map>
<map>
<entry key="host" value="${core.redis.host3}"></entry>
<entry key="port" value="${core.redis.port3}"></entry>
</map>
</list>
</property>
</bean>
2. 整合Redis集群
有些小伙伴是将Redis的配置写到xml中,jedisCluster的配置方式,但是此处采取的自定义一个类来组装Redis连接参数。并且也可以看到采用的是
init-method="init"
,即在项目初始化的时候,执行该配置方法。
1. Redis集群接口
public interface IJedisCluster {
/**
* 获取redis集群对象
*
* @return
*/
JedisCluster getJedis();
int getDataTimeOut();
<T> T get(String key, Class<T> requiredType);
void set(String key, Object valueObj);
String get(String key);
void set(String key, String value);
void del(String key);
Set<String> keys(String key);
/**
* 指定的 key 设置值及其过期时间。如果 key已经存在, SETEX命令将会替换旧的值
* @param key
* @param valueObj
* @param seconds
*/
void setex(String key ,Object valueObj,int seconds);
void setexString(String key ,String valueObj,int seconds);
/**
* 设置 key对应的值为 string类型的 value。 如果key已经存在,返回 0,不存在返回1
* @param key
* @param value
* @param expireTime
* @return
*/
int setnx(String key, String value, int expireTime);
}
2. 自定义Redis配置类
public class MyJedisCluster implements IJedisCluster {
private static final Logger log = LoggerFactory.getLogger(MyJedisCluster.class);
/**
* 默认超时时间
*/
public static final int DEFALT_TIMEOUT = 6000;
/**
* 默认最大重定向数
*/
public static final int DEFALT_MAX_REDIRECTIONS = 5;
/**
* 默认最大连接数
*/
public static final int DEFALT_MAX_TOTAL = 5;
/**
* 默认最大空闲数
*/
public static final int DEFALT_MAX_IDLE = 5;
/**
* 默认最小空闲数
*/
public static final int DEFALT_MIN_IDLE = 5;
/**
* 主机关键字
*/
public static final String KEY_HOST = "host";
/**
* 端口关键字
*/
public static final String KEY_PORT = "port";
/**
* 密码
*/
private String password ;
/**
* 返回值得超时时间
*/
private int soTimeout;
/**
* 是否适用密码
*/
private boolean usePwd;
/**
* Redis连接测试KEY
*/
private static final String CONN_TEST_KEY = "test:redis";
/**
* 超时时间
*/
private int timeout;
/**
* 会话超时时间
*/
private int dataTimeOut;
/**
* 最大重定向数
*/
private int maxRedirections;
/**
* 集群节点集合
*/
private List<Map<String, String>> jedisClusterNode;
/**
* 线程池总数
*/
private int poolMaxTotal;
/**
* 最大空闲数
*/
private int poolMaxIdle;
/**
* 最小空闲数
*/
private int poolMinIdle;
/**
* jedis集群对象
*/
private JedisCluster jedis;
public MyJedisCluster() {
}
/**
* 初始化
*/
public void init() {
log.debug("初始化redis集群连接开始");
//初始化线程池参数
setDefalt();
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxIdle(this.poolMaxIdle);
poolConfig.setMinIdle(this.poolMinIdle);
poolConfig.setMaxTotal(this.poolMaxTotal);
List<Map<String, String>> nodes = this.jedisClusterNode;
Set<HostAndPort> clusterNode = new HashSet<HostAndPort>();
for (Map<String, String> map : nodes) {
clusterNode.add(new HostAndPort(map.get(KEY_HOST), Integer.parseInt(map.get(KEY_PORT))));
}
if(this.usePwd){
this.jedis = new JedisCluster(clusterNode, this.timeout,this.soTimeout, this.maxRedirections,this.password, poolConfig);
}else{
this.jedis = new JedisCluster(clusterNode, this.timeout, this.maxRedirections, poolConfig);
}
log.debug("初始化redis集群连接结束");
log.debug("测试redis集群连接开始");
try {
jedis.set(CONN_TEST_KEY, "test");
jedis.expire(CONN_TEST_KEY, 3);
jedis.get(CONN_TEST_KEY);
} catch (Exception e) {
log.debug("测试redis集群连接失败", e);
log.error("测试redis集群连接失败" + e);
throw new CustomBusinessException(e);
}
log.debug("测试redis集群连接结束");
}
/**
* 获取redis集群对象
*/
private void setDefalt() {
if (this.poolMaxIdle == 0) {
this.poolMaxIdle = DEFALT_MAX_IDLE;
}
if (this.maxRedirections == 0) {
this.maxRedirections = DEFALT_MAX_REDIRECTIONS;
}
if (this.poolMaxTotal == 0) {
this.poolMaxTotal = DEFALT_MAX_TOTAL;
}
if (this.poolMinIdle == 0) {
this.poolMinIdle = DEFALT_MIN_IDLE;
}
}
//节省篇幅,省略getter和setter方法
public <T> T get(String key, Class<T> requiredType) {
String value = jedis.get(key);
if(value == null) {
return null;
}
return SerializeUtil.deserialize(value, requiredType);
}
//redis的操作API方法
public void set(String key, Object valueObj) {
String value = SerializeUtil.serialize(valueObj);
jedis.set(key, value);
//log.debug("缓存置入数据 key:{} value:{}", key, value);
return;
}
public void setex(String key, Object valueObj,int seconds) {
String value = SerializeUtil.serialize(valueObj);
jedis.setex(key, seconds, value);
return;
}
public void setexString(String key, String valueObj,int seconds) {
jedis.setex(key, seconds, valueObj);
return;
}
public int setnx(String key, String value, int expireTime) {
if (jedis == null) {
init();
}
Long result = jedis.setnx(key, value);
jedis.expire(key, expireTime);
return result.intValue();
}
public String get(String key) {
return jedis.get(key);
}
public void set(String key, String value){
jedis.set(key, value);
}
public void del(String key) {
jedis.del(key);
}
public Set<String> keys(String key) {
Set<String> set = jedis.hkeys(key);
return set;
}
}
3. 工具类
public class SerializeUtil {
public static byte[] serialize(Object value) {
if (value == null) {
throw new NullPointerException("Can't serialize null");
}
byte[] rv = null;
ByteArrayOutputStream bos = null;
ObjectOutputStream os = null;
try {
bos = new ByteArrayOutputStream();
os = new ObjectOutputStream(bos);
os.writeObject(value);
os.close();
bos.close();
rv = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
System.out.println("serialize error");
}
return rv;
}
public static Object deserialize(byte[] in) {
return deserialize(in, Object.class);
}
@SuppressWarnings("unchecked")
public static <T> T deserialize(byte[] in, Class<T> requiredType) {
Object rv = null;
ByteArrayInputStream bis = null;
ObjectInputStream is = null;
try {
if (in != null) {
bis = new ByteArrayInputStream(in);
is = new ObjectInputStream(bis);
rv = is.readObject();
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("deserialize error");
}
return (T) rv;
}
}
JDK1.7以上,流实现的是
AutoCloseable
接口,即执行完try方法(即使执行失败)自动关闭资源。
3. JedisManager的管理
获取JedisCluster集群对象,完成增该删查等Redis操作。
public class JedisManager {
//Spring在配置文件中注入的
private MyJedisCluster myJedisCluster;
public String getValueBykey(String keyStr){
JedisCluster jedisCluster = myJedisCluster.getJedis();
return jedisCluster.get(keyStr);
}
public void deleteByKey(String keyStr){
JedisCluster jedisCluster = myJedisCluster.getJedis();
jedisCluster.del(keyStr);
}
public void setValueByKey(String keyStr, String valueStr, int expireTime) {
JedisCluster jedisCluster = myJedisCluster.getJedis();
jedisCluster.set(keyStr, valueStr);
if (expireTime > 0) {
jedisCluster.expire(keyStr, expireTime);
}
}
public MyJedisCluster getMyJedisCluster() {
return myJedisCluster;
}
public void setMyJedisCluster(MyJedisCluster myJedisCluster) {
this.myJedisCluster = myJedisCluster;
}
}
4. ShiroSessionRepository的管理
在得到的JedisManager之后,开始操作Shiro的Session处理。
1. 定义ShiroSessionRepository接口
对Session操作的接口。
public interface ShiroSessionRepository {
void saveSession(Session session);
void deleteSession(Serializable sessionId);
Session getSession(Serializable sessionId);
Collection<Session> getAllSessions();
}
2. 使用Redis实现上面的接口
public class JedisShiroSessionRepository implements ShiroSessionRepository {
private Logger logger = LoggerFactory.getLogger(JedisShiroSessionRepository.class);
private JedisManager jedisManager;
//将SessionId:Session存入Redis中
public void saveSession(Session session) {
if (session == null || session.getId() == null)
throw new NullPointerException("session is empty");
try {
byte[] key = SerializeUtil.serialize(buildRedisSessionKey(session.getId()));
byte[] value = SerializeUtil.serialize(session);
String keyStr = Base64.encode(key);
String valueStr = Base64.encode(value);
int expireTime = Constant.SESSION_VAL_TIME_SPAN;
jedisManager.setValueByKey(keyStr, valueStr, expireTime);
freshSessionId(session);
} catch (Exception e) {
e.printStackTrace();
System.out.println("save session error");
}
}
public void deleteSession(Serializable id) {
if (id == null) {
throw new NullPointerException("session id is empty");
}
try {
byte[] idByte = SerializeUtil.serialize(buildRedisSessionKey(id));
String idStr = Base64.encode(idByte);
jedisManager.deleteByKey(idStr);
} catch (Exception e) {
e.printStackTrace();
System.out.println("delete session error");
}
}
public Session getSession(Serializable id) {
if (id == null)
throw new NullPointerException("session id is empty");
Session session = null;
try {
byte[] idByte = SerializeUtil.serialize(buildRedisSessionKey(id));
String idStr = Base64.encode(idByte);
String valueStr = jedisManager.getValueBykey(idStr);
if(valueStr != null){
byte[] valueByte = Base64.decode(valueStr);
session = SerializeUtil.deserialize(valueByte, Session.class);
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("get session error");
}
return session;
}
public Collection<Session> getAllSessions() {
Set<Session> sessions = new HashSet<Session>();
//TODO,查询Redis中Constant.REDIS_SHIRO_SESSION前缀的变量。放入Set中。
return sessions;
}
//Constant.REDIS_SHIRO_SESSION前缀
private String buildRedisSessionKey(Serializable sessionId) {
return Constant.REDIS_SHIRO_SESSION + sessionId;
}
public JedisManager getJedisManager() {
return jedisManager;
}
public void setJedisManager(JedisManager jedisManager) {
this.jedisManager = jedisManager;
}
private void freshSessionId(Session session) {
//Session中获取userId
Object userId= session.getAttribute(Constant.GET_USER_ID);
if(accountObj == null) {
return;
}
String sessionId = null;
try {
sessionId= Base64.encode(SerializeUtil.serialize(buildRedisSessionKey(session.getId())));
} catch (Exception e) {
logger.error("刷新sessionId出错", e);
}
//将userId和sessionId保存到Redis中。
jedisManager.setValueByKey("IS_LOGIN" + userId, sessionId, Constant.SESSION_VAL_TIME_SPAN);
}
}
5. 实现CustomShiroSessionDAO接口
实现AbstractSessionDAO接口,将Session保存在Redis中。
public class CustomShiroSessionDAO extends AbstractSessionDAO {
private ShiroSessionRepository shiroSessionRepository;
public void update(Session session) throws UnknownSessionException {
BASE64Encoder encoder = new BASE64Encoder();
getShiroSessionRepository().saveSession(session);
}
public void delete(Session session) {
if (session == null) {
return;
}
Serializable id = session.getId();
if (id != null) {
getShiroSessionRepository().deleteSession(id);
}
//TODO if session is too large,when session destory clear shiro cache
}
public Collection<Session> getActiveSessions() {
return getShiroSessionRepository().getAllSessions();
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
getShiroSessionRepository().saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return getShiroSessionRepository().getSession(sessionId);
}
public ShiroSessionRepository getShiroSessionRepository() {
return shiroSessionRepository;
}
public void setShiroSessionRepository(
ShiroSessionRepository shiroSessionRepository) {
this.shiroSessionRepository = shiroSessionRepository;
}
}
最终实现的效果是:操作session信息最终保存到了Redis中。
推荐阅读:
https://www.sojson.com/blog/137.html