Shrio+Redis实现tomcat集群session共享

一、背景

  当我们使用了nginx做项目集群以后,就会出现一个很严重的问题亟待解决,那就是:tomcat集群之间如何实现session共享的问题,如果这个问题不解决,就会出现登陆过后再次请求资源依旧需要登陆的问题。这篇文章我们就解决这个问题。

二、实现步骤

说明:本篇是在spring+shiro集成的基础上进行改进的,如果不知道springshiro怎么集成,请移步:spring集成shiro做登陆认证

1.pom.xml中添加shiro-redisjedis的依赖

<dependency>

     <groupId>org.crazycake</groupId>

     <artifactId>shiro-redis</artifactId>

     <version>2.4.2.1-RELEASE</version>

</dependency>

<dependency>

     <groupId>redis.clients</groupId>

     <artifactId>jedis</artifactId>

      <version>2.7.2</version>

</dependency>

2.首先我们需要对redis进行集成,在resources下新建config.properties

#redis pool config

redis.pool.maxActive=200

redis.pool.maxIdle=100

redis.pool.maxWait=100

redis.pool.testOnBorrow=true

 

#redis config

redis.host=192.168.85.129

redis.port=6379

redis.timeout=2000

redis.password=123456

redis.dbindex=8

redis.default.expire=1800000

3.resources/spring文件夹下新建spring-redis.xml来集成redis操作客户端

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd">

 

    <description>Redis configuration</description>

 

    <bean id="redisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">

        <property name="maxTotal" value="${redis.pool.maxActive}"/>

        <property name="maxIdle" value="${redis.pool.maxIdle}"/>

        <property name="maxWaitMillis" value="${redis.pool.maxWait}"/>

        <property name="testOnBorrow" value="${redis.pool.testOnBorrow}"/>

    </bean>

 

    <bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy">

        <constructor-arg ref="redisPoolConfig"/>

        <constructor-arg value="${redis.host}"/>

        <constructor-arg type="int" value="${redis.port}"/>

        <constructor-arg type="int" value="${redis.timeout}"/>

        <constructor-arg type="java.lang.String" value="${redis.password}"/>

        <constructor-arg type="int" value="${redis.dbindex}"/>

    </bean>

 

    <bean id="redisClient" class="com.hafiz.www.redis.RedisClient">

        <constructor-arg name="jedisPool" ref="jedisPool"/>

        <property name="expire" value="${redis.default.expire}"/>

    </bean>

</beans>

4.添加redisClient.java作为访问redis的客户端

package com.hafiz.www.redis;

 

import org.crazycake.shiro.RedisManager;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import redis.clients.jedis.Jedis;

import redis.clients.jedis.JedisPool;

 

import java.util.Set;

 

/**

 * Desc: Jedis 操作客户端

 * Created by hafiz.zhang on 2017/7/21.

 */

public class RedisClient extends RedisManager{

 

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisClient.class);

 

    private static JedisPool jedisPool = null;

 

    public RedisClient(JedisPool jedisPool) {

        this.jedisPool = jedisPool;

    }

 

    public void init() {

        super.init();

    }

 

    @Override

    public byte[] get(byte[] key) {

        Jedis jedis = jedisPool.getResource();

        byte[] value;

 

        try {

            value = jedis.get(key);

        } catch (Exception e) {

            LOGGER.error("redis key:{} get value occur exception", new String(key));

            throw new RuntimeException("redis operation error:", e);

        } finally {

            jedis.close();

        }

 

        return value;

    }

 

    @Override

    public byte[] set(byte[] key, byte[] value) {

        Jedis jedis = jedisPool.getResource();

 

        try {

            jedis.set(key, value);

            Integer expire = getExpire();

            if(expire != 0) {

                jedis.expire(key, expire);

            }

        } catch (Exception e) {

            LOGGER.error("redis key:{} set value:{} occur exception", new String(key), new String(value));

            throw new RuntimeException("redis operation error:", e);

        } finally {

            jedis.close();

        }

 

        return value;

    }

 

    @Override

    public byte[] set(byte[] key, byte[] value, int expire) {

        Jedis jedis = jedisPool.getResource();

 

        try {

            jedis.set(key, value);

            if(expire != 0) {

                jedis.expire(key, expire);

            }

        } catch (Exception e) {

            LOGGER.error("redis key:{} set value:{} in expire:{} occur exception", new String(key), new String(value), expire);

            throw new RuntimeException("redis operation error:", e);

        } finally {

            jedis.close();

        }

 

        return value;

    }

 

    @Override

    public void del(byte[] key) {

        Jedis jedis = jedisPool.getResource();

 

        try {

            jedis.del(key);

        } catch (Exception e) {

            LOGGER.error("redis key:{} del value occur exception", new String(key));

            throw new RuntimeException("redis operation error:", e);

        } finally {

            jedis.close();

        }

    }

 

    @Override

    public void flushDB() {

        Jedis jedis = jedisPool.getResource();

 

        try {

            jedis.flushDB();

        } catch (Exception e) {

            LOGGER.error("redis flushDB occur exception");

            throw new RuntimeException("redis operation error:", e);

        } finally {

            jedis.close();

        }

 

    }

 

    @Override

    public Long dbSize() {

        Long dbSize = Long.valueOf(0L);

        Jedis jedis = jedisPool.getResource();

 

        try {

            dbSize = jedis.dbSize();

        } catch (Exception e) {

            LOGGER.error("redis get dbSize occur exception");

            throw new RuntimeException("redis operation error:", e);

        } finally {

            jedis.close();

        }

 

        return dbSize;

    }

 

    @Override

    public Set<byte[]> keys(String pattern) {

        Set keys = null;

        Jedis jedis = jedisPool.getResource();

 

        try {

            keys = jedis.keys(pattern.getBytes());

        } catch (Exception e) {

            LOGGER.error("redis get keys in pattern:{} occur exception", pattern);

            throw new RuntimeException("redis operation error:", e);

        } finally {

            jedis.close();

        }

 

        return keys;

    }

}

5.接着,我们对spring-shiro.xml做如下修改(红色标记的部分)

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:util="http://www.springframework.org/schema/util"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/util

        http://www.springframework.org/schema/util/spring-util.xsd">

 

    <description>Shiro Configuration</description>

 

    <!--shiro-redis redisCacheManager-->

    <bean id="redisCacheManager" class="org.crazycake.shiro.RedisCacheManager">

        <property name="keyPrefix" value="shiro_redis_session:"/>

        <property name="redisManager" ref="redisClient"/>

    </bean>

 

    <!--custom myself realm-->

    <bean id="customRealm" class="com.hafiz.www.shiro.CustomRealm">

        <property name="cacheManager" ref="redisCacheManager"/>

    </bean>

 

    <!--redisSessionDAO-->

    <bean id="redisSessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"></bean>

 

 

    <!--simpleCookie,不定义在集群环境下会出现There is no session with id ....-->

    <bean id="simpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie">

        <constructor-arg name="name" value="custom.session"/>

        <property name="path" value="/"/>

    </bean>

 

    <!--sessionManager-->

    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">

        <property name="sessionDAO" ref="redisSessionDAO"/>

        <property name="sessionIdCookie" ref="simpleCookie"/>

    </bean>

 

    <!--Shiro`s main business-tier object for web-enable applications-->

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">

        <property name="realm" ref="customRealm"/>

        <property name="cacheManager" ref="redisCacheManager"/>

        <property name="sessionManager" ref="sessionManager"/>

    </bean>

 

    <!--shiro filter-->

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">

        <property name="securityManager" ref="securityManager"/>

        <property name="loginUrl" value="/login.html"/>

        <property name="successUrl" value="/index.html"/>

        <property name="unauthorizedUrl" value="/unauthorized.html"/>

        <property name="filters">

            <util:map>

                <entry key="auth">

                    <bean class="com.hafiz.www.filter.AuthorizeFilter"/>

                </entry>

            </util:map>

        </property>

        <property name="filterChainDefinitions">

            <value>

                /login.json = anon

                /logout.json = anon

                /js/** = anon

                / = authc

                /** = auth

            </value>

        </property>

    </bean>

</beans>

6.spring.xml做如下修改

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:context="http://www.springframework.org/schema/context"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

        http://www.springframework.org/schema/beans/spring-beans.xsd

        http://www.springframework.org/schema/context

        http://www.springframework.org/schema/context/spring-context.xsd">

   

    <context:property-placeholder location="classpath:config.properties,classpath:jdbc.properties"/>

    <import resource="spring-*.xml"/>

 

</beans>

到此我们就完了shiro+redis实现session共享的问题,其实也很简单,其中的实现逻辑也很简单,就是shiro的拦截器会首先去redis里面获取session,作为当本次请求的session.

其他代码以及简单测试代码不再贴出,给出github地址:https://github.com/hafizzhang/shiro-session-cluster.git

三、总结

  通过本文,我们就完成了spring+shiro+redis实现集群session共享的问题,经过亲测可行。但有一点遗憾的地方,就是每次请求至少会有8redis的读操作,一次写操作,这个问题还没有找到很好的解决办法,本地使用ehcahe也不太现实,因为涉及到本地缓存和redis缓存同步的问题,如果你有好的办法,欢迎讨论交流学习!