shiro整合redis實現分散式session和單點登入
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.