Shiro安全框架基于Redis的分布式集群方案

前段时间做了一个市场推广相关的项目,安全框架使用的是Shiro,缓存框架使用的是spring-data-redis。为了使用户7x24小时访问,决定把项目由单机升级为分布式部署架构。但是安全框架shiro只有单机存储的SessionDao,尽管Shrio有基于Ehcache-rmi的组播/广播实现,然而集群的分布往往是跨网段的,甚至是跨地域的,所以寻求新的方案。

运行环境

Nginx + Tomcat7(3台) + JDK1.7

项目架构图

项目实现

pom.xml引入配置(版本自行更换):

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>1.7.10.RELEASE</version>
</dependency>

redis.properties配置:

#============================#
#===== redis sttings     ====#
#============================#
redis.host=127.0.0.1
redis.port=6379
redis.password=123456
#单位秒
redis.expire=1800
redis.timeout=2000
redis.usepool=true
redis.database=1

spring-context-redis.xml配置:

    <!-- redis 配置 -->
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig" />

    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="${redis.host}" />
        <property name="port" value="${redis.port}" />
        <property name="password" value="${redis.password}" />
        <property name="timeout" value="${redis.timeout}" />
        <property name="poolConfig" ref="jedisPoolConfig" />
        <property name="usePool" value="true" />
    </bean>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
    </bean>

RedisSessionDAO配置(重写 AbstractSessionDAO):

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
 * 重写 AbstractSessionDAO
 * 使用Redis缓存
 * 创建者 张志朋
 * 创建时间    2018年1月10日
 */
public class RedisSessionDAO extends AbstractSessionDAO {

    private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
    /**
     * shiro-redis的session对象前缀
     */
    private RedisTemplate<String, Object> redisTemplate;
    // 0 - never expire
    private int expire = 3600000;
    
    
    /**
     * The Redis key prefix for the sessions 
     */
    private String keyPrefix = "shiro_market_redis_session:";
    
    @Override
    public void update(Session session) throws UnknownSessionException {
        this.saveSession(session);
    }
    
    /**
     * save session
     * @param session
     * @throws UnknownSessionException
     */
    private void saveSession(Session session) throws UnknownSessionException{
        if(session == null || session.getId() == null){
            logger.error("session or session id is null");
            return;
        }
        
        String key = session.getId().toString();
        session.setTimeout(expire);        
        redisTemplate.opsForValue().set(keyPrefix+key, session, expire, TimeUnit.MILLISECONDS);
    }

    @Override
    public void delete(Session session) {
        if(session == null || session.getId() == null){
            logger.error("session or session id is null");
            return;
        }
        redisTemplate.delete(keyPrefix+session.getId().toString());

    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<Session> sessions = new HashSet<Session>();
        Set<String> keys = redisTemplate.keys(this.keyPrefix + "*");
        if(keys != null && keys.size()>0){
            for(String key:keys){
                Session s = (Session)redisTemplate.opsForValue().get(key);
                sessions.add(s);
            }
        }
        
        return sessions;
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);  
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        if(sessionId == null){
            logger.error("session id is null");
            return null;
        }
        Session s = (Session)redisTemplate.opsForValue().get(keyPrefix+sessionId);
        return s;
    }
    
    /**
     * Returns the Redis session keys
     * prefix.
     * @return The prefix
     */
    public String getKeyPrefix() {
        return keyPrefix;
    }

    /**
     * Sets the Redis sessions key 
     * prefix.
     * @param keyPrefix The prefix
     */
    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

spring-shiro.xml配置:

<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- 会话超时时间,单位:毫秒  20m=1200000ms, 30m=1800000ms, 60m=3600000ms-->
        <!-- 设置session过期时间为1小时(单位:毫秒),默认为30分钟 -->
        <!-- 如果设置 Redis缓存 此处不生效将 -->
        <property name="globalSessionTimeout" value="3600000"></property>
        <property name="sessionValidationSchedulerEnabled" value="true"></property>
        <property name="sessionIdUrlRewritingEnabled" value="false"></property>
        <!-- 注入 redisSessionDAO -->
        <property name="sessionDAO" ref="sessionDAO"/>
    </bean>

    <!-- redisSessionDAO -->
    <bean id="sessionDAO" class="com.acts.market.common.session.RedisSessionDAO">
        <property name="redisTemplate" ref="redisTemplate" />
    </bean>

乱码问题

2018年1月11日,新增了一个在线用户查询的功能,使用API查询所有用户:

 Collection<Session> sessions =  redisSessionDAO.getActiveSessions();

结果sessions的size居然为空,继续跟踪底层代码:

private String keyPrefix = "shiro_market_redis_session:";

@Override
public Collection<Session> getActiveSessions() {
    Set<Session> sessions = new HashSet<Session>();
    Set<Serializable> keys = redisTemplate.keys(this.keyPrefix + "*");
    if(keys != null && keys.size()>0){
        for(Serializable key:keys){
            Session s = (Session)redisTemplate.opsForValue().get(key);
            sessions.add(s);
        }
    }
    return sessions;
}

感觉API没啥问题,后台登录redis查询下:

./redis-cli -h 192.168.1.180
# 输入 auth password (没有设置密码的略过)

查看所有Keys:

keys *

keys中居然出现了乱码

由于之前是精确匹配,虽然也有乱码的问题,但是可以查询出来,这次模糊匹配就出问题了。

由于我们使用的是spring-data-redis 中的核心操作类是 RedisTemplate<K, V>, key 和 value 都是泛型的,这就涉及到将类型进行序列化的问题了。

RedisTemplate源码中存在以下序列环工具类:

private RedisSerializer<?> defaultSerializer;
private ClassLoader classLoader;
private RedisSerializer keySerializer = null;
private RedisSerializer valueSerializer = null;
private RedisSerializer hashKeySerializer = null;
private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = new StringRedisSerializer();

默认使用的是:

if (defaultSerializer == null) {
    defaultSerializer = new JdkSerializationRedisSerializer(
        classLoader != null ? classLoader : this.getClass().getClassLoader());
}

继续跟踪JdkSerializationRedisSerializer中的序列化方法:

public byte[] serialize(Object object) {
        if (object == null) {
            return SerializationUtils.EMPTY_ARRAY;
        }
        try {
            return serializer.convert(object);
        } catch (Exception ex) {
            throw new SerializationException("Cannot serialize", ex);
        }
    }

SerializingConverter 类中的转化方法:

/**
     * Serializes the source object and returns the byte array result.
     */
    @Override
    public byte[] convert(Object source) {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream(1024);
        try  {
            this.serializer.serialize(source, byteStream);
            return byteStream.toByteArray();
        }
        catch (Throwable ex) {
            throw new SerializationFailedException("Failed to serialize object using " +
                    this.serializer.getClass().getSimpleName(), ex);
        }
    }

由于项目中使用String作为缓存的key,变更了序列化类就可以了。

解决办法:

<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
                    p:connection-factory-ref="jedisConnectionFactory">
    <property name="keySerializer">
       <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    </property>
    <property name="hashKeySerializer">
       <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
    </property>
</bean>

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏阮一峰的网络日志

Nginx 容器教程

春节前,我看到 Nginx 加入了 HTTP/2 的 server push 功能,就很想试一下。 正好这些天,我在学习 Docker,就想到可以用 Nginx...

45340
来自专栏前端小叙

vue-cli打包之后的项目在nginx的部署

vue-cli执行 npm run build 进行打包,生成dist文件夹,把该文件夹下的文件直接复制到nginx服务器目录下,就可打开项目,但是只有首页是可...

61080
来自专栏IT笔记

Nginx学习之静态文件服务器配置

在Java开发过程以及生产环境中,最常用的web应用服务器当属Tomcat,尽管这只猫也能够处理一些静态请求,例如图片、html、样式文件等,但是效率并不是那么...

689100
来自专栏安恒信息

漏洞预警 | Nginx range过滤器模块存在远程信息泄露漏洞(CVE-2017-7529)

Nginx是一款使用非常广泛的高性能Web服务器。 Nginx的range过滤器模块中存在安全漏洞,特制的请求可能触发整数溢出,导致泄露敏感信息。 在处理HTT...

31350
来自专栏技术博文

如何查看已经安装的nginx、apache、mysql和php的编译参数

1、nginx编译参数: nginx -V(大写) #注意:需保证nginx在环境变量中,或者使用这样的形式:/user/local/nginx/sbin/ng...

53680
来自专栏架构说

线程池的作用

问题: nginx 写的线程池太过抽象根本理解不了 例如 pthread_create()函数创建了线程 线程就开始提供服务了(还需要提供别人使用),回收更解...

449130
来自专栏Albert陈凯

Hadoop数据分析平台实战——150Flume介绍离线数据分析平台实战——150Flume介绍

离线数据分析平台实战——150Flume介绍 Nginx介绍 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。...

33070
来自专栏前端小叙

查看端口被占用的情况以及如何解除端口占用

在windows安装好nginx之后,打开nginx.exe失败,我想应该是80端口被占用了,遂找到此方法。 注:以下命令需要在管理员权限下运行 以下文章主要以...

46750
来自专栏技术博文

LNMP源码编译安装(centos7+nginx1.9+mysql5.6+php7)

1.准备工作: 1)把所有的软件安装在/Data/apps/,源码包放在/Data/tgz/,数据放在/Data/data,日志文件放在/Data/logs,项...

50460
来自专栏逸鹏说道

Session分布式共享 = Session + Redis + Nginx

一、Session 1、Session 介绍 我相信,搞Web开发的对Session一定再熟悉不过了,所以我就简单的介绍一下。 Sess...

49350

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励