【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【19】认证服务03—分布式下Session共享问题

48 篇文章 3 订阅
31 篇文章 0 订阅

持续学习&持续更新中…

守破离


session原理

在这里插入图片描述

在这里插入图片描述

分布式下session共享问题

问题1、不能跨不同域名共享

在这里插入图片描述

问题2、就算是相同域名,一个进程就对应一个session

在这里插入图片描述

相同域名下Session共享问题解决

session复制

在这里插入图片描述

优点 :web-server(Tomcat)原生支持,只需要修改配置 文件

缺点 :

  • session同步需要数据传输,占用大量网络带宽,降低了服务器群的业务处理能力
  • 任意一台web-server保存的数据都是所有web- server的session总和,受到内存限制无法水平扩展更多的web-server
  • 大型分布式集群情况下,由于所有web-server都全量保存数据,所以此方案不可取。

客户端存储

在这里插入图片描述

优点

  • 服务器不需存储session,用户保存自己的 session 信息到 cookie 中。节省服务端资源

缺点

  • 都是缺点,这只是一种思路。
  • 具体如下:
  • 每次http请求,携带用户在cookie中的完整信息, 浪费网络带宽
  • session数据放在cookie中,cookie有长度限制 4 K,不能保存大量信息
  • session数据放在cookie中,存在泄漏、篡改、 窃取等安全隐患
  • 这种方式不会使用。

hash一致性

在这里插入图片描述

优点:

  • 只需要改nginx配置,不需要修改应用代码
  • 负载均衡,只要hash属性的值分布是均匀的,多台 web-server的负载是均衡的
  • 可以支持web-server水平扩展(session同步法是不行的,受内存限制)

缺点:

  • session还是存在web-server中的,所以web-server重启可能导致部分session丢失,影响业务,如部分用户需要重新登录
  • 如果web-server水平扩展,rehash 后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
                 */
//				注意:
//                Redis中不应该以loginUser作为用户的key,而是应该以UUID作为key来存储用户信息。
//                而且,我们在这儿实现是将用户转为JSON字符串存起来,为了后续方便取出用户这个对象信息,应该直接以对象的方式将用户保存起来,这样从Redis中取出来的数据对象直接就能使用
                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";
    }
}

手动设置session,手动拿取session

一个用户,一个浏览器,对应一次会话,也就是一个session

Servlet的Session原理是,在内存中创建一个session对象,这个session对象有一个id,我们让浏览器保存一个叫jsessionid=该session对象id的一个cookie,以后浏览器访问服务器都会带上这个cookie,那么自然也就可以获取到这个session对象,该session对象的attribute属性当然也就可以拿到。

那么如果我们要将session保存在redis中,同理,给redis中创建一个session对象,然后将这个session对象的key作为cooike的值让浏览器保存,浏览器以后访问我们的服务器,带上这个cookie,就可以拿到redis中的session对象,自然也可以拿到该session对象的attribute

在Java中,获取session对象的时候是看有没有一个叫jsessionid的cookie,如果没有创建一个,如果有,拿出来让你用

那么,在redis中同理,看你有没有一个叫rsessionid的cookie,如果没有,创建一个,如果有,拿出来让你用

代码实现:

auth服务:

@Component
public class GulimallAuthSessionInterceptor implements HandlerInterceptor {

    public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        判断浏览器有没有带来rsessionid这个Cookie
//        rsessionid起任何名都行,我在这儿模仿jsessionid
        boolean flag = false;
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AuthServerConstant.REDIS_SESSION_ID_KEY)) {
//                    如果浏览器带来了rsessionid这个Cookie,代表Redis已经存着这个session对象了
                    THREAD_LOCAL.set(cookie.getValue());
                    flag = true;
                    break;
                }
            }
        }

        if (!flag) { // 如果没有带来rsessionid这个Cookie,给Redis中创建一个
            String s = UUID.randomUUID().toString();
            stringRedisTemplate.opsForValue().set(s, "");
            THREAD_LOCAL.set(s);
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        Cookie cookie = new Cookie(AuthServerConstant.REDIS_SESSION_ID_KEY, THREAD_LOCAL.get());
        cookie.setDomain("gulimall.com");
        response.addCookie(cookie); // 让浏览器保存这个rsessionid这个Cookie
    }

}

    @PostMapping("/login")
    public String login(@Valid UserLoginVo vo, BindingResult result, RedirectAttributes redirectAttributes, HttpSession session) {
        if (result.hasErrors()) { //数据校验不通过
            Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
            redirectAttributes.addFlashAttribute("errors", errors);
            return "redirect:http://auth.gulimall.com/reg.html";
        }
        R login = memberFeignService.login(vo);
        if (login.getCode() == BizCodeEnume.SUCCESS.getCode()) {
            MemberRespVo loginUser = login.getData(new TypeReference<MemberRespVo>() {
            });

            /**
             * 手动设置Session
             */
            String rsessionId = GulimallAuthSessionInterceptor.THREAD_LOCAL.get();
            String rsessionJson = stringRedisTemplate.opsForValue().get(rsessionId);
            HashMap<String, String> rsession;
            if (StringUtils.isEmpty(rsessionJson)) {
                rsession = new HashMap<>();
            } else {
                rsession = JSON.parseObject(rsessionJson, HashMap.class);
            }
            rsession.put(AuthServerConstant.LOGIN_USER, JSON.toJSONString(loginUser));
            stringRedisTemplate.opsForValue().set(rsessionId, JSON.toJSONString(rsession));


            session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);
            return "redirect:http://gulimall.com";
        }
        return "redirect:http://auth.gulimall.com/login.html";
    }

其他服务:

    @GetMapping({"/", "/index.html"})
    public String indexPage(Model model, HttpServletRequest httpServletRequest, HttpSession session) {



        Cookie[] cookies = httpServletRequest.getCookies();
        if (null != cookies && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(AuthServerConstant.REDIS_SESSION_ID_KEY)) {
                    String rsessionId = cookie.getValue();
                    String rsessionJson = stringRedisTemplate.opsForValue().get(rsessionId);
                    HashMap<String, String> rsession;
                    if (StringUtils.isEmpty(rsessionJson)) {
                        rsession = new HashMap<>();
                    } else {
                        rsession = JSON.parseObject(rsessionJson, HashMap.class);
                    }
                    String s = rsession.get(AuthServerConstant.LOGIN_USER);
                    if (!StringUtils.isEmpty(s)) {
                        MemberRespVo loginUser = JSON.parseObject(s, new TypeReference<MemberRespVo>() {
                        });
                        session.setAttribute(AuthServerConstant.LOGIN_USER, loginUser);
                    }
                }
            }
        }
        Object attribute = session.getAttribute(AuthServerConstant.LOGIN_USER);
        model.addAttribute(AuthServerConstant.LOGIN_USER, attribute);
        System.out.println("thymeleaf可以直接获取session中的属性进行使用:" + attribute);



        List<CategoryEntity> categorys = categoryService.listLevel1Categorys();
        model.addAttribute("categorys", categorys);
        return "index";
    }

可以优化的地方

  • cookie和session都有过期时间

  • 看一看Java的sessionid是怎么生成的,那么我们redis中放置的sessionid可以以同样的方法生成

整合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架构师 | 微服务 | 大型电商项目.


本文完,感谢您的关注支持!


  • 17
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值