分布式会话
1. 分布式会话
1.1 什么是会话?
会话 session 代表的是客户端与服务器的一次交互过程,这个过程可以是连续的也可以是时断时续的.曾经的 servlet 时代(jsp),一旦用户与服务端交互,tomcat 服务器就会为用户创建一个 session,同时前端会有一个 jsessionid,每次交互都会携带。如此一来, 服务器只要在接收到用户请求的时候,就可以拿到 jsessionid,并根据这个 id 在内存中找到对应的会话 session,当拿到 session 会话后,那么我们就可以操作会话了。会话存活期间,我们就能认为用户一直处于正在使用着网站的状态,一旦 session 超期过时,那么就可以认为用户已经离开网站,停止交互了。用户的身份信息,我们也是通过 session 来判断的,在 session 中可以保存不同用户的信息。
session 的使用方式如下:
1 | |
1.2 无状态会话
HTTP 请求是无状态的,用户向服务端发起多次请求,服务端并不会知道这么多次请求都来自同一个用户,这个就是无状态的。
cookie 的出现就是为了有状态的记录用户。
常见的,ios 与服务端交互,安卓与服务端交互,前后端分离,小程序端与服务端交互,它们都是通过发起 http 请求来调用接口数据的,每次交互服务端都不会拿到客户端的状态,但我们可以通过手段去处理,比如用户每次发起请求的时候携带一个 userid 或 user-token,如此一来,就能让服务端根据 userid 或 token 来获得相应的数据。每个用户的下一次请求都能被服务器识别为同一个用户。
1.3 有状态会话
Tomcat 中的会话,就是有状态的,一旦用户和服务端交互,就有会话,会话保存了用户的信息,这样用户就有“状态”了,服务端会和每个客户端都保持着这样的一层关系,这个由容器来管理(也就是 tomcat),这个 session 会话是保存到内存空间里的,如此依赖,当不同的用户访问服务端,那么就能通过会话知道谁是谁了。如果用户不再和服务端交互,那么会话则消失,结束了它的生命周期。如此一来,每个用户其实都会有一个会话被维护,这就是有状态会话。
场景:在传统项目或者 jsp 项目中使用的最多的 session 都是有状态的,session 的存在就是为了弥补 http 的无状态。
提示:tomcat 会话可以通过手段实现多系统之间的状态同步,但是会损耗一定的时间,一旦发生同步那么用户请求就会等待,这种做法不可取。
1.4 为何使用无状态会话
- 有状态的会话都是放在服务器的内存中的,一旦用户会话量多,那么内存就会出现瓶颈。而无状态会话可以采用介质,前端可以使用 cookie(app 可以使用缓存)保存用户 id 或 token,后端比如 redis,相应的用户会话都能放入 redis 中进行管理,如此,对应用部署的服务器就不会造成内存压力。用户在前端发起 http 请求,携带 id 或 token,这样服务器就能根据前端提供的 id 或 token 来识别用户了,可伸缩性就更强了。
1.5 单 tomcat 会话
先来看下单 tomcat 会话,这个是有状态的,用户首次访问服务端,这时候会话产生,并且会设置 jsessionid 放入 cookie 中,后续每次请求都会携带 jsessionid 以保持用户状态

1.6 动静分离会话
动静分离会话
用户请求服务端,由于动静分离,前端发起 http 请求,不会携带任何状态,当用户第一次请求以后,我们手动设置一个 token,作为用户会话,放入 redis 中,如此作为 redis-session,并且这个 token 设置后放入前端 cookie 中(app 或 小程序可以放入本地缓存),如此后续交互中,前端只需传递 token 给后端,后端就能识别这个用户来自谁了

1.7 集群分布式系统会话
集群分布式系统会话
集群或分布式系统本质都是多个系统,假设这里有两个服务器节点,分别是AB系统,它们可以是集群,也可以是分布式系统,一开始用户和 A 系统交互,那么这个时候的用户状态,我们可以保存到 redis 中,作为 A 系统的会话信息,随后用户的请求进入到了 B 系统,那么 B 系统中的会话也同样和 redis 关联,如此 AB 系统的 session 就统一了。当然 cookie 是会随着用户的访问携带过来的。那么这个其实就是分布式会话,通过 redis 来保存用户的状态。

2. SpringSession 实现用户会话
引入依赖
1
2
3
4
5
6
7
8
9<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>新增配置信息
1
2
3spring:
session:
store-type: redis启动类开启 HttpSession
1
2
3
4
5
6
7
8@EnableRedisHttpSession // 开启使用 redis 作为 SpringSession
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}测试
1
2
3
4
5
6
7
8
9@GetMapping("/setSession")
public Object setSession(HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute("userInfo", "new user");
session.setMaxInactiveInterval(3600);
session.getAttribute("userInfo");
// session.removeAttribute("userInfo");
return "ok";
}
3.分布式会话拦截器
基于分布式会话的权限拦截
编写拦截器代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class UserTokenInterceptor implements HandlerInterceptor {
public static final String REDIS_USER_TOKEN = "redis_user_token";
@Autowired
private RedisOperator redisOperator;
/**
* controller调用之前
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("进入到拦截器,被拦截... ...");
String headerUserId = request.getHeader("headerUserId");
String headerUserToken = request.getHeader("headerUserToken");
if (StringUtils.isNotBlank(headerUserId) && StringUtils.isNotBlank(headerUserToken)) {
String uniqueUserToken = this.redisOperator.get(REDIS_USER_TOKEN + ":" + headerUserId);
if (StringUtils.isBlank(uniqueUserToken)) {
returnErrorResponse(response,JSONResult.errorMsg("请登录... ...."));
return false;
} else {
if (!Objects.equals(headerUserToken, uniqueUserToken)) {
returnErrorResponse(response,JSONResult.errorMsg("账号在异地登录,请重新登录... ...."));
return false;
}
}
} else {
returnErrorResponse(response,JSONResult.errorMsg("请登录... ...."));
return false;
}
return true;
}
public void returnErrorResponse(HttpServletResponse response, JSONResult result) {
OutputStream out = null;
response.setCharacterEncoding("utf-8");
response.setContentType("text/json");
try {
out = response.getOutputStream();
out.write(JsonUtils.objectToJson(result).getBytes(StandardCharsets.UTF_8));
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* controller 之后,渲染视图之前
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* controller 之后,渲染视图之后
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}添加拦截器并指定拦截路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// 实现静态资源的映射
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/META-INF/resources/") // 映射swagger2
.addResourceLocations("file:/workspaces/images/"); // 映射本地静态资源
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
@Bean
public UserTokenInterceptor userTokenInterceptor() {
return new UserTokenInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userTokenInterceptor())
.addPathPatterns("/hello");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
4. CAS 单点登录
4.1 相同顶级域名的单点登录 SSO
引子
单点登录又称之为 Single Sign On,简称 SSO,单点登录可以通过基于用户会话的共享,分为两种:
第一种:基于分布式会话实现
比如现在有个一级域名为
www.fengjian.tech,是教育类网站,这个网站还有其它产品线,可以通过构建二级域名提供服务给用户访问,比如math.fengjian.tech、english.fengjian.tech等等,分别为数学、英语等,用户只需要在其中一个站点登录,那么其它站点也会随之登录。
Cookie + Redis 实现 SSO
基于 redis 的分布式会话可以流窜在后端的各个系统,都可以获取到 redis 中的用户信息。前端通过使用 cookie (可以保证在同域名下的一级、二级获取)保存用户的 userid 和 token,访问后端时进行携带,用户在任意系统登录后,cookie 和 redis 中都会有用户的信息,只要用户不退出,那么就可以随意登录任意站点了。
- 顶级域名
www.fengjian.tech和*.fengjian.tech的 cookie 是可以共享的,都可以携带到后端 - 二级域名自己独立的 cookie 是不能共享的,
math.fengjian.tech和english.fengjian.tech的 cookie 无法实现共享,两者互不影响
- 顶级域名
Cookie 共享测试
打开前端项目:设置域名,必须和 SwitchHosts 中设置的一致
1
cookieDomain: ".fengjian.tech"1
2127.0.0.1 math.fengjian.tech
127.0.0.1 english.fengjian.tech
4.2 不同顶级域名的单点登录SSO

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!