单机部署web应用的时候session是唯一的,但是如果水平扩展后,通过nginx负载访问,就会出现session不一致的情况,
例如在A节点登录的用户,后续的操作请求访问到B节点的接口,但是B节点session中没有用户身份信息,就会导致重新跳转到登录页的情况。
解决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的共享
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
2.3.0.RELEASE
org.springframework.session
spring-session-data-redis
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
@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();
}
}
}
对除了登录接口的所有以**/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/**");
}
}
测试用的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接口就可以看到请求已经被成功拦截了。
到此该博客的任务已经完成,是不是配置起来很简单呢,感兴趣的小伙伴可以自己试试。
github地址
创作不易,要是觉得我写的对你有点帮助的话,麻烦在github上帮我点下 Star
【SpringBoot框架篇】其它文章如下,后续会继续更新。