持續學習&持續更新中…
守破離
【雷豐陽-谷粒商城 】【分布式高級篇-微服務架構篇】【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架構師 | 微服務 | 大型電商項目.
本文完,感謝您的關注支持!