session共享

session共享问题的由来

session共享是因为架构演变而出现的问题。

项目由初期直接使用一台tomcat做为web服务器,这时是没有session问题。

项目到nginx做动静分离,还是一台tomcat做为web服务器,这时是没有session问题。

项目到nginx做负载均衡,后端是多台tomcat时,还使用tomcat的session,这时就会出现session问题。

session共享的解决方案

client -> nginx -> tomcat

方案1:nginx使用ip_hash负载均衡算法,这样同一个ip请求的是一直是一个tomcat,所以使用的session也是一个。

方案2:tomcat内置session同步

方案3:增加后端组件存储session。数据库:mysql/mongodb 、redis、memcached

方案4:存储在client, jwt

方案对比:

​ 在项目初期使用方案1,配置简单,但是因为是ip_hash,这样如果是一批ip都请求到一个tomcat,这时候就造成一个tomcat空闲,一个忙的情况。

​ 为了解决造成一个tomcat空闲,一个忙的情况,可以使用方案2,方案2使用内置同步机制,同步机制就会出现同步延迟。增加tomcat的压力。

​ 因为同步session会有同步延迟等问题,采用增加组件存储session。如果用户比较少可以使用mysql、mongodb进行存储。

​ 如果对用户数大,可以内存数据库redis或memcached进行存储。

​ 如果没有踢出用户和查看登录用户数的要求可以使用jwt进行客户端存储。

方案1实现

在nginx那一篇已经实现

方案2实现

Tomcat 官网 https://tomcat.apache.org/tomcat-8.5-doc/cluster-howto.html

tomcat自带的session同步支持3种存储类型

1.文件系统

2.数据库

3.内存

这里使用的内存。

安装jdk

下载安装tomcat

1
tar zxf apache-tomcat-8.5.78.tar.gz

配置环境变量

1
2
3
4
5
export JAVA_HOME=/usr/java/jdk1.8.0_333-aarch64
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export CATALINA_HOME=/usr/local/apache-tomcat-8.5.78
export PATH=${JAVA_HOME}/bin:$CATALINA_HOME/bin:$PATH
1
source /etc/profile

环境变量验证

1
catalina.sh version

启动tomcat

1
startup.sh

访问 http://192.168.158.137:8080/

/usr/local/apache-tomcat-8.5.78/bin/shutdown.sh

创建测试项目:

1
2
3
4
cd /usr/local/apache-tomcat-8.5.78/webapps
mkdir webapp1
cd webapp1
vi index.jsp

index.jsp内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ page import="java.util.*" %>
<html>
<head>
<title>session</title>
</head>
<body>
<table>
<tr>
<td>session id</td>
<td><%=session.getId() %></td>
<% session.setAttribute("", "abc"); %>
</tr>
<tr>
<td>create time</td>
<td><%= session.getCreationTime()%></td>
</tr>
</table>
ip: 192.168.128.137
</body>
</html>

配置tomcat的server.xml

1
2
cd /usr/local/apache-tomcat-8.5.78/conf
vi server.xml

在localhost的Host中添加

1
<Context docBase="webapp1" path="" reloadable="true"/>

启动服务,访问http://192.168.158.137:8080/

重复配置两台服务器,发现两个session是不一样的。

配置tomcat会话共享集群:

在tomcat的server.xml的Engine标签上添加jvmRoute属性

1
2
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat-1">
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat-2">

在Engine标签下:把cluster打开

1
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>

在新建的webapp1项目下添加

1
2
mkdir WEB-INF
vi web.xml

Web.xml内容:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<distributable/>
</web-app>

tomcat里使用的java.net.InetAddress.getLocalHost().getHostAddress()获取IP地址,需要修改hosts文件,把127.0.0.1改为你的IP

1
2
3
vi /etc/hosts
192.168.158.137 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

启动tomcat

配置nginx为轮询负载。

这时访问nginx的地址一直返回同一个sessionID。

注意

tomcat同步需要开放端口tcp 4000 udp 45564

1
2
3
firewall-cmd --add-port=4000/tcp --permanent
firewall-cmd --add-port=45564/udp --permanent
firewall-cmd --reload

其他第三方包实现

memcached-session-filter

memcached-session-manager

tomcat-redis-session-manager

方案3实现

创建java maven项目

加入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
</dependencies>

添加redis配置

1
2
3
spring.redis.host=192.168.158.137
spring.redis.port=6379
spring.redis.password=123456

添加controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class RedidSessionController {

@Value("${server.port}")
public String port;

@PostMapping("/save")
public String saveName(String name, HttpSession session) throws SocketException {
session.setAttribute("name", name);
return "hello," + IpUtil.getLocalIp4Address().get().toString().replaceAll("/","") + ":" + session.getAttribute("name");
}

@PostMapping("/get")
public String getName(HttpSession session) throws SocketException {
return "hello," + session.getAttribute("name") + ":" + IpUtil.getLocalIp4Address().get().toString().replaceAll("/","");
}
}

启动 两台spring boot 项目 配置nginx

先请求save接口http://192.168.158.135/save?name=test1

再一直访问http://192.168.158.135/get,这时ip会变但是一直可以取到name值

方案4实现

加入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>

添加jwt工具类

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

public class TokenUtli {

//Issuer
public static final String ISSUER = "auth0";
//Audience
public static final String AUDIENCE = "Client";
//密钥
public static final String KEY = "123456";
//算法
public static final Algorithm ALGORITHM = Algorithm.HMAC256(TokenUtli.KEY);
//Header
public static final Map<String, Object> HEADER_MAP = new HashMap<String, Object>() {
{
put("alg", "HS256");
put("typ", "JWT");
}
};

/**
* 生成 Token 字符串
*
* @param claimMap claim 数据
* @return Token 字符串
*/
public static String GenerateToken(Map<String, String> claimMap) {
Date nowDate = new Date();
//120 分钟过期
Date expireDate = TokenUtli.AddDate(nowDate, 2 * 60);

//Token 建造器
JWTCreator.Builder tokenBuilder = JWT.create();

for (Map.Entry<String, String> entry : claimMap.entrySet()) {
//Payload 部分,根据需求添加
tokenBuilder.withClaim(entry.getKey(), entry.getValue());
}

//token 字符串
String token = tokenBuilder.withHeader(TokenUtli.HEADER_MAP)//Header 部分
.withIssuer(TokenUtli.ISSUER)//issuer
.withAudience(TokenUtli.AUDIENCE)//audience
.withIssuedAt(nowDate)//生效时间
.withExpiresAt(expireDate)//过期时间
.sign(TokenUtli.ALGORITHM);//签名,算法加密

return token;
}

/**
* 时间加法
*
* @param date 当前时间
* @param minute 持续时间(分钟)
* @return 时间加法结果
*/
private static Date AddDate(Date date, Integer minute) {
if (null == date) {
date = new Date();
}
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, minute);

return calendar.getTime();
}

/**
* 验证 Token
*
* @param webToken 前端传递的 Token 字符串
* @return Token 字符串是否正确
* @throws Exception 异常信息
*/
public static boolean VerifyJWTToken(String webToken) throws Exception {
if (StringUtils.isEmpty(webToken)) {
return false;
}
String[] token = webToken.split(" ");
if (token.length <= 1) {
throw new Exception("token错误");
}
if (token[1].equals("")) {
throw new Exception("token错误");
}


//JWT验证器
JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();

//解码
DecodedJWT jwt = verifier.verify(token[1]);//如果 token 过期,此处就会抛出异常

//Audience
List<String> audienceList = jwt.getAudience();
String audience = audienceList.get(0);

//Payload
Map<String, Claim> claimMap = jwt.getClaims();
for (Map.Entry<String, Claim> entry : claimMap.entrySet()) {

}

//生效时间
Date issueTime = jwt.getIssuedAt();
//过期时间
Date expiresTime = jwt.getExpiresAt();

return true;
}

}

登录接口返回jwt

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/login")
public class LoginController {

@PostMapping(value = "/login")
public String Login() {
Map<String, String> claimMap = new HashMap<>();
claimMap.put("username", "abc");
return TokenUtli.GenerateToken(claimMap);
}

}

需要jwt的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/demo")
public class DemoController {

@GetMapping(value = "/get")
public List<String> get() {
List<String> list = new ArrayList<>();
list.add("test1");
list.add("test2");
return list;
}

}

jwt拦截器

1
2
3
4
5
6
7
8
9
10
11
12
public class JWTInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
//从请求头内获取token
String token = request.getHeader("authorization");

//验证令牌,如果令牌不正确会出现异常会被全局异常处理
return TokenUtli.VerifyJWTToken(token);
}
}

拦载器配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class InterceptorConfig implements WebMvcConfigurer
{
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(new JWTInterceptor()).addPathPatterns("/**")//全部路径
.excludePathPatterns("/login/login");//开放登录路径
}
}

测试:直接访问 http://localhost:8080/demo/get

登录 http://localhost:8080/login/login

添加header的authorization再访问http://localhost:8080/demo/get


session共享
http://hanqichuan.com/2024/03/22/java/session共享/
作者
韩启川
发布于
2024年3月22日
许可协议