登录状态的保存
# 登录状态的保存专题
## 为什么要保持登录状态?
> 因为很多请求都是受保护的请求, 这里受保护具体指的是必须要有登录权限才能做的一些操作, 如购物车, 订单, 评论等请求, 因此, 我们需要保持登录状态, 来判断是否有与之相应的权限
## 如何保持登录状态
> 最传统的方式是使用`session`会话域来保持登陆状态, 因为session的作用范围一般是浏览器启动到关闭
> 注意: <font color='red'>**session的销毁机制是延迟销毁, 默认30min, 30min内如果没有再次访问, 就会删除, 而浏览器范围内的原因是cookie默认的生命周期为内存, 即浏览器开启到结束, 且通过JsessionId这个cookie获取的session, 所以session的生命周期是浏览器**</font>
> 而且, 传统原生的session都是基于本地内存的, 我们可以在登陆成功后将用户信息保存在session, 用于维持登陆状态
## session理论
### 原理

> 概括: session和本地内存相似, key是名字叫jessionId的cookie, value是一个哈希表, 整理而言也是一个哈希表, 而value的key是会话域数据的key, value就是值, 访问会话域有如下步骤
1. 通过jessionid的cookie找到对应的会话域
2. 通过key找到对应的value
### 传统session的情况下, 为什么登陆了, 首页没有登录状态?
> 1 **`cookie`的角度:** 因为在默认情况下, `cookie`的作用范围是当前域名下的所有路径及其子域名的所有路径, 但是, auth.bitmall.com和www.bitmall.com的域名完全不同, cookie的作用范围不够大, 即, 在www.bitmall.com发送请求的时候不会携带cookie
> 2 **`session`角度:** 因为这里处于两个微服务, 传统session存储在独立的内存中, 两个微服务有两个独立的session内存, 因此, 第二个独立的session获取不到登陆状态
### session共享问题

> 1. 在同域名的情况下, 确实可以携带cookie, 但是因为原生session是本地内存的原因, 在分布式场景下, 即使有cookie, 也会因为内存间不同步而获取不到登陆状态
> 2. **(同一个父域名的前提下)** 不同域名的情况下, 主要是cookie的携带问题
### session共享问题的解决方案-1 <i>session复制</i>
> **前提: 相同域名的前提下!**

#### 优点
1. session复制是原生支持的, 不需要引入额外的框架, 可以让系统更加轻量级
#### 缺点
1. 占用大量的带宽, 因为session复制会时刻将session里面的数据同步到其他微服务的session, 这样就会产生网络传输, 一旦服务器多了, 且用户访问量大, 那么, 后面sesson同步就会一直数据传输, 占用带宽多, 最终导致整个系统的延迟变高, 吞吐量变低
2. 占用大量的内存, 如果有一百个微服务, 每一个实际有1G的用户数据, 那么需要100G去存储, 99G冗余存储, 非常浪费内存
3. 无法水平扩容, 因为session复制通俗而言即保存到一个session的同时复制到其他session, 都是基于当前数据的, 如果运行过程中水平扩容, 即加入一个微服务, 之前的数据就没了, 因此无法水平扩容
> **总结: 弊大于利**
### session共享问题的解决方案-2 *客户端存储*

#### 优点
1. 不会占用服务器资源, 没有任何的内存占用, 不可能存在内存浪费
#### 缺点
1. cookie保存数据的长度是有限的, 如果保存用户数据, 可能会造成溢出的情况, 导致用户信息保存的不完整
2. 以cookie的形式存储在浏览器, 响应回来的cookie通常非常的大, 即相应的数据很大, 会占用大量的带宽, 导致延迟变高, 吞吐量变低
3. 以cookie的形式存储到浏览器, 是一件极其危险的事情, 无论是传输过程还是存储过程, 都极大可能被窃取
> **总结: 安全性极差, 最不能采用的一种方法**
### session共享问题的解决方案-3 *ip_hash一致性*
> ip_hash是NGINX负载均衡的一种策略, 这种策略是通过计算用户ip的哈希值,然后匹配到某一个应用服务器上, 一个ip对应着一台服务器

#### 优点
1. ip_hash只需要改NGINX的配置文件, 不需要动源码
2. 支持服务的水平扩展
#### 缺点
1. 如果进行了水平扩展, 有可能同一个hash值分配到的服务器发生改变, 这样需要重新登陆, session问题就会出现
2. 如果某一台服务器宕机重启了, 这台服务器的用户需要重新登陆
> **总结: 总体而言利大于弊, 下面这些问题发生概率不大, 可以考虑采用**
### session共享问题的解决方案-4 *统一存储*

#### 优点
1. 数据非常的安全, 除非redis被攻破了
2. 支持水平扩展, 如果如何扩展, session都能保持一致
3. 微服务重启不会导致session丢失
### 缺点
1. 增加了一个中间件, 有网络传输的环节, 整体延迟变高
> **总结: 相较于前面的方案, 该方案最好, 缺点的延迟不足挂齿**
### 不同子域名session共享问题的解决

> ***一句话, 扩大cookie的作用范围***
>** 最终采取的方案: 扩大cookie作用范围, 并采用统一存储**
## 配置SpringSession
[官网配置指导](https://docs.spring.io/spring-session/reference/2.7/guides/boot-redis.html)
> 彩蛋:

这里说明了HttpSession被替换了
> 注意, 因为这里的版本和我们的版本不一样, 所以有些配置项不一定是一样的, 仔细斟酌
### 配置相关问题-1序列化异常

> 序列化异常的原因是存储到Redis默认需要将Java对象序列化后存储, 而对应的VO没有实现序列化接口
> 这也引发了问题, 我们不应该采取序列化的方式, 因为如果以序列化的形式存储, 扩展性极差, 其他语言写的微服务可能就读不到这部分会话信息, 而且可读性也差
### 优化
#### 优化方向
1. Redis序列化形式
2. cookie的作用范围
```java
@EnableRedisHttpSession
@Configuration
public class SessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericFastJsonRedisSerializer();
}
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("bitmall.com"); // 这里设置父域名, 扩大cookie的作用范围
cookieSerializer.setCookieName("bitmall-login-sessionId");
return cookieSerializer;
}
}
```
##### 问题-配置类配置好了后序列化错误
> **因为序列化的名字必须是`springSessionDefaultRedisSerializer`这个才行**, 否则组件无法注入
##### 问题-配置类配置好了后反序列化错误
> **因为其他微服务没有配置序列化的方式, 默认用Java的反序列化, 应该把对应的配置类拷贝到所需要使用的微服务中**
##### 问题-配置类配置好了为什么会话域有错误信息
> **因为RedirectAttributes放入了会话域**
##### 问题-配置类配置好了为什么获取不了数据
> **`cookieSerializer.setUseSecureCookie(true); // 标记为安全的cookie`的**这段代码, 浏览器无法正常携带cookie, 因此删除即可
##### 注意
> 必须严格按照上面的CV, 否则会错误
## 其他知识
### 什么是魔法值
> 魔法值可以理解为一些存在其那套逻辑的使用量, 就像是这个session的key, 因为这个key的定义, 以后都要用这个key名字, 所以这个key是魔法值
## SpringSession原理
[原理图](https://www.processon.com/embed/64e87a2037c9b75d18db1a38)
> **总结: 装饰者模式**