1. 程式人生 > >shiro整合redis實現分散式session和單點登入

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.