Springboot2集成Shiro框架(八)使用redis管理session

1、为什么要使用redis管理session

很多公司会使用分布式来部署项目,使用反向代理功能来分发请求,但是如果依旧采用session的方式来对登录信息等进行管理,就会限制反向代理服务器的配置(根据ip,hash服务器地址),为了解决这个问题,使用独立的session服务器可以完美解决这个问题,如下图所示 在这里插入图片描述
每个服务器都会从相同的redis里读取用户登录信息,如果用户登录认证是在服务器1上面完成的,服务器1会把相应的会话数据保存到redis中,当用户访问其他链接时,请求被分配到了服务器3,由于服务器是无状态的,服务器3会到redis中查找该用户状态,查询到已登录后,处理数据,成功返回!

2、session的工作原理

在使用之前,我们先了解一下session的原理,session是一段会话,由于http是无状态的,为了保存会话状态,才有了session。session在服务器端是存放在内存中的,如果登录用户多会占用很多内存资源,而session在客户端时保存在cookie中的一段文本JSESSIONID,而这个id正式链接客户端和服务器内存的凭证。

下图模拟用户登录后修改密码流程

在这里插入图片描述

3、session的生命周期

  1. session在用户首次访问系统时创建(访问静态资源不会创建)
  2. 生命周期可以由服务器手动配置,tomcat默认为30分钟
  3. 由于session对应的信息保存在cookie中,关闭浏览器并不会使session失效(除非改写session)
  4. 服务器端可以使用 invalidate() 手动失效session

4、 shiro的session

4.1、官方说明

默认 SecurityManager 实现默认使用 DefaultSessionManager 开箱即用。该 DefaultSessionManager 实现提供了应用程序所需的所有企业级会话管理功能,例如会话验证,孤立清理等。可在任何应用程序中使用。像所管理的所有其他组件一样 SecurityManagerSessionManager 可以通过 Shiro 的所有默认 SecurityManager 实现( getSessionManager()/ setSessionManager() )上的JavaBeans风格的getter / setter方法来获取或设置它们。每当创建或更新会话时,其数据都需要保留到存储位置,以便应用程序稍后可以访问它。类似地,当会话无效且已被更长时间使用时,需要将其从存储中删除,因此会话数据存储空间不会耗尽。这些SessionManager实现将这些创建/读取/更新/删除(CRUD)操作委托给一个内部组件,该组件SessionDAO反映了数据访问对象(DAO)设计模式。

SessionDAO的功能是您可以实现此接口以与所需的任何数据存储进行通信。这意味着您的会话数据可以驻留在内存中,文件系统上,关系数据库或NoSQL数据存储区中,或您需要的任何其他位置。您可以控制持久性行为。

您可以将任何SessionDAO实现配置为默认SessionManager实例上的属性。
我们可以根据下图查看到shiro的session具体结构。在这里插入图片描述

4.2、shiro默认session的实现

  1. 在之前的配置中,我们知道了shiro的核心配置项是 SecurityManager 类,发现这个类是提供 setSessionManager() 方法让我们自定义session管理器的。
	/**
	 * 注入 securityManager
	 */
	@Bean
	public SecurityManager securityManager() {
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		securityManager.setRealm(myShiroRealm());
		securityManager.setRememberMeManager(rememberMeManager());
		securityManager.setCacheManager(myEhCacheManager());// 将缓存管理交给ehCache
		securityManager.setSessionManager(****);//设置session管理器
		return securityManager;
	}
  1. 所有的配置都会注入我们使用的 DefaultWebSecurityManager 中,查看该类发现在构建实例的时候,系统默认设置了 ServletContainerSessionManager() 为session管理器
	class DefaultWebSecurityManager
	......
	//无参构造
    public DefaultWebSecurityManager() {
        super();
        ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
        this.sessionMode = HTTP_SESSION_MODE;
        setSubjectFactory(new DefaultWebSubjectFactory());
        setRememberMeManager(new CookieRememberMeManager());
        setSessionManager(new ServletContainerSessionManager());
    }
  1. 继续查看 ServletContainerSessionManager 类,在new的时候,会依次执行父类的构造,在 SessionsSecurityManager 中,我们发现了如下代码,设置sessionManager为 DefaultSessionManager
    在这里插入图片描述
    /**
     * Default no-arg constructor, internally creates a suitable default {@link SessionManager SessionManager} delegate
     * instance.
     */
    public SessionsSecurityManager() {
        super();
        this.sessionManager = new DefaultSessionManager();
        applyCacheManagerToSessionManager();
    }
  1. 而在 DefaultSessionManager 的构造中,我们找到了默认提供的sessionDao的实现 MemorySessionDAO
class DefaultSessionManager
	......
    public DefaultSessionManager() {
        this.deleteInvalidSessions = true;
        this.sessionFactory = new SimpleSessionFactory();
        this.sessionDAO = new MemorySessionDAO();
    }
  1. MemorySessionDAO 关系图,可以看到,他继承了AbstractSessionDAO抽象类,而顶级接口则是SessionDao层,另外以外的发现了shiro为我们提供的一个企业级session缓存的实现类 EnterpriseCacheSessionDAO
    在这里插入图片描述
    6.查看了一下 EnterpriseCacheSessionDAO 类发现,这个缓存是利用了shiro提供的 Cache 接口 下的 MapCache 实现类实现的,ConcurrentHashMap作为value来保证线程安全!
public class EnterpriseCacheSessionDAO extends CachingSessionDAO {

    public EnterpriseCacheSessionDAO() {
        setCacheManager(new AbstractCacheManager() {
            @Override
            protected Cache<Serializable, Session> createCache(String name) throws CacheException {
                return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());
            }
        });
    }
......
  1. shiro 提供的Cache接口另外实现类
    在这里插入图片描述
    8.回到 MemorySessionDAO 我们查看这个类的相关代码
public class MemorySessionDAO extends AbstractSessionDAO {

    private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class);
	//保存session的容器
    private ConcurrentMap<Serializable, Session> sessions;
//构造
    public MemorySessionDAO() {
        this.sessions = new ConcurrentHashMap<Serializable, Session>();
    }
	//创建session
    protected Serializable doCreate(Session session) {
    //sessionid生成器
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        storeSession(sessionId, session);
        return sessionId;
    }
//存储会话
    protected Session storeSession(Serializable id, Session session) {
        if (id == null) {
            throw new NullPointerException("id argument cannot be null.");
        }
        //只有在key不存在或者key为null的时候,value值才会被覆盖  jdk 1.8新特性
        return sessions.putIfAbsent(id, session);
        /**putIfAbsent 源码展示
		    default V putIfAbsent(K key, V value) {
		        V v = get(key);
		        if (v == null) {
		            v = put(key, value);
		        }
	        	return v;
	    	}
		*/
    }
//根据sessionid获取session对象
    protected Session doReadSession(Serializable sessionId) {
        return sessions.get(sessionId);
    }
//根据sessionid更新session对象
    public void update(Session session) throws UnknownSessionException {
        storeSession(session.getId(), session);
    }
//删除session
    public void delete(Session session) {
        if (session == null) {
            throw new NullPointerException("session argument cannot be null.");
        }
        Serializable id = session.getId();
        if (id != null) {
            sessions.remove(id);
        }
    }
//获取存活的session对象
    public Collection<Session> getActiveSessions() {
        Collection<Session> values = sessions.values();
        if (CollectionUtils.isEmpty(values)) {
            return Collections.emptySet();
        } else {
            return Collections.unmodifiableCollection(values);
        }
    }

}

可以看到,在默认情况下,session的操作全是有 MemorySessionDAO 类实现的,我们猜想,新增redis操作类,继承 AbstractSessionDAO ,重写其中的重要方法,是不是就可以实现redis管理session了?

5、使用redis管理session(配置)

redis的安装就不介绍了,只讲解如何在shiro中使用redis管理session!

5.1 、引入jar包

上面虽然已经发现如何替换session操作类来实现session的redis管理,但是已经有的轮子直接拿来即可,首先我们要引入redis的jar

<!-- shiro-redis -->
		<dependency>
			<groupId>org.crazycake</groupId> 
			<artifactId>shiro-redis</artifactId>
			<version>3.1.0</version>
		</dependency>

我在引入上面的包后会报错,引入这个解决,未报错不用引入,报错原因未深究
err:Missing artifact com.sun:tools:jar:1.8.0

<!-- tools -->
		<dependency>
			<groupId>com.sun</groupId>
			<artifactId>tools</artifactId>
			<version>1.8.0</version>
			<scope>system</scope>
			<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
			<optional>true</optional>
		</dependency>

5.2、新增redis配置

在boot配置文件中新增如下文件,我采用的是yml文件,其他格式需要自行转换
application.yml

#redis
redis:
  #redis机器ip
  host: 127.0.0.1
  #redis端口
  port: 6379
  #redis密码
  password: 123456
  #默认数据库
  database: 10
  #redis超时时间(毫秒),如果不设置,取默认值2000
  timeout: 10000

5.3、ShiroConfig配置

  1. 新增读取配置项属性及redis实例和redisdao实例
    @Value("${redis.host}")
    private String host;

    @Value("${redis.port}")
    private int port;

    @Value("${redis.password}")
    private String password;

    @Value("${redis.database}")
    private int database;

    @Value("${redis.timeout}")
    private int timeout;


    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);// 主机地址
        redisManager.setPort(port);// 端口
        redisManager.setPassword(password);// 访问密码
        redisManager.setDatabase(database);// 默认数据库
        redisManager.setTimeout(timeout);// 过期时间
        return redisManager;
    }

    /**
     * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件 MemorySessionDAO 直接在内存中进行会话维护
     * EnterpriseCacheSessionDAO
     * 提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
     * 
     * @return
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDao = new RedisSessionDAO();
        redisSessionDao.setKeyPrefix("shiro-session");//配置session前缀
        redisSessionDao.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDao.setRedisManager(redisManager());
        // session在redis中的保存时间,最好大于session会话超时时间
        redisSessionDao.setExpire(timeout);
        return redisSessionDao;
    }
  1. 配置自定义sessionid生成器
    /**
     * 配置会话ID生成器
     * 
     * @return
     */
    @Bean
    public SessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator();
    }
  1. 配置session监听器
    /**
     * 配置session监听
     * 
     * @return
     */
    @Bean
    public MySessionListener sessionListener() {
        MySessionListener sessionListener = new MySessionListener();
        return sessionListener;
    }

MySessionListener 类


import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
/**
 * 
 * @ClassName:  MySessionListener   
 * @Description 统计session数量
 * @version 
 * @author JH
 * @date 2019年9月2日 上午11:15:38
 */
public class MySessionListener implements SessionListener {

    private final AtomicInteger sessionCount = new AtomicInteger(0);

    /**
     * 登录
     */
    @Override
    public void onStart(Session session) {
        sessionCount.incrementAndGet();
        System.out.println("登录,有效session数量:"+sessionCount.get());
    }

    /**
     * 登出
     */
    @Override
    public void onStop(Session session) {
        sessionCount.decrementAndGet();
        System.out.println("登出,有效session数量:"+sessionCount.get());
    }

    /**
     * session过期
     */
    @Override
    public void onExpiration(Session session) {
        sessionCount.decrementAndGet();
        System.out.println("session过期,有效session数量:"+sessionCount.get());
    }

}
  1. 配置自定义session的cookie,替换JSESSIONID
    /**
     * 配置保存sessionId的cookie 注意:这里的cookie 不是上面的记住我 cookie 记住我需要一个cookie session管理
     * 也需要自己的cookie 默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid
     * 
     * @return
     */
    @Bean("sessionIdCookie")
    public SimpleCookie sessionIdCookie() {
        // 这个参数是cookie的名称
        SimpleCookie simpleCookie = new SimpleCookie("REDIS-SESSION");
        // setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:

        // setcookie()的第七个参数
        // 设为true后,只能通过http访问,javascript无法访问
        // 防止xss读取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        // maxAge=-1表示浏览器关闭时失效此Cookie
        simpleCookie.setMaxAge(-1);
        return simpleCookie;
    }
  1. 配置会话管理器
    /**
     * 配置会话管理器,设定会话超时及保存
     * 
     * @return
     */
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        Collection<SessionListener> listeners = new ArrayList<SessionListener>();
        // 配置监听
        listeners.add(sessionListener());
        sessionManager.setSessionListeners(listeners);
        sessionManager.setSessionIdCookie(sessionIdCookie());
        sessionManager.setSessionDAO(redisSessionDAO());
        
        // 全局会话超时时间(单位毫秒),默认30分钟 暂时设置为10秒钟 用来测试
        sessionManager.setGlobalSessionTimeout(1800000);//单位毫秒
        // 是否开启删除无效的session对象 默认为true
        sessionManager.setDeleteInvalidSessions(true);
        // 是否开启定时调度器进行检测过期session 默认为true
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
        // 设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler
        // 底层也是默认自动调用ExecutorServiceSessionValidationScheduler
        sessionManager.setSessionValidationInterval(3600000);//单位毫秒
        // 取消url 后面的 JSESSIONID,设置为false为取消
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;

    }
  1. 将会话管理器交给 securityManager 管理
    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        securityManager.setRememberMeManager(rememberMeManager());
        securityManager.setCacheManager(myEhCacheManager());// 将缓存管理交给ehCache
        securityManager.setSessionManager(sessionManager());//将session管理交给reids
        return securityManager;
    }

至此,shiro的session交给redis管理已经配置完成了,很简单啊!

6、验证猜想

  • 6.1、在之前的源码跟踪中,我们猜想重写一个类继承 AbstractSessionDAO* ,替换 MemorySessionDAO ,在引入 shiro-redis jar包后,查看是用的redisSessionDAO关系图,验证猜想!
    在这里插入图片描述
  • 6.2、部分 RedisSessionDAO 源码
	@Override
	protected Serializable doCreate(Session session) {
		if (session == null) {
			logger.error("session is null");
			throw new UnknownSessionException("session is null");
		}
		Serializable sessionId = this.generateSessionId(session);  
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
		return sessionId;
	}

	@Override
	protected Session doReadSession(Serializable sessionId) {
		if (sessionId == null) {
			logger.warn("session id is null");
			return null;
		}
		Session s = getSessionFromThreadLocal(sessionId);

		if (s != null) {
			return s;
		}

		logger.debug("read session from redis");
		try {
			s = (Session) valueSerializer.deserialize(redisManager.get(keySerializer.serialize(getRedisSessionKey(sessionId))));
			setSessionToThreadLocal(sessionId, s);
		} catch (SerializationException e) {
			logger.error("read session error. settionId=" + sessionId);
		}
		return s;
	}

	private void setSessionToThreadLocal(Serializable sessionId, Session s) {
		Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
		if (sessionMap == null) {
            sessionMap = new HashMap<Serializable, SessionInMemory>();
            sessionsInThread.set(sessionMap);
        }
		SessionInMemory sessionInMemory = new SessionInMemory();
		sessionInMemory.setCreateTime(new Date());
		sessionInMemory.setSession(s);
		sessionMap.put(sessionId, sessionInMemory);
	}

  • 6.3 redis集群
    在使用shiro-redis工具包的时候,惊讶的发现了它已经为我们提供了redis集群的支持,后续会进行测试。
    在这里插入图片描述

7、测试

  1. 打开登录页面,查看cookie是否被修改(已修改为我们设置的名称)
    在这里插入图片描述
  2. session监听器控制台打印人数
    在这里插入图片描述
    3.redis可视化工具(成功将session写入redis中)
    在这里插入图片描述

8、源码

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值