03-shiro权限系统-单点登陆

上篇文章,我们完成了 Shiro 权限系统,足以应对中小企业的内部应用。但随着业务量的扩大,我们会把单服务器环境,变成 服务器集群,即系统部署在 2 台、3 台,甚至更多的服务器上。这时候,我们要改造系统,让它更适应集群环境。其中之一,就是单点登陆,即用户登陆后,无论负载均衡到哪一台服务器,都能保持登陆状态。

接下来,我们用之前的权限项目,实现服务器集群下的单点登陆

架构

服务器集群下,我们的服务器架构,也变得更加复杂,最基本的架构如下:

03-shiro权限系统-单点登陆_第1张图片
01-服务器集群及分布式缓存.png

其中,服务端有下面几个模块:

  1. 负载均衡:监测应用服务器的状态,把“用户请求”分发到相对空闲的服务器;
  2. 应用服务器:运行我们开发的程序;
  3. 缓存服务器:储存用户登陆状态;
  4. 数据库服务器:储存供应用程序使用的“持久化数据”;

我们可以理解成:Tomcat 集群下,每个服务都是分开的,所以必须在改造代码,令每个服务之间的数据都能够共享。其中的一个功能,就是单点登陆。在这里,我们要做 2 件事:

  1. 增加缓存服务器,并开发基础的操作类;
  2. 改造 Session 管理的方式,使Session的增删改查,写入到缓存服务器;

首先,增加缓存服务器,可以参考《redis 缓存数据库整合》。

然后,接下来开始改造Session 管理的方式,在 Shrio 中,我们通过覆盖默认的 SessionManager(Session 管理器)来实现。

覆盖默认的 SessionManager(Session 管理器)

在 Shiro 中,Session 管理器 负责用户登录后,session 的增删改查操作。因为 Shiro 默认的Session 管理器,使用的是 Tomcat 缓存,只能支持单服务器的环境。如果在高并发的情况下,用户被分配到另外一台服务器,那么就得重新登陆。所以,要实现单点登陆,我们要重写Session 管理器,开发流程如下:

  1. 新建 RedisSessionDao:redis 中, session 的“增删改查”操作;
  2. 创建 RedisSessionDao 配置:用于覆盖默认的 session 操作类;
  3. 创建 Cookie 配置:在浏览器中,写入 sessionId,用来记住登陆状态;
  4. 创建 SessionManager 配置:利用上述配置,重新创建 SessionManager;
  5. 覆盖默认 SessionManager:覆盖 Shiro 的默认实现;

新建 RedisSessionDao(Session 操作类)

SessionDao 是用来操作 Session 的类,Shiro 提供了一个默认的实现。如果只是单服务器的环境,可以直接用默认配置,我们看看源码DefaultSessionManager

03-shiro权限系统-单点登陆_第2张图片
02-DefaultSessionManager源码.PNG

在源码截图中,我们可以看到DefaultSessionManager 的构造方法中,已经定义了默认的Session 操作类,但这个操作类用的是 Tomcat 的内存,无法满足我们的业务需求。所以,我们要新建一个自己的Session 操作类,用 Redis 缓存,来实现 Session 信息的独立储存。

首先,我们新建一个类:RedisSessionDao,并让它继承 Shiro 的抽象类AbstractSessionDAO,代码如下:

import com.mmall.demo2.common.Const;
import com.mmall.demo2.common.serialize.ObjectMapper;
import com.mmall.demo2.common.response.Servlets;
import com.mmall.demo2.common.redis.RedisPoolUtil;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Collection;

/**
 * @Author: jiaru
 * @Description: session 保存到 redis 的增删改查操作
 * @redis写入方式:   1. 写入:对象转换成byte —> 存入redis;
 *                  2. 取出: 获取byte数据 —> 序列化为java对象;
 * @Date: Created on 2018/10/25
 * @Modify:
 */
public class RedisSessionDao extends AbstractSessionDAO {

    /**
     * 创建session 信息
     * @param session 前端传来的 session 信息
     * @return
     */
    @Override
    protected Serializable doCreate(Session session) {
        // 创建唯一编号
        Serializable sessionId = this.generateSessionId(session);
        // 创建session
        this.assignSessionId(session, sessionId);
        // session 保存到redis缓存中
        this.update(session);
        // 返回session编号
        return sessionId;
    }

    /**
     * 读取 session 信息
     * @param sessionId 前端传来的 session 编号
     * @return
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        /**** 如果 sessionId 为空,则直接返回 ****/
        if (sessionId == null) {
            return null;
        }
        /**** 从 request 中获取 session 信息 ****/
        Session session = null;
        // 获取 request 请求
        HttpServletRequest request = Servlets.getRequest();
        // 获取 session
        if (request != null) {
            session = (Session) request.getAttribute((String) sessionId);
        }
        // 如果 session 非空,直接返回 session
        if (session != null){
            return session;
        }

        /****  如果 sesssion 为空,则从redis中读取 ****/
        // 获取的键名称
        byte[] sessionKey = ObjectMapper.getBytes((String) sessionId);
        // 从 redis 中取出的 byte[] 数据
        byte[] sessionByte = RedisPoolUtil.get(sessionKey);
        // redis 中的 byte[] 转换 Java 对象
        session = (Session) ObjectMapper.toObject(sessionByte);
        return session;
    }

    /**
     * 更新redis中的 session 信息
     * @param session 前端传来的 session 信息
     * @throws UnknownSessionException
     */
    @Override
    public void update(Session session) throws UnknownSessionException {
        if (session == null) {
            return;
        }
        // 设置 session 过期时间
        session.setTimeout(30 * 60 * 1000);
        // session 信息
        byte[] sessionId = ObjectMapper.getBytes((String) session.getId());
        byte[] sessionVal = ObjectMapper.toBytes(session);
        // 保存 session 到 redis 中
        RedisPoolUtil.setEx(sessionId, sessionVal, Const.RedisCacheExTime.REDIS_SESSION_EXTIME);
    }


    /**
     * 删除 session
     * @param session 前端传来的 session 信息
     */
    @Override
    public void delete(Session session) {
        if (session == null) {
            return;
        }
        byte[] sessionId = ObjectMapper.getBytes((String)session.getId());
        RedisPoolUtil.del(sessionId);
    }

    /**
     * 获取在线用户
     * @return
     */
    @Override
    public Collection getActiveSessions() {
        return null;
    }
}

在继承了这个抽象类后,我们按照业务需求,重写抽象方法,就能完成了 80% 的开发工作。接下来的所有工作,相对与开发,更加需要我们去阅读 Shiro 的架构图和运行流程。我们要用 Shiro 的配置类,来定义我们自己的 SessionManager。为此,我们要在上两篇文章的模版项目中,找到ShiroConfiguration这个类。

创建 RedisSessionDao 配置

我们要在ShiroConfiguration 这个类中,来增加sessionDao方法,代码如下:

    /**
     * 设置 session 操作器
     * @return
     */
    @Bean("sessionDAO")
    public SessionDAO sessionDAO() {
        return new RedisSessionDao();
    }

这里,我们直接 new 刚才创建的RedisSessionDao ,然后直接返回,这样就完成创建 RedisSessionDao 配置了。

创建 Cookie 配置

我们继续增加sessionIdCookie方法,代码如下:

    /**
     * 设置用户 cookie 中 sessionId 的信息
     * @return
     */
    @Bean("sessionCookie")
    public SimpleCookie sessionIdCookie() {
        // 创建 Cookie 类,并设置名称
        SimpleCookie cookie = new SimpleCookie("session.id");

        // 不允许通过脚本访问 cookie。备注:无法防御所有脚本攻击,但可提高一定的安全性
        cookie.setHttpOnly(true);
        // 设置域名
        // cookie.setDomain("/");
        // 设置作用域
        cookie.setPath("/");
        return cookie;
    }

那么创建 Cookie 配置,也已经完成了。接下来,在配置中,是最重要的一步。

创建 SessionManager 配置

这是配置工作中,最重要的一步。这里要用到前面的配置类sessionDAOsessionIdCookie,我们创建sessionManager方法,代码如下:

    /**
     * 设置 session 管理器
     * @param sessionDAO
     * @return
     */
    @Bean("sessionManager")
    public WebSessionManager sessionManager(@Qualifier("sessionDAO") SessionDAO sessionDAO,
                                            @Qualifier("sessionCookie") SimpleCookie cookie) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        // 自动删除无效 session
        sessionManager.setDeleteInvalidSessions(true);

        // 设置 session 超时时间,30 分钟超时
        sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);

        // 设置 sessionDao
        sessionManager.setSessionDAO(sessionDAO);

        // 设置 cookie 信息
        sessionManager.setSessionIdCookie(cookie);

        return sessionManager;
    }

除了上面说到的两个配置类,我们还要配置其它参数,建议大家仔细阅读上面代码的注释。

覆盖默认 SessionManager(会话管理器)

在完成最后的工作前,我们先来回顾一下 Shiro 的配置顺序,也就是下面这幅图:

03-shiro权限系统-单点登陆_第3张图片
05-shiro配置开发顺序.png

可以看到,SessionManager 是安全管理器的一部分。所以,要找到上篇文章中,安全管理器的配置方法,并加上我们自定义的SessionManager,代码如下:

    /**
     * 配置安全管理器
     * @param authRealm 已在 Spring 容器中注册的“用户域”
     * @param sessionManager 自定义的“会话管理器”
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm,
                                           @Qualifier("sessionManager") WebSessionManager sessionManager) {
        // 新建web管理器
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();

        // 设置“用户域”
        manager.setRealm(authRealm);

        // 设置 session 管理器:用于单点登陆
        manager.setSessionManager(sessionManager);

        return manager;
    }

到此为止,基于 Shiro 的单点登陆已经完成了。

写在最后

至此,我们的 Shiro 权限系统的讲解,已经完结了。希望大家能够好好理解,然后早日把上面的技术用到工作上。这里附上源码地址:

shiro单点登陆版地址

你可能感兴趣的:(03-shiro权限系统-单点登陆)