关于负载均衡session共享问题的解决

我用过的只有两种不同的方法:

  1. 第一种:可以在nginx中加入ip_hash
    这样来解决session共享问题.如果这样的话,若你保存的seesion那台服务器挂了,会退出登录,影响客户体验.(不推荐使用)

spring security session禁用 springboot禁用session的创造_java

第二种:可以加入redis进行来解决session共享问题.

由于我采用的是shiro进行安全验证和授权管理,现需要将shiro管理的session交由redis管理,具体实现代码如下:

导入的jar包

<!-- 集成shiro -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${shiro.version}</version>
</dependency>
<!—session相关的-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-core</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- redis相关 -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.0-m1</version>
</dependency>
<!—启动redis的jar包-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第二步application.properties中加入配置(由于redis相关登录配置,springboot已自动实现,不在重复写)

spring.session.store-type=redis
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session2
shiro.globalSessionTimeout=3600000  #超时时间

第三步:加入缓冲管理,一系列缓冲相关配置.还需要加入session交由redis管理的类
RedisCacheManager.java

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;

//提供给shiro指定缓存管理类的
public class RedisCacheManager  implements CacheManager {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        return new RedisCache<K,V>(name, redisTemplate);
    }

}

RedisCache.java

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

public class RedisCache<K, V> implements Cache<K, V> {

    private static final String REDIS_SHIRO_CACHE = RedisConstants.REDIS_SHIRO_CACHE;
    private String cacheKey;
    private RedisTemplate<K, V> redisTemplate;

    @Value("${shiro.globalSessionTimeout}")
    private Long globExpire;//设置30分钟

    @SuppressWarnings("rawtypes")
    public RedisCache(String name, RedisTemplate client) {
        this.cacheKey = REDIS_SHIRO_CACHE + name + ":";
        this.redisTemplate = client;
    }

    @Override
    public V get(K key) throws CacheException {
        V value = redisTemplate.boundValueOps(getCacheKey(key)).get();
        if(value!=null){
            redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire, TimeUnit.MILLISECONDS);//毫秒
        }
        return value;
    }

    @Override
    public V put(K key, V value) throws CacheException {
        V old = get(key);
        redisTemplate.boundValueOps(getCacheKey(key)).set(value);
        redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire, TimeUnit.MILLISECONDS);//毫秒
        return old;
    }

    @Override
    public V remove(K key) throws CacheException {
        V old = get(key);
        redisTemplate.delete(getCacheKey(key));
        return old;
    }

    @Override
    public void clear() throws CacheException {
        redisTemplate.delete(keys());
    }

    @Override
    public int size() {
        return keys().size();
    }

    @Override
    public Set<K> keys() {
        return redisTemplate.keys(getCacheKey("*"));
    }

    /**
     * 获取所有的value
     */
    @Override
    public Collection<V> values() {
        Set<K> keys = this.keys();

        List<V> list = new ArrayList<>();
        for (K key : keys) {
            list.add(get(key));
        }
        return list;
    }

    private K getCacheKey(Object k) {
        return (K) (this.cacheKey + k);
    }


}

RedisConstants.java

/**
 * 常量
 */
public class RedisConstants {
    /**shiro相关*/
    public static final String REDIS_SHIRO_CACHE = "dream-shiro-cache:";//shiro cache
    public static final String REDIS_SHIRO_SESSION = "dream-shiro-session:";//shiro session
}

RedisSessionDao.java

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * redis实现共享session
 */
@Component
public class RedisSessionDAO  extends AbstractSessionDAO {

    // session 在redis过期时间是30分钟30*60
    @Value("${shiro.globalSessionTimeout}")
    private Integer expireTime;//设置30分钟

    private static String prefix = RedisConstants.REDIS_SHIRO_SESSION;

    RedisSessionDAO() {
        super();
    }

    @Autowired
    private RedisTemplate redisTemplate;

    // 创建session,保存到数据库
    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        //Serializable sessionId = super.doCreate(session);
        try {
            String key = prefix + sessionId.toString();
            redisTemplate.opsForValue().set(key, session,expireTime,TimeUnit.MILLISECONDS);//毫秒
        } catch (Exception e) {
        }
        return sessionId;
    }


    /**
     * 更新session
     * @param session
     * @throws UnknownSessionException
     */
    @Override
    public void update(Session session) throws UnknownSessionException {
        try {
            //如果会话过期/停止 没必要再更新了
            //这里很重要,不然导致每次接口调用完毕后,由于会话结束,导致更新了空的session到redis了。导致源码老报错session不存在
            if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                return;
            }

            String key = prefix + session.getId().toString();
            redisTemplate.opsForValue().set(key, session,expireTime,TimeUnit.MILLISECONDS);
        } catch (Exception e) {
        }
    }

    /**
     * 删除session
     * @param session
     */
    @Override
    public void delete(Session session) {
        try {
            String key = prefix + session.getId().toString();
            redisTemplate.delete(key);
        } catch (Exception e) {
        }
    }

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

    // 获取session
    @Override
    protected Session doReadSession(Serializable sessionId) {
        Session session = null;
        try {
            String key = prefix + sessionId.toString();
            session = (Session) redisTemplate.opsForValue().get(key);
            if(session!=null){
                // session没过期,则刷新过期时间
                redisTemplate.boundValueOps(key).expire(expireTime, TimeUnit.MILLISECONDS);//毫秒
            }
        } catch (Exception e) {

        }
        return session;
    }

}

Shiro.java

import com.rjxy.realm.UserRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {


    @Bean
    public RedisSessionDAO getRedisSessionDao() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        return sessionDAO;
    }

    /**
     * redis缓存管理
     * @return
     */
    @Bean
    public RedisCacheManager redisCacheManager() {
        return new RedisCacheManager();
    }

    /**
     * sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID
     * @return
     */
    @Bean
    public SimpleCookie sessionIdCookie() {
        SimpleCookie simpleCookie = new SimpleCookie();
        //cookie的name,对应的默认是 JSESSIONID
        simpleCookie.setName("JSESSIONID");
        simpleCookie.setMaxAge(-1);//设置浏览器关闭才删除cookie
        simpleCookie.setPath("/");
        simpleCookie.setHttpOnly(true);//只支持http
        return simpleCookie;
    }

    /**
     * shiro的session管理
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix="shiro") //从配置文件注入globalSessionTimeout属性
    public SessionManager sessionManager() {
        //因为@Bean执行比@Value快,为了先注入@Value,只能把@Value作为函数的参数声明了
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(getRedisSessionDao());//自定义redis的sessionDao
        //sessionManager.setGlobalSessionTimeout(getSessionTimeout(1800)*1000);//设置全局session超时时间 ms
        //sessionManager.setCacheManager(redisCacheManager());
        sessionManager.setSessionIdCookieEnabled(true);//启用自定义的SessionIdCookie
        sessionManager.setSessionIdCookie(sessionIdCookie());//自定义SessionIdCookie
        sessionManager.setSessionIdUrlRewritingEnabled(false);//关闭URL中带上JSESSIONID
        sessionManager.setSessionValidationSchedulerEnabled(true);//定时检查失效的session
        sessionManager.setDeleteInvalidSessions(true);//启用删除无效sessioin
        return sessionManager;
    }

    /**
     * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
     * @return
     */
    @Bean
    public UserRealm myShiroRealm() {
        UserRealm myShiroRealm = new UserRealm();
        return myShiroRealm;
    }


    //2,创建SecurityManager
    @Bean(name="securityManager")
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());//自定义Realm
        securityManager.setSessionManager(sessionManager());//自定义session管理,使用redis
        securityManager.setCacheManager(redisCacheManager());//自定义缓存时间,使用redis
        return securityManager;
    }

    //3,创建ShiroFilter
    @Bean(name="shiroFilter")
    public ShiroFilterFactoryBean  shiroFilterFactoryBean(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //设置登录页面
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        shiroFilterFactoryBean.setSuccessUrl("/index.html");
        shiroFilterFactoryBean.setUnauthorizedUrl("unauthorized.html");
        //拦截的路径的详细设置
        //什么Map是存取有序的?
        Map<String,String> map = new LinkedHashMap<>();
        map.put("/sys/login","anon");//匿名访问
        map.put("/captcha.jpg","anon");//验证码放行
        map.put("/public/**","anon");
        map.put("/json/**","anon");
        map.put("/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return  shiroFilterFactoryBean;
    }

    //4,BeanLifeCycle  生命周期
    @Bean(name="lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor();
        return  lifecycleBeanPostProcessor;
    }

    /**
     * 使授权注解起作用
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
        aasa.setSecurityManager(securityManager());
        return new AuthorizationAttributeSourceAdvisor();
    }

    //5,开启shiro的注解   默认授权所用配置
    @Bean(name = "defaultAdvisorAutoProxyCreator")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);//cglib方式
        return  defaultAdvisorAutoProxyCreator;
    }

    @Bean(name="authorizationAttributeSourceAdvisor")
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

}

以及集群的配置

<dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

# Redis数据库索引(默认为0)
spring.redis.database=0
spring.redis.cluster.nodes=192.168.44.135:6381,192.168.44.135:6382,192.168.44.135:6383,192.168.44.135:6384,192.168.44.135:6385,192.168.44.135:6386
spring.redis.cluster.max-redirects=6
# Redis服务器地址
#spring.redis.host=192.168.44.135
# Redis服务器连接端口
#spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接超时时间(毫秒)
spring.redis.timeout=5000
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0

这只是在springboot项目分布式集群的时候,关于共享session的处理,博主只为做个笔记.