SpringBoot集成Shiro實現(xiàn)多數(shù)據(jù)源認(rèn)證授權(quán)與分布式會話(三)

前言

前面我們利用springboot結(jié)合shiro在項目中解決了多數(shù)據(jù)源認(rèn)證的問題,接下來我們來看看如何在之前的框架基礎(chǔ)上實現(xiàn)分布式session管理.在一般情況下web服務(wù)器與客戶端采用http協(xié)議進(jìn)行通訊,大家都知道http協(xié)議本身是一種無狀態(tài)的協(xié)議,即每次請求之間都是相互獨立的,服務(wù)器無法記住當(dāng)前請求的用戶是誰,可想而知在絕大部分業(yè)務(wù)場景下,對于用戶來講這種體驗是非常糟糕的,而session便是一種讓web服務(wù)器與客戶端之間進(jìn)行狀態(tài)保持的解決方案.

session與cookie

session即會話,會話是存儲在web服務(wù)器端內(nèi)存中的,類似于map這樣的一種數(shù)據(jù)結(jié)構(gòu),session與cookie之間的關(guān)系簡單點說就是用戶在第一次通過瀏覽器訪問服務(wù)端時,服務(wù)端會為當(dāng)前用戶創(chuàng)建一個session對象并將生成的唯一標(biāo)識sessionid返回給客戶端瀏覽器存儲到cookie中,當(dāng)用戶登錄之后,服務(wù)端通過瀏覽器請求頭中傳遞過來的sessionid在web容器中找到對應(yīng)session并將用戶的個人信息與登錄狀態(tài)和其綁定起來,這樣子以后每次請求,服務(wù)端都能與客戶端建立起有效的狀態(tài)保持了.

什么是session共享

通常在單體應(yīng)用下是無需考慮session的共享問題的,因為這種架構(gòu)一般是集中式部署的,即所有的代碼都部署到一臺web服務(wù)器上,代碼分層也是很經(jīng)典的MVC三層架構(gòu).
單體架構(gòu)圖.png

隨著業(yè)務(wù)的發(fā)展以及應(yīng)用的迭代開發(fā),單體架構(gòu)臃腫的代碼結(jié)構(gòu)不但難以維護(hù)而且無法滿足迅速變化的業(yè)務(wù)需求,所以集中式部署的架構(gòu)必須演進(jìn)為分布式架構(gòu)來突破原有架構(gòu)的瓶頸,以應(yīng)付高并發(fā)的訪問量.在分布式架構(gòu)中把原來的單體架構(gòu)按照功能模塊解耦成若干個微服務(wù)的形式對外提供api接口,并且每個微服務(wù)都獨立的部署到各自的服務(wù)器上面.
分布式架構(gòu)圖.png

此時應(yīng)用雖然不存在單點問題,但是也由于服務(wù)器是多個節(jié)點同時向外提供服務(wù)的,如果此時前端負(fù)載均衡器將客戶端請求分發(fā)到不同的服務(wù)器節(jié)點上面,就會因為其中某些節(jié)點上沒有用戶session而導(dǎo)致請求失效.所以在單體架構(gòu)中依賴于容器自身所提供的session管理方案已經(jīng)無法滿足分布式或集群場景下的需求,于是我們需要有一套機(jī)制來保證當(dāng)服務(wù)器在多節(jié)點的情況下session數(shù)據(jù)可以共享.

session一致性解決方案

接下來我們來看看主要有哪些常見的session共享方案.
**1.session復(fù)制 **
這種方案主要依賴于tomcat等web服務(wù)器,可在多個服務(wù)器之間自動實時復(fù)制session數(shù)據(jù),如利用Terracotta來實現(xiàn)tomcat間session共享,配置對原來的應(yīng)用完全透明,原有程序幾乎不用做任何修改,而且Terracotta本身支持HA或者使用tomcat自帶的cluster也可以,但是這些方案效率較低,用戶量大并發(fā)量大時會大量占用網(wǎng)絡(luò)帶寬而且可能有延遲,整體上來說非常消耗系統(tǒng)資源.
**2.nginx配置ip的hash路由策略 **
利用nginx的基于訪問ip的hash路由策略,其原理就是同一個ip的所有請求都會被nginx進(jìn)行ip_hash進(jìn)行計算,通過結(jié)果定位到指定的后臺服務(wù)器即一個用戶如果ip不變,那么每次請求的都是同一后臺服務(wù)器.但是最外層的代理要保證源ip在請求的過程不會被修改,如果你的架構(gòu)里在最外層不單單是nginx服務(wù),而是類似于請求分發(fā)的服務(wù)那么一個用戶的請求可能被定位到不同的服務(wù)器上或者說一個局域網(wǎng)有許多用戶同時登錄系統(tǒng)的話,那么ip_hash就沒有什么用了.
**3.存儲在cookie中 **
session也可存儲于客戶端cookie中,但數(shù)據(jù)大小有限制,且用戶有可能禁用cookie,存在安全隱患.
**4.Spring Session **
spring提供的一整套支持分布式session管理的方案,默認(rèn)采用外置的redis來存儲數(shù)據(jù),以此來解決會話共享的問題。
**5.實現(xiàn)獨立的會話中心 **
利用如數(shù)據(jù)庫、redis或者memcache等第三方存儲介質(zhì)來保存會話信息,負(fù)責(zé)session數(shù)據(jù)共享,并實現(xiàn)獨立的會話中心來管理session的生命周期,讓其不再與web容器耦合在一起.
在項目中我們采取了自建會話中心的方式來支持session共享.

shiro如何結(jié)合redis實現(xiàn)session共享

首先shiro提供了可擴(kuò)展會話管理的頂層接口AbstractSessionDAO,我們可以自定義自己的RedisSessionDao類繼承自AbstractSessionDAO,在此實現(xiàn)類中分別覆寫以下方法:
doCreate():用戶第一次訪問系統(tǒng)時創(chuàng)建會話信息
doReadSession():讀取會話信息
update():更新用戶會話信息
delete():刪除用戶會話信息
getActiveSessions():獲取所有的在線會話信息
這幾個方法即提供了對會話的基本管理crud以及session超時策略的控制,主要代碼如下:

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = SessionCons.TOKEN_PREFIX
                + UUID.randomUUID().toString();
        assignSessionId(session, sessionId);
        redisTemplate.opsForValue().set(sessionId, session);
        redisTemplate.expire(sessionId, PC_EXPIRE_TIME, TimeUnit.SECONDS);
        if (logger.isDebugEnabled()) {
            logger.debug("create shiro session ,sessionId is :{}",
                    sessionId.toString());
        }
        return sessionId;
    }
    @Override
    protected Session doReadSession(Serializable sessionId) {
        Session session = redisTemplate.opsForValue().get(sessionId);
        if (null != session) {
            String deviceType = (String) session
                    .getAttribute(SessionCons.DEVICE_TYPE);
            if (StringHelpUtils.isNotBlank(deviceType)) {
                if (deviceType.equals(DeviceType.PC.toString())) {
                    // PC會話信息
                    session.setTimeout(PC_EXPIRE_TIME * 1000);
                    if (logger.isDebugEnabled()) {
                        logger.debug("read pc session ,sessionId is :{}",
                                sessionId.toString());
                    }
                } else {
                    // APP會話信息
                    session.setTimeout(APP_EXPIRE_TIME * 1000);
                    if (logger.isDebugEnabled()) {
                        logger.debug("read app session ,sessionId is :{}",
                                sessionId.toString());
                    }
                }
            }
        }
        return session;
    }
    @Override
    public void update(Session session) throws UnknownSessionException {
        if (null != session && null != session.getId()) {
            String deviceType = (String) session
                    .getAttribute(SessionCons.DEVICE_TYPE);
            if (StringHelpUtils.isBlank(deviceType))
                deviceType = DeviceType.PC.toString();
            redisTemplate.opsForValue().set(session.getId(), session);
            if (deviceType.equals(DeviceType.PC.toString())) {
                // PC會話信息
                session.setTimeout(PC_EXPIRE_TIME * 1000);
                redisTemplate.expire(session.getId(), PC_EXPIRE_TIME,
                        TimeUnit.SECONDS);
                if (logger.isDebugEnabled()) {
                    logger.debug("update pc session ,sessionId is :{}", session
                            .getId().toString());
                }
            } else {
                // APP會話信息
                session.setTimeout(APP_EXPIRE_TIME * 1000);
                redisTemplate.expire(session.getId(), APP_EXPIRE_TIME,
                        TimeUnit.SECONDS);
                if (logger.isDebugEnabled()) {
                    logger.debug("update app session ,sessionId is :{}",
                            session.getId().toString());
                }
            }
        }
    }
    @Override
    public void delete(Session session) {
        if (logger.isDebugEnabled()) {
            logger.debug("delete shiro session ,sessionId is :{}", session
                    .getId().toString());
        }
        redisTemplate.opsForValue().getOperations().delete(session.getId());
    }
    @Override
    public Collection<Session> getActiveSessions() {
        Set<Serializable> keys = redisTemplate
                .keys(SessionCons.TOKEN_PREFIX_KEY);
        if (keys.size() == 0) {
            return Collections.emptySet();
        }
        List<Session> sessions = redisTemplate.opsForValue().multiGet(keys);
        return Collections.unmodifiableCollection(sessions);
    }

然后在shiro的配置類中配置自定義的sessionDAO.

    @Bean
    public RedisSessionDao redisSessionDAO() {
        return new RedisSessionDao();
    }

設(shè)置shiro的會話管理器.

    @Bean
    public CustomerWebSessionManager sessionManager() {
        CustomerWebSessionManager sessionManager = new CustomerWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        ......
        return sessionManager;
    }

接著定義一個會話常量類SessionCons,內(nèi)部包含會話key前綴等系統(tǒng)常量.

    String LOGIN_USER_PERMISSIONS = "session_login_user_permissions";
    String LOGIN_USER_SESSION = "session_login_user";
    String TOKEN_PREFIX = "web_session_key-";
    String TOKEN_PREFIX_KEY = "web_session_key-*";
    String DEVICE_TYPE = "device_type";
    ......

最后在用戶登錄之后的時候就可以把相關(guān)的用戶session信息和權(quán)限數(shù)據(jù)存儲到redis中了.

     subject.getSession().setAttribute(SessionCons.LOGIN_USER_SESSION,loginAccinfo);
     subject.getSession().setAttribute(SessionCons.DEVICE_TYPE,DeviceTypePC.toString());
     subject.getSession().setAttribute(SessionCons.LOGIN_USER_PERMISSIONS, permissions);
     ......

到此我們已經(jīng)在之前的框架基礎(chǔ)上實現(xiàn)了分布式session管理了.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容