第七章 实现分布式会话SessionManager
1、会话的问题
2、分布式会话实现思路
【1】原理分析
所有服务器的session信息都存储到了同一个Redis集群中,即所有的服务都将 Session 的信息存储到 Redis 集群中,无论是对 Session 的注销、更新都会同步到集群中,达到了 Session 共享的目的。
Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。
? 在实际工作中我们建议使用外部的缓存设备(包括Redis)来共享 Session,避免单个服务器节点挂掉而影响服务,共享数据都会放到外部缓存容器中
【2】设计类图详解
3、RedisSessionDao
RedisSessionDao继承AbstractSessionDAO,重写了会话的创建、读取、修改等操作,全部缓存与redis中
package com.itheima.shiro.core.impl;
import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.utils.ShiroRedissionSerialize;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Description 实现shiro session的memcached集中式管理~
*/
@Log4j2
public class RedisSessionDao extends AbstractSessionDAO {
@Resource(name = "redissonClientForShiro")
RedissonClient redissonClient;
private Long globalSessionTimeout;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
// log.info("=============创建sessionId:{}",sessionId);
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+sessionId.toString());
sessionIdRBucket.trySet(ShiroRedissionSerialize.serialize(session), globalSessionTimeout, TimeUnit.SECONDS);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+sessionId.toString());
Session session = (Session) ShiroRedissionSerialize.deserialize(sessionIdRBucket.get());
// log.info("=============读取sessionId:{}",session.getId().toString());
return session;
}
@Override
public void delete(Session session) {
// log.info("=============删除sessionId:{}",session.getId().toString());
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+session.getId().toString());
sessionIdRBucket.delete();
}
@Override
public Collection<Session> getActiveSessions() {
return Collections.emptySet();
}
@Override
public void update(Session session) {
RBucket<String> sessionIdRBucket = redissonClient.getBucket(CacheConstant.GROUP_CAS+session.getId().toString());
sessionIdRBucket.set(ShiroRedissionSerialize.serialize(session), globalSessionTimeout, TimeUnit.SECONDS);
// log.info("=============修改sessionId:{}",session.getId().toString());
}
public void setGlobalSessionTimeout(Long globalSessionTimeout) {
this.globalSessionTimeout = globalSessionTimeout;
}
}
4、重写ShiroConfig
package com.itheima.shiro.config;
import com.itheima.shiro.core.ShiroDbRealm;
import com.itheima.shiro.core.impl.RedisSessionDao;
import com.itheima.shiro.core.impl.ShiroCacheManager;
import com.itheima.shiro.core.impl.ShiroDbRealmImpl;
import com.itheima.shiro.filter.RolesOrAuthorizationFilter;
import com.itheima.shiro.properties.PropertiesUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.session.mgt.eis.SessionDAO;
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.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @Description 权限配置类
*/
@Configuration
@ComponentScan(basePackages = "com.itheima.shiro.core")
@EnableConfigurationProperties({ShiroRedisProperties.class})
@Log4j2
public class ShiroConfig {
@Autowired
private ShiroRedisProperties shiroRedisProperties;
/**
* @Description redission客户端
*/
@Bean("redissonClientForShiro")
public RedissonClient redissonClient() {
log.info("=====初始化redissonClientForShiro开始======");
String[] nodeList = shiroRedisProperties.getNodes().split(",");
Config config = new Config();
if (nodeList.length == 1) {
config.useSingleServer().setAddress(nodeList[0])
.setConnectTimeout(shiroRedisProperties.getConnectTimeout())
.setConnectionMinimumIdleSize(shiroRedisProperties.getConnectionMinimumidleSize())
.setConnectionPoolSize(shiroRedisProperties.getConnectPoolSize()).setTimeout(shiroRedisProperties.getTimeout());
} else {
config.useClusterServers().addNodeAddress(nodeList)
.setConnectTimeout(shiroRedisProperties.getConnectTimeout())
.setMasterConnectionMinimumIdleSize(shiroRedisProperties.getConnectionMinimumidleSize())
.setMasterConnectionPoolSize(shiroRedisProperties.getConnectPoolSize()).setTimeout(shiroRedisProperties.getTimeout());
}
RedissonClient redissonClient = Redisson.create(config);
log.info("=====初始化redissonClientForShiro完成======");
return redissonClient;
}
/**
* @Description 创建cookie对象
*/
@Bean(name="sessionIdCookie")
public SimpleCookie simpleCookie(){
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("ShiroSession");
return simpleCookie;
}
/**
* @Description 权限管理器
* @param
* @return
*/
@Bean(name="securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroDbRealm());
securityManager.setSessionManager(shiroSessionManager());
return securityManager;
}
/**
* @Description 自定义RealmImpl
*/
@Bean(name="shiroDbRealm")
public ShiroDbRealm shiroDbRealm(){
return new ShiroDbRealmImpl();
}
/**
* @Description 自定义session会话存储的实现类 ,使用Redis来存储共享session,达到分布式部署目的
*/
@Bean("redisSessionDao")
public SessionDAO redisSessionDao(){
RedisSessionDao sessionDAO = new RedisSessionDao();
sessionDAO.setGlobalSessionTimeout(shiroRedisProperties.getGlobalSessionTimeout());
return sessionDAO;
}
/**
* @Description 会话管理器
*/
@Bean(name="sessionManager")
public DefaultWebSessionManager shiroSessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDao());
sessionManager.setSessionValidationSchedulerEnabled(false);
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(simpleCookie());
sessionManager.setGlobalSessionTimeout(shiroRedisProperties.getGlobalSessionTimeout());
return sessionManager;
}
/**
* @Description 保证实现了Shiro内部lifecycle函数的bean执行
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* @Description AOP式方法级权限检查
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* @Description 配合DefaultAdvisorAutoProxyCreator事项注解权限校验
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
aasa.setSecurityManager(defaultWebSecurityManager());
return new AuthorizationAttributeSourceAdvisor();
}
/**
* @Description 过滤器链
*/
private Map<String, String> filterChainDefinition(){
List<Object> list = PropertiesUtil.propertiesShiro.getKeyList();
Map<String, String> map = new LinkedHashMap<>();
for (Object object : list) {
String key = object.toString();
String value = PropertiesUtil.getShiroValue(key);
log.info("读取防止盗链控制:---key{},---value:{}",key,value);
map.put(key, value);
}
return map;
}
/**
* @Description 自定义过滤器定义
*/
private Map<String, Filter> filters() {
Map<String, Filter> map = new HashMap<String, Filter>();
map.put("roleOr", new RolesOrAuthorizationFilter());
return map;
}
/**
* @Description Shiro过滤器
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(defaultWebSecurityManager());
//使自定义过滤器生效
shiroFilter.setFilters(filters());
shiroFilter.setFilterChainDefinitionMap(filterChainDefinition());
shiroFilter.setLoginUrl("/login");
shiroFilter.setUnauthorizedUrl("/login");
return shiroFilter;
}
}
5、测试
此时我们需要2个相同的项目shiro-day01-12shiro-redis-SessionManager-A和shiro-day01-13shiro-redis-SessionManager-B
分别启动A,B2个项目
访问http://127.0.0.1:8081/shiro/login,使用admin/pass登录
修改地址栏http://127.0.0.1:8081/shiro/menus/system为http://127.0.0.1:8082/shiro/menus/system,也可以正常访问
此时我们实现了在A服务登录,直接访问B服务则无需登录