前两天,在项目中遇到需要对session管理的 一个需求,查询了各种资料,也遇到了各种问题,不过,最后还是实现了需求,在此,也总结一下实现的过程,方便以后查阅。文中借鉴了很多其他文章内容,如有不当敬请谅解,一切以学习为主。
保证同一用户只能在同一客户端登录
基于需求分析,考虑使用redis缓存session的方案:
1.使用redis可方便的设置缓存的有限期,这样可控制用户的在线时长问题。用户登录,前端操作均会更新缓存数据。
2.redis性能毋庸置疑,同时,redis的java api也能方便操作redis中的数据
项目依赖
<properties>
<springside.version>4.2.3-GAspringside.version>
<spring.version>4.0.5.RELEASEspring.version>
<shiro.version>1.2.3shiro.version>
properties>
<dependency>
<groupId>org.springsidegroupId>
<artifactId>springside-coreartifactId>
<version>${springside.version}version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>${shiro.version}version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-ehcacheartifactId>
<version>${shiro.version}version>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
<version>1.1.1.RELEASEversion>
<type>pomtype>
dependency
<dependency>
<groupId>org.crazycakegroupId>
<artifactId>shiro-redisartifactId>
<version>2.4.6version>
dependency>
web.xml配置filter,加载shiro和其他配置文件
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>
classpath*:/applicationContext.xml,
classpath*:/applicationContext-shiro.xml,
classpath*:/applicationContext-session.xml
param-value>
context-param>
<filter>
<filter-name>shiroFilterfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
<init-param>
<param-name>targetFilterLifecycleparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>shiroFilterfilter-name>
<url-pattern>/*url-pattern>
<dispatcher>REQUESTdispatcher>
<dispatcher>FORWARDdispatcher>
<dispatcher>INCLUDEdispatcher>
<dispatcher>ERRORdispatcher>
filter-mapping>
shiro配置文件applicationContext-shiro.xml
<beans xmlns="http://www.springframework.org/schema/beans" 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"
default-lazy-init="true">
<description>Shiro安全配置description>
<bean id="shiroRealm" class="com.sinosoft.bi.base.security.ShiroRealm">bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroRealm" />
<property name="sessionManager" ref="sessionManager">property>
<property name="cacheManager" ref="shiroEhcacheManager" />
bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/login" />
<property name="successUrl" value="/" />
<property name="unauthorizedUrl" value="/unauthorized" />
<property name="filterChainDefinitions">
<value>
/login = authc
/logout = logout
/static/** = anon
/admin/** = roles[admin]
/** = user
value>
property>
bean>
<bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache/ehcache-shiro.xml" />
bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="sessionDao">property>
<property name="globalSessionTimeout" value="60000" />
<property name="deleteInvalidSessions" value="true">property>
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionListeners" ref="myShiroSessionListener">property>
bean>
<bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator">bean>
<bean id="myShiroSessionListener" class="com.sinosoft.bi.base.security.MyShiroSessionListener">bean>
<bean id="sessionDao" class="com.sinosoft.bi.base.security.SessionDao">
<property name="redisUtil" ref="redisUtil">property>
<property name="sessionIdGenerator" ref="sessionIdGenerator">property>
bean>
beans>
applicationContext-session.xml
<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:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
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/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"
default-lazy-init="true">
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig" >
<property name="maxIdle" value="${redis.maxIdle}" />
<property name="maxTotal" value="${redis.maxTotal}" />
<property name="maxWaitMillis" value="${redis.maxWaitMillis}" />
<property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" />
<property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" />
<property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" />
<property name="testOnBorrow" value="${redis.testOnBorrow}" />
<property name="testWhileIdle" value="${redis.testWhileIdle}" />
bean >
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">
<property name="poolConfig" ref="jedisPoolConfig">property>
<property name="hostName" value="127.0.0.1">property>
<property name="port" value="6379">property>
<property name="timeout" value="${redis.timeout}">property>
bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" >
<property name="connectionFactory" ref="jedisConnectionFactory" />
<property name="keySerializer" >
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
property>
<property name="valueSerializer" >
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" />
property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
property>
<property name="enableTransactionSupport" value="true">property>
bean >
beans>
redis配置文件redis.properties
redis.hostName=127.0.0.1
redis.port=6379
#redis.password=123456
redis.timeout=10000
redis.maxIdle=300
redis.maxTotal=1000
redis.maxWaitMillis=1000
redis.minEvictableIdleTimeMillis=300000
redis.numTestsPerEvictionRun=1024
redis.timeBetweenEvictionRunsMillis=30000
redis.testOnBorrow=true
redis.testWhileIdle=true
ehcache-shiro.xml
<ehcache updateCheck="false" name="shiroCache">
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
ehcache>
User实体
public class User implements Serializable{
private static final long serialVersionUID = 1L;
private String userID;
private String username;
private String password;
...getter and setter
}
序列化和反序列化工具
public class SerializeUtils {
/**
* 序列化
*
* @param object
* @return
* @throws Exception
*/
public static byte[] serialize(Object object) throws Exception {
if(object == null) return null;
ObjectOutputStream oos = null;
ByteArrayOutputStream baos = null;
try {
// 序列化
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (Exception e) {
throw e;
}
}
/**
* 反序列化
*
* @param bytes
* @return
* @throws Exception
*/
public static Object unSerialize(byte[] bytes) throws Exception {
if(bytes == null) return null;
ByteArrayInputStream bais = null;
try {
// 反序列化
bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
// 把session对象转化为byte保存到redis中
public static byte[] sessionToByte(Session session){
ByteArrayOutputStream bo = null;
byte[] bytes = null;
ObjectOutput oo = null;
try {
bo = new ByteArrayOutputStream();
oo = new ObjectOutputStream(bo);
oo.writeObject(session);
bytes = bo.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if(null != oo){
oo.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if(null != bo){
bo.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bytes;
}
// 把byte还原为session
public static Session byteToSession(byte[] bytes){
ObjectInputStream in = null;
Session session = null;
try {
in = new ObjectInputStream(new BufferedInputStream(new ByteArrayInputStream(bytes)));
session = (Session) in.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return session;
}
}
redis工具类
/**
* @author myj
* 基于spring和redis的redisTemplate工具类
* 针对所有的hash 都是以h开头的方法
* 针对所有的Set 都是以s开头的方法 不含通用方法
* 针对所有的List 都是以l开头的方法
*/
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key
* 键
* @param time
* 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key
* 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key
* 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public List hmget(){
List list = new ArrayList();
List
MyShiroSessionListener
public class MyShiroSessionListener implements SessionListener {
@Autowired RedisUtil redisUtil;
@Override
public void onStart(Session session) {
}
@Override
public void onStop(Session session) {
redisUtil.remove(session.getId().toString());
}
@Override
public void onExpiration(Session session) {
redisUtil.remove(session.getId().toString());
}
}
SessionDao
@Component
public class SessionDao extends EnterpriseCacheSessionDAO {
private RedisUtil redisUtil;
public RedisUtil getRedisUtil(){
return redisUtil;
}
public void setRedisUtil(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
// 创建session,保存到数据库
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
redisUtil.set(session.getId().toString(), SerializeUtils.sessionToByte(session),1*60*30L);
return sessionId;
}
// 获取session
@Override
protected Session doReadSession(Serializable sessionId) {
// 先从缓存中获取session,如果没有再去数据库中获取
Session session = super.doReadSession(sessionId);
if(session == null){
Object object = null;
byte[] bytes = null;
if(redisUtil.exists(sessionId.toString())){
object = redisUtil.get(sessionId.toString());
}
if(null != object){
bytes = (byte[])object;
}
if(bytes != null && bytes.length > 0){
session = (Session) SerializeUtils.byteToSession(bytes);
}
}
return session;
}
// 更新session的最后一次访问时间
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
redisUtil.set(session.getId().toString(), SerializeUtils.sessionToByte(session),1*60*30L);
}
@Override
public Collection getActiveSessions() {
List list = redisUtil.hmget();
return list;
}
// 删除session
@Override
public void doDelete(Session session) {
super.doDelete(session);
redisUtil.remove(session.getId().toString());
}
}
Usercontroller部分代码
/**
* 下线某在线用户
* @return
*/
@RequestMapping(value = "/offline")
@ResponseBody
public String offline(){
String sessionid = getParams().getString("id");
byte[] bytes = null;
Session session = null;
try{
if(redisUtil.exists(sessionid.toString())){
bytes = (byte[]) redisUtil.get(sessionid.toString());
}
if(bytes != null && bytes.length > 0){
session = SerializeUtils.byteToSession(bytes);
if(session != null){
sessionDao.delete(session);
}
}
return "success";
}catch(Exception e){
e.printStackTrace();
return "error";
}
}
/**
* 获取在线人数
* @param model
* @return
*/
@RequestMapping(value = "/getUserOnline")
public String getUserOnline(Model model){
Map onlineMap = null;
Session session = null;
User user = null;
byte[] bytes = null;
try {
List list = redisUtil.keys();
if(list.size() >0){
onlineMap = new LinkedHashMap();
for (String key : list) {
if(redisUtil.exists(key.toString())){
bytes = (byte[]) redisUtil.get(key.toString());
}
if(bytes != null && bytes.length > 0){
session = SerializeUtils.byteToSession(bytes);
if(session != null){
user = (User) session.getAttribute("currentUser");
if(user != null){
onlineMap.put(key, user);
}
}
}
}
}
model.addAttribute("onlineMap", onlineMap);
} catch (Exception e) {
e.printStackTrace();
}
return "base/user/online";
}
spring容器启动监听,清楚redis中的缓存
/**
* spring容器监听类
* 容器重启时,清空redis缓存
* @author mayi
*
*/
@Component
public class MyContextRefreshedEvent implements ApplicationListener<ContextRefreshedEvent>{
@Override
public void onApplicationEvent(ContextRefreshedEvent arg0) {
@SuppressWarnings("resource")
JedisPool jedisPool = new JedisPool("127.0.0.1",6379);
Jedis jedis = jedisPool.getResource();
jedis.flushAll();
jedis = null;
jedisPool = null;
}
}