1. 什么是session?
会话技术简介
http协议是无状态的,因此对于服务端来说,当它接收到客户端的http请求时,无法识别这个请求来源于哪个客户端。无状态的协议有优点也有缺点,但对于需要识别客户端甚至是需要记住客户端的业务来说,应当要让http协议"有状态"。
需要记住客户端的业务种类非常多。例如登陆系统,在一个页面登录后,新打开一个该网站页面,应当也保持登录状态。再例如购物车系统,某用户添加商品1后应当保证他还能继续添加商品2,在结算时能够读取购物车中的所有商品。
如何让服务端记住客户端?目前使用最多的是cookie和session两种会话技术。
- Cookie:数据存储在浏览器内存,减少服务器端的存储的压力,安全性不好,客户端可以清除cookie。位于http的请求头里面。
- Session:将数据存储到服务器端,安全性相对好,会增加服务器的压力。
cookie
Cookie技术是将用户的数据存储到客户端的技术,它的作用是为了让服务端根据每个客户端持有的cookie来区分不同客户端。
cookie由cookie name、具有唯一性的cookie value以及一些属性(path、expires、domain等)构成,其中value是区分客户端的唯一依据。
Cookie的原理为:服务端在接收到客户端首次发送的请求后,服务端在响应首部中加入"set-cookie"字段发送给客户端,其中包含了JSSIONID作为唯一身份识别ID;客户端在此后的请求中都将携带这一字段。服务端借此就可以识别客户端,并从cookie中找到该客户端的信息。
整个过程:
-
client—the 1st time—>sever,header 的cookie中无JSSIONID
-
server–response—>client, server发现请求头中无JSSIONID,好,我来给你产生一个随机值,返回给你,在response的header中,多了一个东西, Set-Cookie:JSESSIONID=xxx
-
client–reveive response from server,发现response header中有Set-Cookie,好,以后我再请求server时,把这些Cookie都带上,其中就包含了JSESSIONID
-
client–the 2nd time—>server, 带上了所有的cookie内容
session
但是cookie是存在本地浏览中的,始终存在安全问题。而且cookie保存的字段大小和类型都被限制了,因此产生了session。
Session的原理:服务端接收到某客户端首次发送的请求后,为此客户端生成一个session,并分配一段属于该session的缓冲区,同时将该session配对的标识号JSESSIONID作为cookie的name添加到响应首部中返回给客户端;客户端下次访问时,请求首部中将携带该JSESSIONID,服务端将根据该JSESSIONID寻找与之配对的session,如果能找到对应的session,进行相应的操作。
2. 为什么要分布式session?
如果是单机应用,那么谈不上session共享,session放哪都无所谓,不在乎放到默认的servlet容器中,还是抽出来放到单独的地方;
也就是说session共享是针对集群(或分布式、或分布式集群)的;如果不做session共享,仍然采用默认的方式(session存放到默认的servlet容器),当我们的应用是以集群的方式发布的时候,同个用户的请求会被分发到不同的集群节点(分发依赖具体的负载均衡规则),那么每个处理同个用户请求的节点都会重新生成该用户的session,这些session之间是毫无关联的。那么同个用户的请求会被当成多个不同用户的请求,这肯定是不行的。
3. 分布式session的几种实现方式
3.1Session复制
在支持Session复制的Web服务器上,通过修改Web服务器的配置,可以实现将Session同步到其它Web服务器上,达到每个Web服务器上都保存一致的Session。
优点:代码上不需要做支持和修改。
缺点:需要依赖支持的Web服务器,一旦更换成不支持的Web服务器就不能使用了,在数据量很大的情况下不仅占用网络资源,而且会导致延迟。
适用场景:只适用于Web服务器比较少且Session数据量少的情况。
可用方案:开源方案tomcat-redis-session-manager,暂不支持Tomcat8。
3.2 Session粘滞
同一个用户的所有请求都被分配到同一个服务器上。
优点:使用简单,没有额外开销。
缺点:一旦某个Web服务器重启或宕机,相对应的Session数据将会丢失,而且需要依赖负载均衡机制。
适用场景:对稳定性要求不是很高的业务情景。
3.3 Session集中管理
在单独的服务器或服务器集群上使用缓存技术,如Redis存储Session数据,集中管理所有的Session,所有的Web服务器都从这个存储介质中存取对应的Session,实现Session共享。
优点:可靠性高,减少Web服务器的资源开销。
缺点:实现上有些复杂,配置较多。
适用场景:Web服务器较多、要求高可用性的情况。
可用方案:开源方案Spring Session,也可以自己实现,主要是重写HttpServletRequestWrapper中的getSession方法,博主也动手写了一个,github搜索joincat用户,然后自取。
3.4 基于Cookie管理
这种方式每次发起请求的时候都需要将Session数据放到Cookie中传递给服务端。
优点:不需要依赖额外外部存储,不需要额外配置。
缺点:不安全,易被盗取或篡改;Cookie数量和长度有限制,需要消耗更多网络带宽。
适用场景:数据不重要、不敏感且数据量小的情况。
4. Shiro-redis实现分布式session
在这里我们使用shiro进行用户登录和授权,使用redis进行分布式session共享。下面的步骤都是在已经整合了shiro的基础上进行的。
第一步:导入相关依赖,并且进行配置
需要说明的是,我是用的是lettuce,而不是jedis。这个非常重要,影响你导入的依赖。因为jedis是需要自定义连接池的,lettuce不需要。
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.2</version>
</dependency>
<!--shiro-redis插件-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
<exclusions>
<exclusion>
<artifactId>shiro-core</artifactId>
<groupId>org.apache.shiro</groupId>
</exclusion>
</exclusions>
</dependency>
在配置文件中主要是redis的一些配置,还有关于session的一些过期时间由于在程序中也可以定义在此就不做要求了。可以看到我配置的是lettuce的相关信息而不是jedis.
#redis
redis:
host: ***
port: 6379
password: **
# 连接超时时间(ms)
timeout: 50000
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
database: 0
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
配置RedisConfig
/**
* @Version: 1.0
* @Desc:
*/
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
/**
* Spring Cache 自定义key生成器,缓存数据时key生成策略
* @return
*/
@Bean
@Override
// 自定义的缓存key的生成策略
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuffer sb = new StringBuffer();
sb.append(method.getName());
sb.append(':');
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
//自定义缓存配置
//duration 配置的是缓存过期时间,作为参数的原因是可以针对不同的缓存进行设置不同的缓存过期时间
private RedisCacheConfiguration redisCacheConfiguration(Duration duration) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
//获取默认配置
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(duration)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
.disableCachingNullValues();
return config;
}
//缓存管理器
//可以在这个里面配置多个缓存 比如user和goods
@Bean
public RedisCacheManager cacheManager(LettuceConnectionFactory lettuceConnectionFactory) {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
//获取user默认配置
RedisCacheConfiguration userRedisCacheConfiguration = redisCacheConfiguration(Duration.ofHours(1L));
//获取goods默认配置
RedisCacheConfiguration goodsRedisCacheConfiguration = redisCacheConfiguration(Duration.ofHours(2L));
redisCacheConfigurationMap.put("user", userRedisCacheConfiguration);
redisCacheConfigurationMap.put("goods", goodsRedisCacheConfiguration);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig();
RedisCacheManager redisCacheManager = RedisCacheManager.builder(lettuceConnectionFactory)
.cacheDefaults(defaultCacheConfig)
.withInitialCacheConfigurations(redisCacheConfigurationMap)
.build();
return redisCacheManager;
}
//设置RedisTemplate序列化
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
来看看我们在这个配置里面干了什么:
KeyGenerator
:就是自定义了一个key的生成器,很多人会自己单独写个类对key进行包装,在此不过是进行了统一设置。RedisCacheConfiguration
:自定义一个缓存的配置模板类,不同的对象可以通过参数设置不同的缓存策略。RedisCacheManager
:这个就是对redis中的缓存进行统一的管理。RedisTemplate
:目的就是自己写一个RedisTemplate覆盖掉默认的。因为默认的RedisTemplate会有序列化问题,导致redis中产生乱码
配置ShiroConfig
@Configuration
public class ShiroConfig {
private static Logger logger = Logger.getLogger(ShiroConfig.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//设置未认证(登录)时,访问需要认证的资源时跳转的页面
shiroFilterFactoryBean.setLoginUrl("/toLogin");
// //设置认证成功时跳转的页面
shiroFilterFactoryBean.setSuccessUrl("/index");
//指定路径和过滤器的对应关系
Map<String, String> filterMap = new LinkedHashMap<>();
// 定义filterChain,静态资源不拦截
// 配置不会被拦截的链接 顺序判断 相关静态资源
filterMap.put("/static/**", "anon");
filterMap.put("/css/**", "anon");
filterMap.put("/font/**", "anon");
filterMap.put("/images/**", "anon");
filterMap.put("/js/**", "anon");
// druid数据源监控页面不拦截
filterMap.put("/druid/**", "anon");
// 配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了
// filterMap.put("/admin/logout", "logout");
filterMap.put("/index", "authc");
filterMap.put("/goods/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
@Bean
//安全管理器配置
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm, RedisCacheManager redisCacheManager, DefaultWebSessionManager redisSessionManager) {
logger.info("--------------shiro已经加载----------------");
// 配置SecurityManager,
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 注入shiroRealm
defaultWebSecurityManager.setRealm(shiroRealm);
defaultWebSecurityManager.setSessionManager(redisSessionManager);
defaultWebSecurityManager.setCacheManager(redisCacheManager);
return defaultWebSecurityManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public RedisManager redisManager() {
logger.info("创建shiro redisManager,连接Redis..URL= " + host + ":" + port);
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);// 配置缓存过期时间
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public RedisCacheManager redisCacheManager() {
logger.info("创建RedisCacheManager...");
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
@Bean
@Lazy
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
//使用HashedCredentialsMatcher带加密的匹配器来替换原先明文密码匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//指定加密算法
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//指定加密次数
hashedCredentialsMatcher.setHashIterations(1024);
// 生成16进制, 与注册时的生成格式相同
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return shiroRealm;
}
}
我们来看看这个里面干啥了:
- 首先肯定是包含了shiro的基础三件套:
ShiroRealm
、DefaultWebSecurityManager
、ShiroFilterFactoryBean
就不再重复说了。唯一需要注意的就是在DefaultWebSecurityManager
里面注册了session和cache的管理器,这个是使用shiro必须要做的。他才是老大。 RedisManager
:这这个里面配置了关于redis的信息,保证能够连接到redisRedisCacheManager
:这个就是关于缓存的配置,由于不止redis一种,因此进行单独的设置RedisSessionDAO
:这个类就是底层的一些操作,插件帮我们封装好了,只需要简单配置一下就好了。DefaultWebSessionManager
:session管理器。需要注册到shiro的安全管理器中。
这样一来,当我们登录成功后,在redis中会出现:
shiro_redis_session:34ea08dc-9d34-451c-2241-d72e5f09cc6a
这个key前缀shiro_redis_session
是可以设置的。后面这一串其实就是sessionId。
这个登录信息的缓存是存在有效时间的可以自行设置。由于这个信息在redis中,因此当我们的请求发送到另外一个服务器上时,cookie中有sessionId,加上前缀之后就可以通过查询redis是否存在该信息来判断用户情况。
值得注意的是:当我们去get这个key的时候,出来的结果是乱码。原因是这个插件使用的是byte[]存值,没有对其进行序列化。在这个session中其实保存了类似于有效时间等信息。因此我们可以发现其实session的value适合使用hash类型进行存储。能够实现对单个的属性进行快速的更新,减少了性能消耗。