SpringBoot集成Shiro实现多数据源认证授权与分布式会话(三)

前言

前面我们利用springboot结合shiro在项目中解决了多数据源认证的问题,接下来我们来看看如何在之前的框架基础上实现分布式session管理.在一般情况下web服务器与客户端采用http协议进行通讯,大家都知道http协议本身是一种无状态的协议,即每次请求之间都是相互独立的,服务器无法记住当前请求的用户是谁,可想而知在绝大部分业务场景下,对于用户来讲这种体验是非常糟糕的,而session便是一种让web服务器与客户端之间进行状态保持的解决方案.

session与cookie

session即会话,会话是存储在web服务器端内存中的,类似于map这样的一种数据结构,session与cookie之间的关系简单点说就是用户在第一次通过浏览器访问服务端时,服务端会为当前用户创建一个session对象并将生成的唯一标识sessionid返回给客户端浏览器存储到cookie中,当用户登录之后,服务端通过浏览器请求头中传递过来的sessionid在web容器中找到对应session并将用户的个人信息与登录状态和其绑定起来,这样子以后每次请求,服务端都能与客户端建立起有效的状态保持了.

什么是session共享

通常在单体应用下是无需考虑session的共享问题的,因为这种架构一般是集中式部署的,即所有的代码都部署到一台web服务器上,代码分层也是很经典的MVC三层架构.
单体架构图.png

随着业务的发展以及应用的迭代开发,单体架构臃肿的代码结构不但难以维护而且无法满足迅速变化的业务需求,所以集中式部署的架构必须演进为分布式架构来突破原有架构的瓶颈,以应付高并发的访问量.在分布式架构中把原来的单体架构按照功能模块解耦成若干个微服务的形式对外提供api接口,并且每个微服务都独立的部署到各自的服务器上面.
分布式架构图.png

此时应用虽然不存在单点问题,但是也由于服务器是多个节点同时向外提供服务的,如果此时前端负载均衡器将客户端请求分发到不同的服务器节点上面,就会因为其中某些节点上没有用户session而导致请求失效.所以在单体架构中依赖于容器自身所提供的session管理方案已经无法满足分布式或集群场景下的需求,于是我们需要有一套机制来保证当服务器在多节点的情况下session数据可以共享.

session一致性解决方案

接下来我们来看看主要有哪些常见的session共享方案.
**1.session复制 **
这种方案主要依赖于tomcat等web服务器,可在多个服务器之间自动实时复制session数据,如利用Terracotta来实现tomcat间session共享,配置对原来的应用完全透明,原有程序几乎不用做任何修改,而且Terracotta本身支持HA或者使用tomcat自带的cluster也可以,但是这些方案效率较低,用户量大并发量大时会大量占用网络带宽而且可能有延迟,整体上来说非常消耗系统资源.
**2.nginx配置ip的hash路由策略 **
利用nginx的基于访问ip的hash路由策略,其原理就是同一个ip的所有请求都会被nginx进行ip_hash进行计算,通过结果定位到指定的后台服务器即一个用户如果ip不变,那么每次请求的都是同一后台服务器.但是最外层的代理要保证源ip在请求的过程不会被修改,如果你的架构里在最外层不单单是nginx服务,而是类似于请求分发的服务那么一个用户的请求可能被定位到不同的服务器上或者说一个局域网有许多用户同时登录系统的话,那么ip_hash就没有什么用了.
**3.存储在cookie中 **
session也可存储于客户端cookie中,但数据大小有限制,且用户有可能禁用cookie,存在安全隐患.
**4.Spring Session **
spring提供的一整套支持分布式session管理的方案,默认采用外置的redis来存储数据,以此来解决会话共享的问题。
**5.实现独立的会话中心 **
利用如数据库、redis或者memcache等第三方存储介质来保存会话信息,负责session数据共享,并实现独立的会话中心来管理session的生命周期,让其不再与web容器耦合在一起.
在项目中我们采取了自建会话中心的方式来支持session共享.

shiro如何结合redis实现session共享

首先shiro提供了可扩展会话管理的顶层接口AbstractSessionDAO,我们可以自定义自己的RedisSessionDao类继承自AbstractSessionDAO,在此实现类中分别覆写以下方法:
doCreate():用户第一次访问系统时创建会话信息
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();
    }

设置shiro的会话管理器.

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

接着定义一个会话常量类SessionCons,内部包含会话key前缀等系统常量.

    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";
    ......

最后在用户登录之后的时候就可以把相关的用户session信息和权限数据存储到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);
     ......

到此我们已经在之前的框架基础上实现了分布式session管理了.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容