上文简单介绍了Cookie和Session,以及为什么需要分布式Session,本文介绍具体实现。
我们打算用docker-compose.yml实现以上服务端结构。为了实现分布式session,我们先需要完成以下工作:
- SpringBoot通过redis保存session&登录功能
- Dockerfile构建单个tomcat(SpringBoot)服务
- 编写docker-compose.yml实现分布式session功能:编排nginx、tomcat和redis
1. SpringBoot通过redis保存session&登录功能
登录流程如下:
- 先创建SpringBoot空应用,其中添加一下要用到的功能:SpringWeb(页面登录)、Redis(存储session)、SpringSession(由Spring实现)
功能实现是简单的,要点如下:
- 登录页面和登录后的页面
- 登录用的controller接口
- 鉴权用的Filter
- 集成redis和SpringSession
实现后文件结构如下:
1 | ├── pom.xml |
此处需要注意resource
文件夹下两个文件夹static
和templates
的区别:
功能不同:static文件夹存放静态资源,例如html、css、js文件等;templates文件夹存放thymeleaf的动态模板文件,一般也是html(如果使用,必须在POM文件中引入thymeleaf的dependency)
使用方式不同:如果想用controller的mapping方法返回视图,static文件夹下需要方法返回文件全名,例如想要返回static文件夹下
order.html
,controller方法需要这么实现:1
2
3
4
5"/order") (
public String getOrderPage() {
// 这里必须写全名
return "order.html";
}相同的文件名,如想要返回templates文件夹下
order.html
,首先需要在pom中引入spring-boot-starter-thymeleaf1
2
3
4
5<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>${thymeleaf.version}</version>
</dependency>然后实现controller的mapping方法:
1
2
3
4
5"/order") (
public String getOrderPage() {
// 这里只写文件名称,不写后缀
return "order";
}这里能如此实现是因为spring-boot-autoconfigure设定好了2个配置项
spring.thymeleaf.prefix
和spring.thymeleaf.suffix
。具体实现在ThymeleafProperties类下:接下来看下具体代码实现:
AuthFilter.java
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
65package my.seckill;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
/**
* 登录权限校验Filter
**/
4j
public class AuthFilter implements Filter {
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
Cookie[] cookies = request.getCookies();
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
log.info(LocalDateTime.now() + " 收到请求:" + requestMethod + " " + requestURI);
// 静态资源不过滤
if (requestURI.endsWith(".css") || requestURI.endsWith(".js") || requestURI.endsWith(".ico")) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 如果cookie为空,证明是第一次请求,那么肯定没登录;
// 如果登录成功后,会写入一个名叫login_user的cookie和名叫user_session_id的session
boolean isUserLoggedIn = cookies != null && Arrays.stream(cookies).anyMatch(x -> x.getName().equals("login_user"))
&& request.getSession().getAttribute("user_session_id") != null;
if ("/exit".equals(requestURI)) {
// 包含两种情况:已经登录 / 其它页面退出登录或者登录超时
// 如果已经登录而且要退出登录的话,调用对应业务逻辑
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 如果未登录,而且页面不是登录页面就转到登录页面
if (!isUserLoggedIn) {
// 此处会有2种方法 GET & POST
if ("/login".equals(requestURI)) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
// forward和redirect的区别:
// servletRequest.getRequestDispatcher("/login").forward(servletRequest, servletResponse);
((HttpServletResponse) servletResponse).sendRedirect("/login");
}
} else {
if ("/user".equals(requestURI)) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
((HttpServletResponse) servletResponse).sendRedirect("/user");
}
}
}
}关于Cookie的几种操作:
登录时添加Cookie,传送到前端:
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"/login") (
public String doLogin(@RequestBody LoginInfo loginInfo, HttpServletRequest request, HttpServletResponse response) {
if (loginInfo == null) {
return "No login info found!";
}
boolean loginPassed = userService.checkUserAndPassword(loginInfo.getUser(), loginInfo.getPassword());
if (!loginPassed) {
return "Login failed!";
}
log.info(loginInfo.toString());
HttpSession session = request.getSession();
session.setAttribute("user_session_id", MD5Encoder.encode(loginInfo.user.getBytes()));
Cookie cookie = new Cookie("login_user", loginInfo.user);
cookie.setPath("/");
cookie.setDomain("localhost");
// 如果设置55min
if (loginInfo.rememberMe) {
cookie.setMaxAge(300);
} else {
// 浏览器退出即删除
cookie.setMaxAge(-1);
}
response.addCookie(cookie);
return "Success";
}退出登录时删除Cookie
1
2
3
4
5
6
7
8
9
10
11
12
13
14"exit") (
public String exitLogin(HttpServletRequest request, HttpServletResponse response) {
Cookie loginUserCookie = Arrays.stream(request.getCookies())
.filter(x -> x.getName().equals("login_user")).findFirst().orElse(null);
if (loginUserCookie == null) {
return "Not logged in!";
}
// Cookie没有方法删除,所以通过这种方式来删除cookie
loginUserCookie.setValue(null);
// 设置maxAge为0则直接删除cookie
loginUserCookie.setMaxAge(0);
response.addCookie(loginUserCookie);
return "Exit success";
}检查Cookie是否存在,如果存在进入到user页面
1
2
3
4
5
6
7
8
9
10
11
12"/user") (
public ModelAndView userDetail(HttpServletRequest request) {
Optional<Cookie> loginUser = Arrays.stream(request.getCookies())
.filter(x -> x.getName().equals("login_user")).findFirst();
if (loginUser.isEmpty()) {
return new ModelAndView("login");
}
String value = loginUser.get().getValue();
ModelAndView modelAndView = new ModelAndView("user");
modelAndView.addObject("name", value);
return modelAndView;
}以上是基本的登录功能,完整代码参见此处。
通过redis储存session非常简单,分以下几步:
- 本地需要先启动redis服务
- 在项目pom.xml中引入session-redis和data-redis的dependency;
- 再在application.properties中加入Redis的配置,
- 启动应用即可把session存入到Redis中
1
2
3
4
5
6
7
8<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>1
2
3
4
5
6localhost =
6379 =
0 =
# 如果Redis没密码就不用设置
# spring.redis.password=abc
true =
2. Dockerfile构建单个tomcat(SpringBoot)服务
1 | FROM openjdk:11 |
构建运行前脚本
1 |
|
3. 编写docker-compose.yml实现分布式session功能
docker-compose.yml核心概念:
- service 服务:一个个应用实例,例如mysql容器,nginx容器,订单微服务等
- project 工程:由一组关联的服务组成的一个完整业务单元,在docker-compose.yml中定义。
使用docker-compose的步骤:
- 编写Dockerfile构建单个服务的镜像
- 使用docker-compose.yml定义一个完整业务单元,安排好整体应用中的各个容器服务
- 执行
docker-compose up
命令启动整个工程,完成部署上线
服务列表:
- springboot * 3
- nginx
- redis
docker-compose.yml如下:
1 | version: '2' |
其中nginx配置如下:
1 | # https://nginx.org/en/docs/beginners_guide.html |
完整代码参见此处。
4. 思考:Cookie和Session还有必要吗?
待续。
5. 其它
下载所有本地jar的documentation:
1 | mvn dependency:resolve -Dclassifier=sources |