持续学习&持续更新中…
守破离
【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【19】分布式下Session共享问题
- session原理
- 分布式下session共享问题
- Session共享问题解决—session复制
- Session共享问题解决—客户端存储
- Session共享问题解决—hash一致性
- Session共享问题解决—统一存储
- Session共享问题解决—不同服务,子域session共享
- 手动设置Cookie,手动拿取Cookie
- 整合SpringSession
- SpringSession核心原理
- 参考
session原理
问题:不能跨不同域名共享
分布式下session共享问题
Session共享问题解决—session复制
优点 :web-server(Tomcat)原生支持,只需要修改配置 文件
缺点 :
- session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力
- 任意一台web-server保存的数据都是所有web- server的session总和,受到内存限制无法水平扩展更多的web-server
- 大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。
Session共享问题解决—客户端存储
优点
- 服务器不需存储session,用户保存自己的 session 信息到 cookie 中。节省服务端资源
缺点
- 都是缺点,这只是一种思路。
- 具体如下:
- 每次http请求,携带用户在cookie中的完整信息, 浪费网络带宽
- session数据放在cookie中,cookie有长度限制 4 K,不能保存大量信息
- session数据放在cookie中,存在泄漏、篡改、 窃取等安全隐患
- 这种方式不会使用。
Session共享问题解决—hash一致性
优点:
- 只需要改nginx配置,不需要修改应用代码
- 负载均衡,只要hash属性的值分布是均匀的,多台 web-server的负载是均衡的
- 可以支持web-server水平扩展(session同步法是不行的,受内存限制)
缺点:
- session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录
- 如果web-server水平扩展,rehash 后session 重新分布, 也会有一部分用户路由不到正确的session
- 但是以上缺点问题也不是很大,因为session本来都是有有效期的。所以这两种反向代理的方式可以使用
Session共享问题解决—统一存储
优点:
- 没有安全隐患
- 可以水平扩展,数据库/缓存水平切分即可
- web-server重启或者扩容都不会有 session 丢失
不足:
- 增加了一次网络调用,并且需要修改应用代码;如将所有的getSession方法替换为从Redis查数据的方式。
- redis获取数据比内存慢很多
- 上面缺点可以用SpringSession完美解决
Session共享问题解决—不同服务,子域session共享
jsessionid这个cookie默认是当前系统域名的。当我们分拆服务,不同域名部署的时候,我们可以使用如下解决方案;
放大Cookie作用域
手动设置Cookie,手动拿取Cookie
gulimall-auth:OAuth2Controller
@GetMapping("/oauth2.0/weibo/success")
public String weibo(@RequestParam("code") String code, HttpSession session,
HttpServletResponse httpServletResponse) throws Exception {
Map<String, String> headers = new HashMap<>();
Map<String, String> bodys = new HashMap<>();
bodys.put("client_id", "3276999101");
bodys.put("client_secret", "452bbefff4680ac8554b97799a8c12cb");
bodys.put("grant_type", "authorization_code");
bodys.put("redirect_uri", "http://auth.gulimall.com/oauth2.0/weibo/success");
bodys.put("code", code);
//1、根据code换取accessToken;
HttpResponse response = HttpUtils.doPost("https://api.weibo.com", "/oauth2/access_token", headers, null, bodys);
if (response.getStatusLine().getStatusCode() == 200) {
//2、获取到了 socialUserAccessToken 进行处理
String json = EntityUtils.toString(response.getEntity());
SocialUserAccessToken socialUserAccessToken = JSON.parseObject(json, SocialUserAccessToken.class);
// String uid = socialUserAccessToken.getUid();
// 通过uid就知道当前是哪个社交用户
//1)、当前用户如果是第一次进网站,进行自动注册(为当前社交用户生成一个会员信息账号,以后这个社交账号就对应指定的会员账号)
R r = memberFeignService.socialLogin(socialUserAccessToken);
if (r.getCode() == BizCodeEnume.SUCCESS.getCode()) {
//登录或者注册这个社交用户
//2)、登录成功就跳回首页
/**
* 手动设置Cookie
*/
MemberRespVo loginUser = r.getData(new TypeReference<MemberRespVo>() {
});
stringRedisTemplate.opsForValue().set("loginUser", JSON.toJSONString(loginUser));
Cookie cookie = new Cookie("GULIMALL", "loginUser");
cookie.setDomain("gulimall.com");
cookie.setMaxAge(24 * 60 * 60);
cookie.setPath("/");
httpServletResponse.addCookie(cookie);
session.setAttribute("loginUser", loginUser);
return "redirect:http://gulimall.com";
}
}
return "redirect:http://auth.gulimall.com/login.html";
}
gulimall-product:IndexController
@GetMapping({"/", "/index.html"})
public String indexPage(Model model, HttpServletRequest httpServletRequest, HttpSession session) {
/**
* 手动获取Cookie
*/
Cookie[] cookies = httpServletRequest.getCookies();
if (null != cookies && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("GULIMALL")) {
String loginUserKey = cookie.getValue();
String json = stringRedisTemplate.opsForValue().get(loginUserKey);
MemberRespVo loginUser = JSON.parseObject(json, new TypeReference<MemberRespVo>(){});
session.setAttribute("loginUser", loginUser);
}
}
}
List<CategoryEntity> categorys = categoryService.listLevel1Categorys();
model.addAttribute("categorys", categorys);
return "index";
}
@Controller
public class LoginController {
@GetMapping("/login.html")
public String loginPage() {
if(stringRedisTemplate.opsForValue().get("loginUser") != null) return "redirect:http://gulimall.com";
return "login";
}
}
整合SpringSession
<!-- 1 整合SpringSession完成session共享问题 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
# 2 整合SpringSession
spring.session.store-type=redis
#server.servlet.session.timeout=60m
# 3 配置Redis的连接信息(之前配过)
#spring.redis.host=xxx
#spring.redis.port=xxx
#spring.redis.password=xxx
@EnableRedisHttpSession // 4 整合Redis作为session存储
// 5 使用SpringSession【跟以前使用session的写法一样】
//第一次使用session;命令浏览器保存卡号。JSESSIONID这个cookie;
//以后浏览器访问哪个网站就会带上这个网站的cookie;
//子域之间; gulimall.com auth.gulimall.com order.gulimall.com
//应该做到:发卡的时候(指定域名为父域名),那么,即使是子域系统发的卡,也能让父域直接使用。
// 1、默认发的令牌。session=xxxxxxx。作用域:当前域;(SpringSession默认没有解决子域session共享问题)
// 2、使用JSON的序列化方式来序列化对象数据到redis中
R r = memberFeignService.socialLogin(socialUserAccessToken);
if (r.getCode() == BizCodeEnume.SUCCESS.getCode()) {
//登录或者注册这个社交用户
//2)、登录成功就跳回首页
MemberRespVo loginUser = r.getData(new TypeReference<MemberRespVo>() {
});
session.setAttribute("loginUser", loginUser);
//6 配置序列化 + Cookie domain
// 解决子域session共享问题
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer(){
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
// cookieSerializer.setCookieMaxAge(); // 默认是浏览器的session级别,关闭浏览器就失效
return cookieSerializer;
}
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
<!-- 7 给其他服务也整合好SpringSession后,直接取session中的数据即可 -->
<a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser==null?'':session.loginUser.nickname}]]</a>
// 登录页面
@GetMapping("/login.html")
public String loginPage(HttpSession session) {
// if(stringRedisTemplate.opsForValue().get("loginUser") != null) return "redirect:http://gulimall.com";
Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
if(attribute != null) return "redirect:http://gulimall.com";
return "login";
}
SpringSession核心原理
/**
* SpringSession 核心原理 装饰者模式;
* @EnableRedisHttpSession导入RedisHttpSessionConfiguration配置
* 1、给容器中添加了一个组件
* SessionRepository = 》》》【RedisOperationsSessionRepository】==》redis操作session。session的增删改查封装类
* 2、SessionRepositoryFilter == 》Filter: session'存储过滤器;每个请求过来都必须经过filter
* 1、创建的时候,就自动从容器中获取到了SessionRepository;
* 2、原始的request,response都被包装。SessionRepositoryRequestWrapper,SessionRepositoryResponseWrapper
* 3、以后获取session。SessionRepositoryRequestWrapper.getSession();
* 4、wrappedRequest.getSession();===> SessionRepository 中获取到的。
*
自动延期;用户只要没有关闭浏览器,SpringSession会自动续期,当然,用户关闭了浏览器,redis中的数据也是有过期时间的。
*/
参考
雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.
本文完,感谢您的关注支持!