红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 其它
  3. 正文

Shiro 学习笔记4:分布式会话

2023年9月24日 937点热度 0人点赞 0条评论

在上篇文章中,我们已经在 Spring Boot 项目中集成了 Shiro,并实现了权限控制。最后用 Redis 缓存用户权限信息的方式优化了 Shiro 鉴权的性能。

但这个项目有一个问题,即只能作为单体应用部署。原因是项目用于跟踪用户状态信息的 Session 实际上存储在内存中。如果对项目进行集群部署,多台服务器之间的内存中 Session 显然是无法共享的。

1.SessionDao

Shiro 使用的 Session 存储方式是由 SessionManager 决定的:

@Bean
public DefaultWebSessionManager webSessionManager() {
    DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
    // 不使用 Session 的验证和更新机制
    webSessionManager.setSessionValidationSchedulerEnabled(false);
    // 设置 session 使用的 cookie
    webSessionManager.setSessionIdCookieEnabled(true);
    webSessionManager.setSessionIdCookie(simpleCookie());
    // 设置 session 的有效时长(超过后 session 失效)单位毫秒
    webSessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
    return webSessionManager;
}

这里DefaultWebSessionManager继承自DefaultSessionManager,而DefaultSessionManager中有一个属性SessionDAO,决定具体的 Session 存储方式,而默认的sessionDao实现是MemorySessionDAO:

public class DefaultSessionManager extends AbstractValidatingSessionManager implements CacheManagerAware {
    protected SessionDAO sessionDAO;  
    // ...
    public DefaultSessionManager() {
        this.deleteInvalidSessions = true;
        this.sessionFactory = new SimpleSessionFactory();
        this.sessionDAO = new MemorySessionDAO();
    }
}

显然,MemorySessionDAO是一个在内存中实现 Session 存储的方案。

如果要改用其他的存储方式,我们可以选择实现一个自定义的SessionDao以替换默认实现。这里我们使用 Redis 作为存储来实现 SessionDao。

1.1.RedisTemplate

在实现SessionDao之前,我们需要先实现一个配套的 RedisTemplate,之前我们实现的 RedisTemplate 对 Value 序列化的方式是 JSON,这样做的好处是 Redis 中存储的数据有良好的可读性,缺点是对于某些对象无法成功序列化,比如这里 Shiro 的 Session 对象。这里定义一个新的 RedisTemplate,并使用 JDK 对象序列化的方式对 Value 序列化和反序列化:

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    // ...
    @Bean
    public RedisTemplate<String, Session> sessionRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Session> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setValueSerializer(new JdkSerializationRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }
}

1.2.RedisSessionDao

现在实现 SessionDao:

public class RedisSessionDao extends AbstractSessionDAO {
    @Resource(name = "sessionRedisTemplate")
    private RedisTemplate<String, Session> redisTemplate;
​
    private final String REDIS_SESSION_KEY_PREFIX = "session:";
    /**
     * session 过期时长,单位秒
     */
    @Getter
    @Setter
    private Long timeout = 1800L;
​
    /**
     * 创建 Session
     *
     * @param session Session 对象
     * @return sessionID
     */
    @Override
    protected Serializable doCreate(Session session) {
        // 生成新的唯一的 SessionID
        Serializable sessionId = generateSessionId(session);
        // 为 Session 对象赋予新生成的 SessionID
        assignSessionId(session, sessionId);
        // 保存 Session
        storeSession(sessionId, session);
        return sessionId;
    }
​
    /**
     * 获取 Redis 中存储的 Session 的 key
     *
     * @param id SessionID
     * @return Redis 中存储的 key
     */
    private String getRedisStoreSessionKey(Serializable id) {
        return REDIS_SESSION_KEY_PREFIX + id.toString();
    }
​
    /**
     * 将 Session 对象存储到 Redis
     *
     * @param id      SessionID
     * @param session Session 对象
     */
    private void storeSession(Serializable id, Session session) {
        ValueOperations<String, Session> ops = redisTemplate.opsForValue();
        String key = getRedisStoreSessionKey(id);
        ops.set(key, session, timeout, TimeUnit.SECONDS);
    }
​
    /**
     * 从 Redis 读取 Session 对象
     *
     * @param sessionId SessionID
     * @return Session 对象
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        ValueOperations<String, Session> ops = redisTemplate.opsForValue();
        return ops.get(getRedisStoreSessionKey(sessionId));
    }
​
    /**
     * 将更新后的 Session 对象写入 Redis
     *
     * @param session Session 对象
     * @throws UnknownSessionException
     */
    @Override
    public void update(Session session) throws UnknownSessionException {
        storeSession(session.getId(), session);
    }
​
    /**
     * 从 Redis 中删除 Session 对象
     *
     * @param session
     */
    @Override
    public void delete(Session session) {
        if (session == null) {
            throw new NullPointerException("session argument cannot be null.");
        }
        Serializable id = session.getId();
        if (id != null) {
            ValueOperations<String, Session> ops = redisTemplate.opsForValue();
            ops.getAndDelete(getRedisStoreSessionKey(id));
        }
    }
​
    /**
     * 获取存活的 Session 对象
     *
     * @return Session 对象序列
     */
    @Override
    public Collection<Session> getActiveSessions() {
        Set<String> keys = redisTemplate.keys(REDIS_SESSION_KEY_PREFIX + "?");
        ValueOperations<String, Session> ops = redisTemplate.opsForValue();
        List<Session> sessions = new ArrayList<>();
        keys.forEach(key -> {
            Session session = ops.get(key);
            if (session != null) {
                sessions.add(session);
            }
        });
        return sessions;
    }
}

在配置类中使用这个 SessionDao 设置 SessionManager:

@Configuration
public class ShiroConfig {
    // ...
    @Bean
    public RedisSessionDao redisSessionDao(){
        RedisSessionDao redisSessionDao = new RedisSessionDao();
        redisSessionDao.setTimeout(sessionProperties.getTimeout());
        return redisSessionDao;
    }
    
    @Bean
    public DefaultWebSessionManager webSessionManager() {
        DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
        webSessionManager.setSessionDAO(redisSessionDao());
        // ...
        return webSessionManager;
    }
}

这里使用了一个配置类以读取 Spring 默认控制 Session 过期时间的配置spring.session.timeout:

@Configuration
@ConfigurationProperties(prefix = "spring.session")
@NoArgsConstructor
@Getter
@Setter
public class SessionProperties {
    // session 过期时长,单位秒
    private Long timeout;
}

配置文件 application.yml 中过期时间设置为 30 分钟:

spring:
  session:
    timeout: 1800 # session 超时时间,单位秒

2.测试

可以用 Nginx 搭建一个分布式应用进行测试。

在不同的两个端口启动两个应用实例:

image-20230924194223371

在 Nginx 中配置负载均衡访问:

http {
    upstream boot-demo {
        server 127.0.0.1:8080;
        server 127.0.0.1:8081;
    }
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   html;
            proxy_pass http://boot-demo;
            index  index.html index.htm;
        }
    }
}

现在对 http://localhost:80 的请求会被转发到 http://localhost:8080 和 http://localhost:8081,这两个服务都使用同一个 Redis 作为 Session,所以都可以对用户的会话进行跟踪。

3.redis-shiro

实际上有一个开源项目 shiro-redis 提供了基于 Redis 存储的 Shiro 完整整合方案,如果是 Spring Boot 项目,还可以很方便的用一个 Starter 快速完成整合。该方案不仅实现了一个 RedisSessionDao 以提供基于 Redis 的 Session 支持,还实现了基于 Redis 的 CacheManager,可以将 Realm 的认证和授权信息用 Redis 进行缓存。

如果你不需要对 Redis 存储 Shiro Session 以及缓存信息的细粒度控制,直接使用该开源项目是一个不错的方案。

The End,谢谢阅读。

本文的完整示例可以从这里获取。

4.参考资料

  • Jackson 反序列化 — UnrecognizedPropertyException异常解决方案_i余数的博客-CSDN博客

  • Windows 基于 WSL 2 安装 Redis 并设置局域网访问 - 前端之路 (niceue.com)

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: redis session shiro
最后更新:2023年9月24日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号