关于负载均衡session共享问题的解决
我用过的只有两种不同的方法:
- 第一种:可以在nginx中加入ip_hash
这样来解决session共享问题.如果这样的话,若你保存的seesion那台服务器挂了,会退出登录,影响客户体验.(不推荐使用)
第二种:可以加入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的处理,博主只为做个笔记.