晚是晚了一点,改补的东西还是要补上的。就怕以后你们看不懂啊- -
shiro实现session共享主要是因为他们身有一个完整的session管理实现,我们要做的也只是根据自己的业务重写shiro中要将session存储到哪,我们这里就存在redis里面了。
网上是有现成的实现方案的,地址在https://github.com/alexxiyang/shiro-redis,github上有现成的教程,我们用maven添加依赖,然后在Spring中添加一些配置就可以了。
首先在pom.xml中添加依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
我这里是直接把源码下载下来,添加到了项目中,学习了一下具体的实现机制,顺便修改了一些自己的东西,所以不要直接按照我的xml文件抄,一定要读懂spring的配置。
继续在上次的技术上添加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:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
default-lazy-init="true">
<!-- 用于扫描其他的.properties配置文件 -->
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:shiro.properties</value>
</list>
</property>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- Shiro的核心安全接口 -->
<property name="securityManager" ref="securityManager"/>
<!-- 要求登录时的链接(登录页面地址),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
<property name="loginUrl" value="${loginUrl}" />
<!-- 登录成功后要跳转的连接(一般可以在Controller中进行处理) -->
<property name="successUrl" value="${SuccessUrl}" />
<!-- 用户访问未对其授权的资源时,所显示的连接 -->
<property name="unauthorizedUrl" value="${UnauthorizedUrl}"/>
<!--配置自定义拦截器,这里是cas的登录和退出拦截器-->
<property name="filters">
<map>
<entry key="casFilter">
<bean class="org.apache.shiro.cas.CasFilter">
<!--配置验证错误时的失败页面 /main 为系统登录页面 -->
<property name="failureUrl" value="/message.jsp" />
</bean>
</entry>
<!-- 重写shiro的logout,shiro执行完logout后使其跳转到cas的登出地址,执行cas的logout-->
<entry key="logoutFilter">
<bean class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl"
value="${logoutUrl}"/>
</bean>
</entry>
</map>
</property>
<property name="filterChainDefinitions">
<value>
<!--不拦截login的url-->
/login=anon
/shiro-cas=casFilter
/logout=logout
/**=user
</value>
</property>
</bean>
<!--shiro核心管理类,这里只配置了自己的realm-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroRealm"/>
<!--加入cas的工厂-->
<property name="subjectFactory" ref="casSubjectFactory"/>
<!--加入Session管理类-->
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- shiro于数据交互的类 ,自己写的类的实现-ShiroRealmBean自己重写的类的实现 -->
<bean id="shiroRealm" class="com.tgb.shiro.realm.shiroRealm">
<!--指定cas的登录地址和前缀-->
<property name="casServerUrlPrefix" value="${casServerUrlPrefix}"/>
<property name="casService" value="${casService}"/>
</bean>
<!-- 如果要实现cas的remember me的功能,需要用到下面这个bean,并设置到securityManager的subjectFactory中 -->
<bean id="casSubjectFactory" class="org.apache.shiro.cas.CasSubjectFactory" />
<!--Session共享配置-->
<!-- session管理器 -->
<bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 设置全局会话超时时间,默认30分钟(1800000) -->
<property name="globalSessionTimeout" value="1800000" />
<!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true -->
<property name="deleteInvalidSessions" value="true" />
<!-- 会话验证器调度时间 -->
<property name="sessionValidationInterval" value="1800000" />
<!-- session存储的实现 -->
<property name="sessionDAO" ref="redisShiroSessionDAO" />
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<property name="sessionIdCookie" ref="sharesession" />
<!--缓存管理-->
<property name="cacheManager" ref="cacheManager"/>
<!-- 定时检查失效的session -->
<property name="sessionValidationSchedulerEnabled" value="true" />
</bean>
<!-- session会话存储的实现类 -->
<bean id="redisShiroSessionDAO" class="com.tgb.shiro.redis.RedisSessionDAO">
<property name="redisManager" ref="redisManager" />
</bean>
<!-- 自定义cacheManager -->
<bean id="redisManager" class="com.tgb.shiro.redis.RedisManager"/>
<!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
<bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie">
<!-- cookie的name,对应的默认是 JSESSIONID -->
<constructor-arg name="name" value="SHAREJSESSIONID" />
<!-- jsessionId的path为 / 用于多个系统共享jsessionId -->
<property name="path" value="/" />
</bean>
<!-- 自定义redisManager-redis -->
<bean id="redisCacheManager" class="com.tgb.shiro.redis.RedisCacheManager">
<property name="redisManager" ref="redisManager" />
</bean>
</beans>
最下面的Session部分的配置,和securityManager中加入sessionManger的引用,主要就是这两部分。至于我的SessionDao,redisCache,RedisCacheManager修改了一些具体的内容就不方便贴了,我就贴一下git上的源码,顺便把里面的英文注释改成中文的吧。
RedisSessionDAO代码如下,主要是集成shiro的抽象sessiondao,重写里面的方法,调用redismanager
package org.creazycake.shiro;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Created by Rephilo on 2016/12/6 0006.
*/
public class RedisSessionDAO extends AbstractSessionDAO {
private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
private RedisManager redisManager;
/**
* shiro-redis的session对象前缀
*/
private String keyPrefix = "shiro_redis_session:";
/**
* 更新session
* @param session
* @throws UnknownSessionException
*/
@Override
public void update(Session session) throws UnknownSessionException {
this.saveSession(session);
}
/**
* 保存Session
* @param session
* @throws UnknownSessionException
*/
private void saveSession(Session session) throws UnknownSessionException{
if(session == null || session.getId() == null){
logger.error("session or session id is null");
return;
}
byte[] key = getByteKey(session.getId());
byte[] value = SerializeUtils.serialize(session);
session.setTimeout(redisManager.getExpire()*1000);
this.redisManager.set(key, value, redisManager.getExpire());
}
/**
* 删除session
* @param session
*/
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
logger.error("session or session id is null");
return;
}
redisManager.del(this.getByteKey(session.getId()));
}
/**
* 获取所有的Session
* @return
*/
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<byte[]> keys = redisManager.keys(this.keyPrefix + "*");
if(keys != null && keys.size()>0){
for(byte[] key:keys){
Session s = (Session)SerializeUtils.deserialize(redisManager.get(key));
sessions.add(s);
}
}
return sessions;
}
/**
* 创建Session
* @param session
* @return sessionid
*/
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
/**
* 读取redis里的session
* @param sessionId
* @return
*/
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
logger.error("session id is null");
return null;
}
Session s = (Session)SerializeUtils.deserialize(redisManager.get(this.getByteKey(sessionId)));
return s;
}
/**
* 获得byte[]型的key
* @param key
* @return
*/
private byte[] getByteKey(Serializable sessionId){
String preKey = this.keyPrefix + sessionId;
return preKey.getBytes();
}
// 后面是getter和setter
public RedisManager getRedisManager() {
return redisManager;
}
/**
* 设置redismanager之后会初始化
* @param redisManager
*/
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
//初始化redisManager
this.redisManager.init();
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
}
RedisManager代码如下,主要是初始化jedis的连接池,其他方法都是调用jedis的方法
package org.creazycake.shiro;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.Set;
/**
* Created by Rephilo on 2016/12/6 0006.
*/
public class RedisManager {
//redis地址
private String host = "127.0.0.1";
//redis端口
private int port = 6379;
//过期时间,0-永不过期,其他-过期秒数
private int expire = 0;
//jedis连接超时时间,表示多少毫秒没有响应即超时
private int timeout = 0;
//redis连接密码
private String password = "";
//Jedis连接池
private static JedisPool jedisPool = null;
//构造方法,有需要的自己改吧
public RedisManager() {
}
/**
* 初始化jedis连接池方法
*/
public void init() {
if (jedisPool == null) {
if (password != null && !"".equals(password)) {
jedisPool = new JedisPool(new JedisPoolConfig(), host, port, timeout, password);
} else if (timeout != 0) {
jedisPool = new JedisPool(new JedisPoolConfig(), host, port, timeout);
} else {
jedisPool = new JedisPool(new JedisPoolConfig(), host, port);
}
}
}
/**
* 从redis取得value
*
* @param key
* @return
*/
public byte[] get(byte[] key) {
byte[] value = null;
Jedis jedis = jedisPool.getResource();
try {
value = jedis.get(key);
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* 设置一个kv到redis
*
* @param key
* @param value
* @return
*/
public byte[] set(byte[] key, byte[] value) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(key, value);
if (this.expire != 0) {
jedis.expire(key, this.expire);
}
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* 设置一个kv到redis,有过期时间
*
* @param key
* @param value
* @param expire
* @return
*/
public byte[] set(byte[] key, byte[] value, int expire) {
Jedis jedis = jedisPool.getResource();
try {
jedis.set(key, value);
if (expire != 0) {
jedis.expire(key, expire);
}
} finally {
jedisPool.returnResource(jedis);
}
return value;
}
/**
* 根据key值删除value
*
* @param key
*/
public void del(byte[] key) {
Jedis jedis = jedisPool.getResource();
try {
jedis.del(key);
} finally {
jedisPool.returnResource(jedis);
}
}
/**
* 清空redis库
*/
public void flushDB() {
Jedis jedis = jedisPool.getResource();
try {
jedis.flushDB();
} finally {
jedisPool.returnResource(jedis);
}
}
/**
* 返回当前选择的数据库中key值的数量.
*/
public Long dbSize() {
Long dbSize = 0L;
Jedis jedis = jedisPool.getResource();
try {
dbSize = jedis.dbSize();
} finally {
jedisPool.returnResource(jedis);
}
return dbSize;
}
/**
* 搜索与key的匹配结果;模糊搜索key值
*
* @param regex
* @return
*/
public Set<byte[]> keys(String pattern) {
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try {
keys = jedis.keys(pattern.getBytes());
} finally {
jedisPool.returnResource(jedis);
}
return keys;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public int getExpire() {
return expire;
}
public void setExpire(int expire) {
this.expire = expire;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
RedisCacheManager代码如下,主要是实现了shiro的CacheManager接口,其实已经属于缓存管理的内容了。这里我并不是很懂,缓存管理这边没有仔细的研究过。
public class RedisCacheManager implements CacheManager {
private static final Logger logger = LoggerFactory
.getLogger(RedisCacheManager.class);
//jvm本地保存一份redisCache缓存实例,key为查询的key值,value为redisCache实例
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>();
//redis的管理类
private RedisManager redisManager;
//session前缀
private String keyPrefix = "shiro_redis_cache:";
/**
* 根据key值取出value
* @param name
* @param <K>
* @param <V>
* @return
* @throws CacheException
*/
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
logger.debug("获取名称为: " + name + " 的RedisCache实例");
//先从jvm缓存查询
Cache c = caches.get(name);
//查询为空,连接redis查询
if (c == null) {
// 初始化redis管理类
redisManager.init();
// 创建一份redisCache
c = new RedisCache<K, V>(redisManager, keyPrefix);
// 把redisCache实例加入jvm缓存
caches.put(name, c);
}
return c;
}
public RedisManager getRedisManager() {
return redisManager;
}
public void setRedisManager(RedisManager redisManager) {
this.redisManager = redisManager;
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
}
RedisCache代码如下,其实和sessiondao连差不多,我觉得sessiondao应该调用cachemanager的类的,不应该直接写入redis
package org.creazycake.shiro;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.util.CollectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Created by Rephilo on 2016/12/6 0006.
*/
public class RedisCache<K, V> implements Cache<K, V> {
private Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* The wrapped Jedis instance.
*/
private RedisManager cache;
/**
* The Redis key prefix for the sessions
*/
private String keyPrefix = "shiro_redis_session:";
/**
* Returns the Redis session keys
* prefix.
* @return The prefix
*/
public String getKeyPrefix() {
return keyPrefix;
}
/**
* Sets the Redis sessions key
* prefix.
* @param keyPrefix The prefix
*/
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
/**
* 通过一个JedisManager实例构造RedisCache
*/
public RedisCache(RedisManager cache){
if (cache == null) {
throw new IllegalArgumentException("Cache argument cannot be null.");
}
this.cache = cache;
}
/**
* 另一个构造函数,可以使用自定义前缀
* @param cache The cache manager instance
* @param prefix The Redis key prefix
*/
public RedisCache(RedisManager cache,
String prefix){
this( cache );
// set the prefix
this.keyPrefix = prefix;
}
/**
* 获得byte[]型的key
* @param key
* @return
*/
private byte[] getByteKey(K key){
if(key instanceof String){
String preKey = this.keyPrefix + key;
return preKey.getBytes();
}else{
return SerializeUtils.serialize(key);
}
}
@Override
public V get(K key) throws CacheException {
logger.debug("根据key从Redis中获取对象 key [" + key + "]");
try {
if (key == null) {
return null;
}else{
byte[] rawValue = cache.get(getByteKey(key));
@SuppressWarnings("unchecked")
V value = (V)SerializeUtils.deserialize(rawValue);
return value;
}
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public V put(K key, V value) throws CacheException {
logger.debug("根据key从存储 key [" + key + "]");
try {
cache.set(getByteKey(key), SerializeUtils.serialize(value));
return value;
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public V remove(K key) throws CacheException {
logger.debug("从redis中删除 key [" + key + "]");
try {
V previous = get(key);
cache.del(getByteKey(key));
return previous;
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public void clear() throws CacheException {
logger.debug("从redis中删除所有元素");
try {
cache.flushDB();
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public int size() {
try {
Long longSize = new Long(cache.dbSize());
return longSize.intValue();
} catch (Throwable t) {
throw new CacheException(t);
}
}
@SuppressWarnings("unchecked")
@Override
public Set<K> keys() {
try {
Set<byte[]> keys = cache.keys(this.keyPrefix + "*");
if (CollectionUtils.isEmpty(keys)) {
return Collections.emptySet();
}else{
Set<K> newKeys = new HashSet<K>();
for(byte[] key:keys){
newKeys.add((K)key);
}
return newKeys;
}
} catch (Throwable t) {
throw new CacheException(t);
}
}
@Override
public Collection<V> values() {
try {
Set<byte[]> keys = cache.keys(this.keyPrefix + "*");
if (!CollectionUtils.isEmpty(keys)) {
List<V> values = new ArrayList<V>(keys.size());
for (byte[] key : keys) {
@SuppressWarnings("unchecked")
V value = get((K)key);
if (value != null) {
values.add(value);
}
}
return Collections.unmodifiableList(values);
} else {
return Collections.emptyList();
}
} catch (Throwable t) {
throw new CacheException(t);
}
}
}
以上就是使用shiro实现session共享的基本原理了,至于有没有必要联系作者更新一下sessionDAO里面的东西。。。git上最新的是3年之前了。大伙凑合着用吧,那天有心情我更一下试试- -