一、介绍
1、什么是session? cookie?
session对象存储在服务器端内存中,cookie存储在客户端浏览器中。一般是客户端请求服务器,服务器端生成session对象,将session对象存储在本后端服务的内存中,并且在响应头中放入sessionId响应给客户端,命令客户端保存sessionid到cookie。当浏览器以后发请求时会自动将本地cookie中存储的seesionId通过请求头的方式传递给服务器,这样服务器和客户端就能保持会话信息。
2、什么是分布式session,为什么需要分布式session?
- 为了提高服务器端的负载能力,后台一般将服务器节点做集群,通过ngnix通过轮询的方式转发到目标服务器。例如:当浏览器首次访问A服务器做登录,生成session对象。然后第二次请求在访问后台接口,如果正好被ngnix转发到了A服务器,那么没问题可以获取到session对象。如果请求被转发到B服务器,由于之前生成的session对象保存在A服务器中了,B服务器根本没有那个session对象,就会认定为未登录等情况。
- session不可跨域,它有自己的作用范围,当我们有跨域的需求时也需要用分布式session。例如:做购物网站时,点击登录,跳转到auth.mail.com域名的认证微服务中进行登录,但是访问mail.com域名下其他服务时,拿不到session,被判定为未登录。
因此,所谓分布式session就是要session要能在不同服务和同服务的集群的共享
二、解决方案
1、session复制(不推荐)
用户登录后得到session后,服务把session也复制到别的机器上,显然这种处理很不好,这里只简单介绍一下这种方案。
2、hash一致性(不推荐)
根据用户计算hash值,路由到指定的机器上登录。每个用户每次都会被路由到同一个后台节点,但是远程调用还是不好解决。
3、springsession整合redis统一存储(推荐)
最终方案,整合springsession,把session放到redis中,后续代码都是基于这个实现的。
三、代码实现
1、先附上官方文档:
https://spring.io/projects/spring-session-data-redis
https://docs.spring.io/spring-session/docs/2.4.2/reference/html5/#modules
核心思想是通过SpringSession修改session的作用域。
2、依赖导入与环境配置
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在properties中添加配置信息。
spring.session.store-type=redis
server.servlet.session.timeout=30m
spring.redis.host=192.168.56.10
在启动类头上添加注解,用于开启springsession。
@EnableRedisHttpSession //创建了一个springSessionRepositoryFilter ,负责将原生HttpSession 替换为Spring Session的实现
3、扩大session作用域与设置session序列化
由于默认使用jdk进行序列化,通过导入RedisSerializer修改为json序列化。并且通过修改CookieSerializer扩大session的作用域至**.mall.com。
编写配置类如下:
@Configuration
public class MallSessionConfig {
@Bean // redis的json序列化
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
@Bean // cookie
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("MAILSESSIONID"); // cookie的键
serializer.setDomainName("mall.com"); // 扩大session作用域,也就是cookie的有效域
return serializer;
}
}
注意,这个配置需要放到每个需要共享session的微服务下(不管是读取还是写入),并不是只是放到写入session的微服务下。
至此,分布式session就保存好了。
4、补充:分布式session的取用例子
假设做个登录验证的Controller:
@GetMapping({"/login.html","/","/index","/index.html"}) // auth服务
public String loginPage(HttpSession session){
// 从会话从获取loginUser
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);// "loginUser";
System.out.println("attribute:"+attribute);
if(attribute == null){
return "login";
}
System.out.println("已登陆过,重定向到首页");
return "redirect:http://mall.com";
}
@PostMapping("/login") // auth服务
public String login(UserLoginVo userLoginVo,
RedirectAttributes redirectAttributes,
HttpSession session){
// 调用远程登录接口,验证账号密码是否正确
R r = memberFeignService.login(userLoginVo);
if(r.getCode() == 0){
// 登录成功
MemberRespVo respVo = r.getData("data", new TypeReference<MemberRespVo>() {});
// 放入session // key为loginUser
session.setAttribute(AuthServerConstant.LOGIN_USER, respVo);//loginUser
log.info("\n欢迎 [" + respVo.getUsername() + "] 登录");
// 登录成功重定向到首页
return "redirect:http://mall.com";
}else {
HashMap<String, String> error = new HashMap<>();
// 获取错误信息
error.put("msg", r.getData("msg",new TypeReference<String>(){}));
redirectAttributes.addFlashAttribute("errors", error);
return "redirect:http://auth.mall.com/login.html";
}
}