认识问题
这篇文章介绍的是服务在集群部署的情况下,用户的请求会被负载均衡到不同的服务器,但是用户的登入数据、权限数据、操作数据等session数据只会存在一个服务器的内存中。若用户下一次请求被转发到其他没有session数据的服务器上,那用户就还需要再重新登入,重新在网站上进行业务操作后创建操作数据。
用户体验极差
如果用户session数据只在一个服务器上,那用户访问另外服务器上服务时候,就会要求用户重新登入和业务操作。多次要求用户进行重新登入和业务操作,这样的体验是个灾难。
解决方案
将用户的session状态从服务中剥离出来,放到一个独立的session服务器上,这里选择redis来建session服务器。集群服务有关状态数据的操作都要和redis服务器交互。这种分布式session方案我们成为共享session服务器方案。
正如上面的架构图所示,所有的应用服务器关于session数据保存和获取操作都会与session服务器交互。用户每次请求的权限验证都与session服务器中的数据进行比对。如果用户已经登入了,那登入状态数据就会保存在session服务器,用户访问其他服务器时就不需要重新登入
本文的方案,笔者提供了一个完整的demo项目,项目地址看末尾获取方式。
SpringBoot项目引入redis依赖
<!-- 引入 redis 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> </dependency>
redis配置
springboot项目默认使用StringRedisTemplate,StringRedisTemplate和RedisTemplate不同,前者比较简单,只需要配置redis参数就行。
第一种
spring: redis: #redis数据库地址 host: localhost port: 6379 password: timeout: 1000 #redis数据库索引,默认0 database: 1
第二种
# REDIS (RedisProperties) # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=localhost # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=5000
使用StringRedisTemplate
直接在service实现类中注入StringRedisTemplate。
@Autowired private StringRedisTemplate redisTemplate;
到这里,我们项目就能正常使用redis服务了。
redis操作实现类RedisOperator
该类简单封装了String类型的redis命令:set、get, ttl等,业务逻辑可以直接调用。
/** * 使用redisTemplate的操作实现类 * @author YI * @date 2018-6-12 10:54:28 */ @Component public class RedisOperator { @Autowired private StringRedisTemplate redisTemplate; // Key(键),简单的key-value操作 /** * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。 * * @param key * @return */ public long ttl(String key) { return redisTemplate.getExpire(key); } ......//因类代码比较多,这里就不全部展示了,读者可以查阅项目demo
用户登入
用户登入接口逻辑:
- 获取username和password数据,与数据库保存的用户密码比对。
- 用户名和密码比对正确后,生成唯一的uuid作为用户的登入状态session数据。
- 通过redis操作类将session数据保存在redis中,key由username生成,并设置30分钟有效时间。
- 将用户username保存到cookie中。
@PostMapping(value = "loginCheck") @ResponseBody public RestResponseBo loginCheck(@RequestParam String username, @RequestParam String password, HttpServletRequest request, HttpServletResponse response) { if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ return RestResponseBo.fail("用户名或者密码不能为空!"); } if(!username.equals("admin") || !password.equals("admin")) { return RestResponseBo.fail("用户名或者密码不正确!"); } String token = StrUtil.uuid(); //存放唯一的 token 并设置过期时间 operator.set(JdkApiInterceptor.USER_REDIS_SESSION + ":" + username, token, REDIS_TIMEOUT); //设置用户 密码 token等信息 operator.set(username, username+":"+password+":"+token); //用户浏览器会存放两种cookie: userToken,userId。 CookieUtil.addCookie("userName", username); return RestResponseBo.ok();// }
拦截器JdkApiInterceptor校验用户登入状态
拦截逻辑:
- 先从请求cookie 中获取username。
- 再查询redis中该用户的登入session数据。
- 不为空则是已登入,为空则是未登入或登入状态过期。
- 未登入或登入状态过期则重定向到登入页面。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception { //获取用户cookies String userName = CookieUtil.getCookie("userName"); //放开登入接口 // String uri = request.getRequestURI(); // logger.info("请求uri:" + uri); // // if(uri.equals("loginCheck")) // return true; // logger.info(" ======= 拦截UserId:" + userName); //用户id和token都不为空 if (!StringUtils.isEmpty(userName)) { //根据userid生成唯一key从redis中查出唯一token String uniqueToken = redis.get(USER_REDIS_SESSION + ":" + userName); logger.info("拦截uniqueToken:" + uniqueToken); //如果唯一token为空 ,则拦截url重定向到登入页面 if (StringUtils.isEmpty(uniqueToken)) { response.sendRedirect("/login"); returnErrorResponse(response, "请登录..."); return false; } //用户id和token有一个为空,则重定向登入页面 } else { response.sendRedirect("/login"); returnErrorResponse(response,"请登录..."); return false; } return true; }
到此,就能实现不同服务器的session共享了。
注册
使用@Configuration修饰创建配置类WebSecurityConfig
@Configuration public class WebSecurityConfig implements WebMvcConfigurer { ...
创建拦截器bean对象,放入ioc容器。
@Bean public JdkApiInterceptor JdkApiInterceptor(){ return new JdkApiInterceptor(); }
配置
配置拦截器要拦截的url和不拦截的url
/** * 拦截器注册 设置拦截接口 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(JdkApiInterceptor()).addPathPatterns("/**")//设置拦截所有的路径 //排除的路径:静态资源路径。防止被JdkApiInterceptor拦截 .excludePathPatterns("/loginCheck","/login","/error", "/js/**", "/css/**", "/imag/**"); }
这里排除拦截的url中,除了静态资源路径和登入接口,为什么还有/error路径呢?具体原因请查看我这篇文章:WebMvcConfigurer的excludePathPatterns配置 "失效" 问题
基于redis的共享session方案能解决用户重复登入的问题,带来更好的用户体验,是分布式架构经典的解决方案,但同时也带了问题,redis服务器的高可用问题以及远处通信的性能问题。当然集群下session问题解决方案有很多种,这只是其中之一,日后我还会进一步以文章形式与大家探讨。
查看更多 “Java架构师方案” 系列文章 以及 SpringBoot2.0学习示例
如果大家觉得这篇文章对你学习架构有帮助的话,还请点赞,在看支持一下。github项目也记得点个星哦!
完整的demo项目,请关注公众号“前沿科技bot“并发送"redis共享session"获取。