持续学习&持续更新中…
守破离
【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【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 {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");
// cookieSerializer.setCookieMaxAge(); // 默认是浏览器的session级别,关闭浏览器就失效return cookieSerializer;}@Beanpublic 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架构师 | 微服务 | 大型电商项目.
本文完,感谢您的关注支持!