集成spring-session利用redis来实现session共享(21)
简介
单机部署web应用的时候session是唯一的,但是如果水平扩展后,通过nginx负载访问,就会出现session不一致的情况,
例如在A节点登录的用户,后续的操作请求访问到B节点的接口,但是B节点session中没有用户身份信息,就会导致重新跳转到登录页的情况。
此文使用SpringBoot+Spring-session+Redis实现Session的共享
pom.xml引入依赖
<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>
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
编写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(); } } }
启用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/**"); } }
测试
测试用的Model类
public class User implements Serializable { private String username; private String loginSessionId; }
存放到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; } }
测试流程
1.首先启动应用2次,端口例如8020,8021
2.打开浏览器访问: http://localhost:8020/api/login/test
3.再打开一个窗口访问: http://localhost:8021/api/index
可以看到上面2个图片返回的sessionId是一致的,表示着操作已经成功了
4.测试session拦截器是否有效
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 20)
由于我上面配置的session超时时间是20秒,所以等20秒后再访问/api/index接口就可以看到请求已经被成功拦截了。