shiro是一款出色的权限框架,能够实现诸如登录校验、权限校验等功能,默认情况下,shir将session保存到内存中,这在应用分布式部署的情况下会出现session不一致的问题,所以我们要将session保存到第三方,应用始终从第三方获取session,从而保证分布式部署时session始终是一致的,这里我们采用redis保存session。单点登陆的实现逻辑是在用户登陆时,生成token,然后将token以用户登陆账号为key,保存到redis中,再把token放到cookie中,用户在访问的时候,我们就能拿到cookie中的token,和redis中的做比较,如果不一致,则认为用户已经下线或者再别的地方登陆,下面看代码。
一、自定义Session
shiro默认的session是SimpleSession,这里我们自定义session,目前不做什么变化,如果有需要,我们就可以扩展自定义Session实现一些特殊功能。
public class ShiroSession extends SimpleSession implements Serializable {
}
二、自定义SessionFactory
shiro使用SessionFactory创建session,这里我们自定义SessionFactory,让它创建我们自定义的Session.
public class ShiroSessionFactory implements SessionFactory {
@Override
public Session createSession(SessionContext sessionContext) {
ShiroSession session = new ShiroSession();
HttpServletRequest request = (HttpServletRequest)sessionContext.get(DefaultWebSessionContext.class.getName() + ".SERVLET_REQUEST");
session.setHost(getIpAddress(request));
return session;
}
public static String getIpAddress(HttpServletRequest request) {
String localIP = "127.0.0.1";
String ip = request.getHeader("x-forwarded-for");
if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(ip) || (ip.equalsIgnoreCase(localIP)) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
三、ShiroRedisDao
这个类就是shiro用来创建、修改、删除session的地方。在创建、修改、删除的时候,其实都是对redis做操作。
public class ShiroSessionRedisDao extends EnterpriseCacheSessionDAO {
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
RedisUtil.setObject(SHIROSESSION_REDIS_DB,(SHIROSESSION_REDIS_PREFIX+sessionId.toString()).getBytes(),sessionToByte(session),SHIROSESSION_REDIS_EXTIRETIME);
return sessionId;
}
@Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
return this.doReadSession(sessionId);
}
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = null;
byte[] bytes = RedisUtil.getObject(SHIROSESSION_REDIS_DB,(SHIROSESSION_REDIS_PREFIX+sessionId.toString()).getBytes(),SHIROSESSION_REDIS_EXTIRETIME);
if(bytes != null && bytes.length > 0){
session = byteToSession(bytes);
}
return session;
}
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
RedisUtil.updateObject(SHIROSESSION_REDIS_DB,(SHIROSESSION_REDIS_PREFIX+session.getId().toString()).getBytes(),ShiroSessionConvertUtil.sessionToByte(session),SHIROSESSION_REDIS_EXTIRETIME);
//也要更新token
User user = (User)session.getAttribute(Const.SESSION_USER);
if(null != user){
RedisUtil.updateString(SHIROSESSION_REDIS_DB,ShiroSessionRedisConstant.SSOTOKEN_REDIS_PREFIX+user.getUSERNAME(),ShiroSessionRedisConstant.SHIROSESSION_REDIS_EXTIRETIME);
}
}
@Override
protected void doDelete(Session session) {
super.doDelete(session);
RedisUtil.delString(SHIROSESSION_REDIS_DB,SHIROSESSION_REDIS_PREFIX+session.getId().toString());
//也要删除token
User user = (User)session.getAttribute(Const.SESSION_USER);
if(null != user){
RedisUtil.delString(SHIROSESSION_REDIS_DB,ShiroSessionRedisConstant.SSOTOKEN_REDIS_PREFIX+user.getUSERNAME());
}
}
四、工具类
1、Session序列化工具类,使用该类将session转化为byte[],保存到redis中
public class ShiroSessionConvertUtil {
/**
* 把session对象转化为byte数组
* @param session
* @return
*/
public static byte[] sessionToByte(Session session){
ByteArrayOutputStream bo = new ByteArrayOutputStream();
byte[] bytes = null;
try {
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(session);
bytes = bo.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return bytes;
}
/**
* 把byte数组还原为session
* @param bytes
* @return
*/
public static Session byteToSession(byte[] bytes){
ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
ObjectInputStream in;
Session session = null;
try {
in = new ObjectInputStream(bi);
session = (SimpleSession) in.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return session;
}
}
2、SessionListener,这个监听器在发生session创建、变化、销毁等事件时,可以进行捕捉,这个类主要处理session销毁时,清楚redis中的数据
public class ShiroSessionListener implements SessionListener {
private static final Logger LOGGER = LoggerFactory.getLogger(ShiroSessionListener.class);
@Override
public void onStart(Session session) {
// 会话创建时触发
LOGGER.info("ShiroSessionListener session {} 被创建", session.getId());
}
@Override
public void onStop(Session session) {
// 会话被停止时触发
ShiroSessionRedisUtil.deleteSession(session);
LOGGER.info("ShiroSessionListener session {} 被销毁", session.getId());
}
@Override
public void onExpiration(Session session) {
//会话过期时触发
ShiroSessionRedisUtil.deleteSession(session);
LOGGER.info("ShiroSessionListener session {} 过期", session.getId());
}
}
3、操作redis的工具类
public class ShiroSessionRedisUtil {
public static Session getSession(Serializable sessionId){
Session session = null;
byte[] bytes = RedisUtil.getObject(SHIROSESSION_REDIS_DB,(SHIROSESSION_REDIS_PREFIX+sessionId.toString()).getBytes(),SHIROSESSION_REDIS_EXTIRETIME);
if(bytes != null && bytes.length > 0){
session = byteToSession(bytes);
}
return session;
}
public static void updateSession(Session session){
RedisUtil.updateObject(SHIROSESSION_REDIS_DB,(SHIROSESSION_REDIS_PREFIX+session.getId().toString()).getBytes(),ShiroSessionConvertUtil.sessionToByte(session),SHIROSESSION_REDIS_EXTIRETIME);
//也要更新token
User user = (User)session.getAttribute(Const.SESSION_USER);
if(null != user){
RedisUtil.updateString(SHIROSESSION_REDIS_DB,ShiroSessionRedisConstant.SSOTOKEN_REDIS_PREFIX+user.getUSERNAME(),ShiroSessionRedisConstant.SHIROSESSION_REDIS_EXTIRETIME);
}
}
public static void deleteSession(Session session){
RedisUtil.delString(SHIROSESSION_REDIS_DB,SHIROSESSION_REDIS_PREFIX+session.getId().toString());
//也要删除token
User user = (User)session.getAttribute(Const.SESSION_USER);
if(null != user){
RedisUtil.delString(SHIROSESSION_REDIS_DB,ShiroSessionRedisConstant.SSOTOKEN_REDIS_PREFIX+user.getUSERNAME());
}
}
}
public final class RedisUtil {
//Redis服务器IP
private static String ADDR = PropertyUtils.redisUrl;
//Redis的端口号
private static int PORT = PropertyUtils.redisPort;
//访问密码
private static String AUTH = PropertyUtils.redisPasswd;
//可用连接实例的最大数目,默认值为8;
//如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
// private static int MAX_ACTIVE = 50;
//控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
private static int MAX_IDLE = 200;
//等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
private static int MAX_WAIT = 10000;
private static int TIMEOUT = 10000;
//在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
private static boolean TEST_ON_BORROW = true;
private static JedisPool jedisPool = null;
/**
* 初始化Redis连接池
*/
static {
try {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(MAX_IDLE);
config.setMaxWaitMillis(MAX_WAIT);
config.setTestOnBorrow(TEST_ON_BORROW);
if(StringUtils.isEmpty(AUTH))
AUTH=null;
jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取Jedis实例
* @return
*/
public synchronized static Jedis getJedis() {
try {
if (jedisPool != null) {
Jedis resource = jedisPool.getResource();
return resource;
} else {
return null;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 释放jedis资源
* @param jedis
*/
public static void returnResource(final Jedis jedis) {
if (jedis != null) {
jedisPool.returnResource(jedis);
}
}
/**
* KEY对应value加1,并且设置过期时间
* @param db
* @param key
* @param ttl(s)
* @return
*/
public static long incrWithExpire(int db, String key, int ttl){
Jedis resource=null;
long res = 0;
try {
if (jedisPool != null) {
resource = jedisPool.getResource();
resource.select(db);
res = resource.incr(key);
if(res == 1){
resource.expire(key, ttl);
}
jedisPool.returnResource(resource);
}
return res;
} catch (Exception e) {
e.printStackTrace();
jedisPool.returnBrokenResource(resource);
return 0;
}
}
/**
* 删除set中多个fields
* @param db
* @param key
* @return
*/
public static long hdel(int db, String key, String[] fields){
Jedis resource=null;
long res = 0;
try {
if (jedisPool != null) {
resource = jedisPool.getResource();
resource.select(db);
res = resource.hdel(key, fields);
jedisPool.returnResource(resource);
}
return res;
} catch (Exception e) {
e.printStackTrace();
jedisPool.returnBrokenResource(resource);
return 0;
}
}
/**
* 获取Redis里面的set里的值
* @param db
* @param key
* @param feild
* @return
*/
public static String hget(int db,String key,String feild){
Jedis jedis = null;
String value = null;
try {
if (jedisPool!=null) {
jedis = jedisPool.getResource();
jedis.select(db);
value=jedis.hget(key,feild);
jedisPool.returnResource(jedis);
}
}catch (Exception e){
e.printStackTrace();;
jedisPool.returnBrokenResource(jedis);
}
return value;
}
/**
* 写入Redis里面的set里的值
* @param db
* @param key
* @param feild
* @return
*/
public static void hset(int db,String key,String feild,String value){
Jedis jedis = null;
try{
if(jedisPool!=null){
jedis = jedisPool.getResource();
jedis.select(db);
jedis.hset(key,feild,value);
jedisPool.returnResource(jedis);
}
}catch (Exception e){
e.printStackTrace();
jedisPool.returnBrokenResource(jedis);
}
}
/**
* 迭代set里的元素
* @param db
* @param key
* @return
*/
public static ScanResult<Map.Entry<String,String>> hscan(int db, String key, String cursor, ScanParams scanParams){
Jedis resource=null;
ScanResult<Map.Entry<String,String>> scanResult = null;
try {
if (jedisPool != null) {
resource = jedisPool.getResource();
resource.select(db);
scanResult = resource.hscan(key, cursor, scanParams);
jedisPool.returnResource(resource);
}
return scanResult;
} catch (Exception e) {
e.printStackTrace();
jedisPool.returnBrokenResource(resource);
return scanResult;
}
}
public static void main(String[] args) {
System.out.println(incrWithExpire(0, "test", 10));
ScanParams scanParams = new ScanParams();
scanParams.count(10);
Map<String, String> map = new HashMap<String, String>();
System.out.println(JSON.toJSONString(hscan(2, "cuserMobileCabSet", "0", scanParams)));
}
/**
* 获取byte类型数据
* @param key
* @return
*/
public static byte[] getObject(int db,byte[] key,int expireTime){
Jedis jedis = getJedis();
byte[] bytes = null;
if(jedis != null){
jedis.select(db);
try{
bytes = jedis.get(key);
if(null != bytes){
jedis.expire(key,expireTime);
}
}catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
return bytes;
}
/**
* 保存byte类型数据
* @param key
* @param value
*/
public static void setObject(int db,byte[] key, byte[] value,int expireTime){
Jedis jedis = getJedis();
if(jedis != null){
jedis.select(db);
try{
jedis.set(key, value);
// redis中session过期时间
jedis.expire(key, expireTime);
} catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
}
/**
* 更新byte类型的数据,主要更新过期时间
* @param key
*/
public static void updateObject(int db,byte[] key,byte[] value,int expireTime){
Jedis jedis = getJedis();
if(jedis != null){
try{
// redis中session过期时间
jedis.select(db);
jedis.set(key, value);
jedis.expire(key, expireTime);
}catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
}
/**
* 删除字符串数据
* @param key
*/
public static void delString(int db ,String key){
Jedis jedis = getJedis();
if(jedis != null){
try{
jedis.select(db);
jedis.del(key);
}catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
}
/**
* 存放字符串
* @param db
* @param key
* @param value
* @param expireTime
*/
public static void setString(int db,String key,String value,int expireTime){
Jedis jedis = getJedis();
if(jedis != null){
jedis.select(db);
try{
jedis.set(key, value);
// redis中session过期时间
jedis.expire(key, expireTime);
} catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
}
/**
* 获取字符串
* @param db
* @param key
* @param expireTime
* @return
*/
public static String getString(int db,String key,int expireTime){
Jedis jedis = getJedis();
String result = null;
if(jedis != null){
jedis.select(db);
try{
result = jedis.get(key);
if(org.apache.commons.lang.StringUtils.isNotBlank(result)){
jedis.expire(key,expireTime);
}
}catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
return result;
}
/**
* 更新string类型的数据,主要更新过期时间
* @param key
*/
public static void updateString(int db,String key,String value,int expireTime){
Jedis jedis = getJedis();
if(jedis != null){
try{
// redis中session过期时间
jedis.select(db);
jedis.set(key, value);
jedis.expire(key, expireTime);
}catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
}
/**
* 更新string类型的数据,主要更新过期时间
* @param key
*/
public static void updateString(int db,String key,int expireTime){
Jedis jedis = getJedis();
if(jedis != null){
try{
// redis中token过期时间
jedis.select(db);
jedis.expire(key, expireTime);
}catch(Exception e){
e.printStackTrace();
} finally{
returnResource(jedis);
}
}
}
}
4、一些常量设置
public class ShiroSessionRedisConstant {
/**
* shirosession存储到redis中key的前缀
*/
public static final String SHIROSESSION_REDIS_PREFIX = "SHIROSESSION_";
/**
* shirosession存储到redis哪个库中
*/
public static final int SHIROSESSION_REDIS_DB = 0;
/**
* shirosession存储到redis中的过期时间
*/
public static final int SHIROSESSION_REDIS_EXTIRETIME = 30*60;
/**
* token存到cookie中的key
*/
public static final String SSOTOKEN_COOKIE_KEY = "SSOTOKENID";
/**
* token存到redis中的key前缀
*/
public static final String SSOTOKEN_REDIS_PREFIX = "SSOTOKEN_";
}
五、用户登陆时,将session保存到redis中
//shiro管理的session
Subject currentUser = SecurityUtils.getSubject();
Serializable sessionId = currentUser.getSession().getId();
Session session = ShiroSessionRedisUtil.getSession(sessionId);
///一些用户查找逻辑,将用户、权限等信息放到session中,再更新redis
session.setAttribute(Const.SESSION_USER, user);
session.removeAttribute(Const.SESSION_SECURITY_CODE);
ShiroSessionRedisUtil.updateSession(session);
//其他校验
if("success".equals(errInfo)){
//校验成功,生成一条token存到redis中,key为SSOTOKEN_userId,并以SSOTOKENID为key,放到cookie中
String token = UUID.randomUUID().toString().trim().replaceAll("-", "");;
RedisUtil.setString(ShiroSessionRedisConstant.SHIROSESSION_REDIS_DB,ShiroSessionRedisConstant.SSOTOKEN_REDIS_PREFIX+KEYDATA[0],token,ShiroSessionRedisConstant.SHIROSESSION_REDIS_EXTIRETIME);
Cookie tokenCookie = new Cookie(ShiroSessionRedisConstant.SSOTOKEN_COOKIE_KEY,token);
tokenCookie.setMaxAge(30*60);
tokenCookie.setPath("/");
response.addCookie(tokenCookie);
}
六、拦截器校验
Subject currentUser = SecurityUtils.getSubject();
Serializable sessionId = currentUser.getSession().getId();
Session session = ShiroSessionRedisUtil.getSession(sessionId);
if (null == session) {
response.sendRedirect(request.getContextPath() + Const.LOGIN);
}
User user = (User) session.getAttribute(Const.SESSION_USER);
if (user != null) {
/*校验token,单点登录*/
Cookie[] cookies = request.getCookies();
boolean hasTokenCookie = false;
for (Cookie cookie : cookies) {
if (ShiroSessionRedisConstant.SSOTOKEN_COOKIE_KEY.equals(cookie.getName())) {
hasTokenCookie = true;
String tokenRedis = RedisUtil.getString(ShiroSessionRedisConstant.SHIROSESSION_REDIS_DB, ShiroSessionRedisConstant.SSOTOKEN_REDIS_PREFIX + user.getUSERNAME(), ShiroSessionRedisConstant.SHIROSESSION_REDIS_EXTIRETIME);
if (StringUtils.isBlank(tokenRedis) || !tokenRedis.equalsIgnoreCase(cookie.getValue())) {
response.sendRedirect(request.getContextPath() + Const.LOGIN);
}
}
}
if (!hasTokenCookie) {
response.sendRedirect(request.getContextPath() + Const.LOGIN);
}
path = path.substring(1, path.length());
boolean b = Jurisdiction.hasJurisdiction(path);
if (!b) {
response.sendRedirect(request.getContextPath() + Const.LOGIN);
}
return b;
} else {
//登陆过滤
response.sendRedirect(request.getContextPath() + Const.LOGIN);
return false;
//return true;
}
七、xml配置
<!-- ================ Shiro start ================ -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="ShiroRealm" />
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 項目自定义的Realm -->
<bean id="ShiroRealm" class="com.rrs.rrsck.interceptor.shiro.ShiroRealm" ></bean>
<bean id="tokenFilter" class="com.rrs.rrsck.filter.AccessTokenShiroFilter"/>
<!-- Shiro Filter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="/" />
<property name="successUrl" value="/main/index" />
<property name="unauthorizedUrl" value="/login_toLogin" />
<property name="filters">
<map>
<entry key="tokenFilter" value-ref="tokenFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<value>
/static/** = anon
/static/login/** = anon
/static/js/myjs/** = authc
/static/js/** = anon
/uploadFiles/uploadImgs/** = anon
/code.do = anon
/login_login = anon
/XWZTMBTX/** = anon
/guiziSunYi/** = anon
/app**/** = anon
/weixin/** = anon
/druid/** = anon
/guiziFlow/showGuiziFlow* = tokenFilter,authc
/contactPoint/showContactPoint* = tokenFilter,authc
/contactPointL2/showContactPointL2* = tokenFilter,authc
/** = authc
</value>
</property>
</bean>
<!--shiro redis start-->
<bean id="shiroSessionDao" class="com.rrs.rrsck.shiroredis.ShiroSessionRedisDao"></bean>
<bean id="shiroSessionFactory" class="com.rrs.rrsck.shiroredis.ShiroSessionFactory"></bean>
<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="false"/>
<!-- 是否开启会话验证器任务 默认true -->
<property name="sessionValidationSchedulerEnabled" value="false"/>
<!-- 会话验证器调度时间 -->
<property name="sessionValidationInterval" value="1800000"/>
<property name="sessionFactory" ref="shiroSessionFactory"/>
<property name="sessionDAO" ref="shiroSessionDao"/>
<!-- 默认JSESSIONID,同tomcat/jetty在cookie中缓存标识相同,修改用于防止访问404页面时,容器生成的标识把shiro的覆盖掉 -->
<property name="sessionIdCookie">
<bean class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="SHRIOSESSIONID"/>
</bean>
</property>
<property name="sessionListeners">
<list>
<bean class="com.rrs.rrsck.shiroredis.ShiroSessionListener"/>
</list>
</property>
</bean>
<!--shiro redis end-->
<!-- ================ Shiro end ================ -->
注意点:
只要session发生了改变,如session.setAttribute(),就要更新redis中的session.
更新redis中session的时间时,也要同步更新redis中的token的时间.
删除redis中的session时,也要删除redis中的token.