1.简介
单机部署web应用的时候session是唯一的,但是如果水平扩展后,通过nginx负载访问,就会出现session不一致的情况,
例如在A节点登录的用户,后续的操作请求访问到B节点的接口,但是B节点session中没有用户身份信息,就会导致重新跳转到登录页的情况。
解决session几种方式,还有其它的就不全列举出来了
- session复制:例如Tomcat的广播机制
- session持久化:把session信息放入sql数据库或者nosql数据库中,例如放入redis中
- 反向代理hash一致性:四层hash和七层hash都可以做,保证一个用户的请求落在一台web-server上,如nginx配置如下即可解决session不一致的问题.
#七层 hash
upstream tomcatServer{
ip_hash;
server 192.168.31.139:8080;
server 192.168.31.138:8080;
}
ip_hash保证每个访客固定访问一个后端服务器,nginx配置这块可以参考我的博客【Nginx从入门到应用】
此文使用SpringBoot+Spring-session+Redis实现Session的共享
2.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<!-- session统一由redis管理 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
3.application.yml配置
spring:
session:
#session存储方式
store-type: redis
redis:
host: 127.0.0.1
port: 6379
#redis的超时时间
timeout: 3000
#设置会话操作后立即更新到redis中,默认是等服务器请求结束后再将变化的值同步到redis中
flush-mode: immediate
pool:
# 连接池中的最大空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池中的最小空闲连接
max-wait: -1
4.编写session拦截器
- 1.查看session中的user是否为null
- 2.根据用户名去redis查询对应的sessionId信息
- 3.如果查询的为null或者取出来的sessionId和当前会话的sessionId不一致,则拦截,否则放行
@Component
public class RedisSessionInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if (session.getAttribute("user") != null) {
try {
User user = (User) session.getAttribute("user");
//验证当前请求的session是否是已登录的session
String loginSessionId = redisTemplate.opsForValue().get("loginUser:" + user.getUsername());
if (loginSessionId != null && loginSessionId.equals(session.getId())) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
}
response401(response);
return false;
}
private void response401(HttpServletResponse response) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try {
response.getWriter().print("用户未登录或登陆超时!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
5.启用RedisHttpSession和注册拦截器
对除了登录接口的所有以**/api**开头的接口进行拦截.
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 20)//session过期时间(秒)
@Configuration
public class RedisSessionConfig implements WebMvcConfigurer {
@Autowired
RedisSessionInterceptor redisSessionInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry)
{
//所有已api开头的访问都要进入RedisSessionInterceptor拦截器进行登录验证;
registry.addInterceptor(redisSessionInterceptor).addPathPatterns("/api/**").excludePathPatterns("/api/login/**");
}
}
6.测试
测试用的Model类
public class User implements Serializable {
private String username;
private String loginSessionId;
//省略 get,set 方法 ...
}
存放到session的model必须实现Serializable接口,不然会出现org.springframework.data.redis.serializer.SerializationException序列化异常,如下
测试用的接口
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/login/{username}")
public User login(HttpSession session, @PathVariable String username){
User user=new User();
user.setUsername(username);
user.setLoginSessionId(session.getId());
session.removeAttribute("user");
session.setAttribute("user",user);
redisTemplate.opsForValue().set("loginUser:"+username, session.getId());
return user;
}
@GetMapping("/index")
public User index(HttpSession session){
User user=(User) session.getAttribute("user");
return user;
}
}
测试流程
第一步.首先启动应用2次,端口例如8020,8021
idea中同一个项目启动2次操作如下
勾选Allow Paraller run,让idea支持同一个项目启动多次
启动完第1个端口的时候,把配置文件的端口修改成8021,然后再次启动即可。
第二步.打开浏览器访问: http://localhost:8020/api/login/test
第三步.再打开一个窗口访问: http://localhost:8021/api/index
可以看到上面2个图片返回的sessionId是一致的,表示着操作已经成功了,
第四步.测试session拦截器是否有效
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 20)
由于我上面配置的session超时时间是20秒,所以等20秒后再访问/api/index接口就可以看到请求已经被成功拦截了。
到此该博客的任务已经完成,是不是配置起来很简单呢,感兴趣的小伙伴可以自己试试。
7.项目配套代码
创作不易,要是觉得我写的对你有点帮助的话,麻烦在gitee上帮我点下 Star
【SpringBoot框架篇】其它文章如下,后续会继续更新。
- 1.搭建第一个springboot项目
- 2.Thymeleaf模板引擎实战
- 3.优化代码,让代码更简洁高效
- 4.集成jta-atomikos实现分布式事务
- 5.分布式锁的实现方式
- 6.docker部署,并挂载配置文件到宿主机上面
- 7.项目发布到生产环境
- 8.搭建自己的spring-boot-starter
- 9.dubbo入门实战
- 10.API接口限流实战
- 11.Spring Data Jpa实战
- 12.使用druid的monitor工具查看sql执行性能
- 13.使用springboot admin对springboot应用进行监控
- 14.mybatis-plus实战
- 15.使用shiro对web应用进行权限认证
- 16.security整合jwt实现对前后端分离的项目进行权限认证
- 17.使用swagger2生成RESTful风格的接口文档
- 18.使用Netty加websocket实现在线聊天功能
- 19.使用spring-session加redis来实现session共享
- 20.自定义@Configuration配置类启用开关
- 21.对springboot框架编译后的jar文件瘦身
- 22.集成RocketMQ实现消息发布和订阅
- 23.集成smart-doc插件零侵入自动生成RESTful格式API文档
- 24.集成FastDFS实现文件的分布式存储
- 25.集成Minio实现文件的私有化对象存储
- 26.集成spring-boot-starter-validation对接口参数校验
- 27.集成mail实现邮件推送带网页样式的消息
- 28.使用JdbcTemplate操作数据库
- 29.Jpa+vue实现单模型的低代码平台
- 30.使用sharding-jdbc实现读写分离和分库分表
- 31.基于分布式锁或xxx-job实现分布式任务调度
- 32.基于注解+redis实现表单防重复提交
- 33.优雅集成i18n实现国际化信息返回
- 34.使用Spring Retry完成任务的重试