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
|
环境变量验证
启动tomcat
访问 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 {
public static final String ISSUER = "auth0"; public static final String AUDIENCE = "Client"; public static final String KEY = "123456"; public static final Algorithm ALGORITHM = Algorithm.HMAC256(TokenUtli.KEY); public static final Map<String, Object> HEADER_MAP = new HashMap<String, Object>() { { put("alg", "HS256"); put("typ", "JWT"); } };
public static String GenerateToken(Map<String, String> claimMap) { Date nowDate = new Date(); Date expireDate = TokenUtli.AddDate(nowDate, 2 * 60);
JWTCreator.Builder tokenBuilder = JWT.create();
for (Map.Entry<String, String> entry : claimMap.entrySet()) { tokenBuilder.withClaim(entry.getKey(), entry.getValue()); }
String token = tokenBuilder.withHeader(TokenUtli.HEADER_MAP) .withIssuer(TokenUtli.ISSUER) .withAudience(TokenUtli.AUDIENCE) .withIssuedAt(nowDate) .withExpiresAt(expireDate) .sign(TokenUtli.ALGORITHM);
return token; }
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(); }
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错误"); }
JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();
DecodedJWT jwt = verifier.verify(token[1]);
List<String> audienceList = jwt.getAudience(); String audience = audienceList.get(0);
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 { 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