分布式回话

相同顶级域名的单点登录 SSO

引子
在面试过程中有时候会被问到单点登录,那么单点登录又称之为 Single Sign On ,简称 SSO, 单点登录可以通过基于用户会话的共享,他分为两种,先来看第一种,那就是他的原理是分布式会话来实现。
比如说现在有个一级域名为 www.imooc.com,是教育类网站,但是慕课网有其他的产品线,可以通过构建二级域名提供服务给用户访问,比如: music. imooc.com,shop. imooc.com,blog. imooc.com等等,分别为慕课音乐,慕课电商以及慕课博客等,用户只需要在其中一个站点登录,那么其他站点也会随之而登录。也就是说,用户自始至终只在某一个网站下登录后,那么他所产生的会话,就共享给了其他的网站,实现了单点网站登录后,同时间接登录了其他的网站,那么这个其实就是单点登录,他们的会话是共享的,都是同一个用户会话。


Cookie + Redis 实现 SSO

那么之前我们所实现的分布式会话后端是基于redis的,如此会话可以流窜在后端的任意系统,都能获取到缓存中的用户数据信息,前端通过使用cookie,可以保证在同域名的一级二级下获取,那么这样一来,cookie中的信息userid和token是可以在发送请求的时候携带上的,这样从前端请求后端后是可以获取拿到的,这样一来,其实用户在某一端登录注册以后,其实cookie和redis中都会带有用户信息,只要用户不退出,那么就能在任意一个站点实现登录了。

  • 那么这 个原理主要也是cookie和网站的依赖关系,顶级域名www. imooc. com 和 *. imooc. com的cookie值是可以共享的可以被携带至后端的,比如设置为 .imoc.com,.t.mukewang. com,如此是OK的。
  • 二级域名自己的独立cookie是不能共享的,不能被其他二级域名获取,比如:music. imooc. com的cookie是不能被mtv. imooc.com共享,两者互不影响,要共享必须设置为 .imooc. com

Cookie 共享测试

找到前端项目app.js,开启如下代码,设置你的对应域名,需要和SwitchHosts相互对应:
image.png
image.png
如下图,可以看到,不论是在shop或是center中,两个站点都能够在用户登录后共享用户信息。
image.png
如此一来,cookie中的信息被携带至后端,而后端又实现了分布式会话,那么如此一-来,单点登录就实现了,用户无需再跨站点登录了。
上述过程我们通过下图来更加具象化的展示,只要前端网页都在同一个顶级域名下,就能实现cookie与session的共享
image.png


不同顶级域名的单点登录

顶级域名不同怎么办?
上一节单点登录是基于相同顶级域名做的,那么如果顶级域名都不一样,咋办? 比如www. imooc. com 要和 ww. mukewang. com 的会话实现共享,这个时候又该如何? !如下图,这个时候的 cookie 由于顶级域名不同,就不能实现 cookie 跨域了,每个站点各自请求到服务端,cookie 无法同步。比如 www. imooc .com 下的用户发起请求后会有 cookie,但是他又访问了 www.abe.com ,由于cookie无法携带,所以会要你二次登录。
image.png
那么遇到顶级域名不同却又要实现单点登录该如何实现呢?我们来参考下面一张图:
image.png
如上图所示,多个系统之间的登录会通过一个独立的登录系统去做验证,它就相当于是一个中介公司,整合了所有人,你要看房经过中介允许拿钥匙就行,实现了统一的登录。那么这个就称之为CAS系统,CAS全称为Central Authentication Service 即中央认证服务,是一个单点登录的解决方案,可以用于不同顶级域名之间的单点登录。那么在咱们课程中呢目前的项目结构源码不需要去破坏,我们只需要构建两个静态站点来测试使用即可。在CAS中的具体的流程参考如下时序图:
image.png
image.png

当前业务下应用

使用Redis实现用户会话

SpringSession实现用户会话

拦截器

主要作用

  • 用户访问对应的接口是否有权限
  • 根据请求头携带的token获取用户信息,放到当前线程中
    • headers、body中进行取出
  • 记录接口访问信息等等

    单点登录

    相同顶级域名的单点登录SSO
    工作暂未用到,不必深入理解,先搞懂其概念,待用时,再详细学习

    CAS单点登录

    code ```java package com.imooc.controller;

@Controller public class SSOController {

  1. public static final String REDIS_USER_TOKEN = "redis_user_token";
  2. public static final String REDIS_USER_TICKET = "redis_user_ticket";
  3. public static final String REDIS_TMP_TICKET = "redis_tmp_ticket";
  4. public static final String COOKIE_USER_TICKET = "cookie_user_ticket";
  5. @Autowired
  6. private UserService userService;
  7. @Autowired
  8. private RedisOperator redisOperator;
  9. @GetMapping("/login")
  10. public String login(String returnUrl,
  11. Model model,
  12. HttpServletRequest request,
  13. HttpServletResponse response) {
  14. model.addAttribute("returnUrl", returnUrl);
  15. // 1. 获取userTicket门票,如果cookie中能够获取到,证明用户登录过,此时签发一个一次性的临时票据并且回跳
  16. String userTicket = getCookie(request, COOKIE_USER_TICKET);
  17. boolean isVerified = verifyUserTicket(userTicket);
  18. if (isVerified) {
  19. String tmpTicket = createTmpTicket();
  20. return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;
  21. }
  22. // 2. 用户从未登录过,第一次进入则跳转到CAS的统一登录页面
  23. return "login";
  24. }
  25. /**
  26. * 校验CAS全局用户门票
  27. *
  28. * @param userTicket
  29. * @return
  30. */
  31. private boolean verifyUserTicket(String userTicket) {
  32. // 0. 验证CAS门票不能为空
  33. if (StringUtils.isBlank(userTicket)) {
  34. return false;
  35. }
  36. // 1. 验证CAS门票是否有效
  37. String userId = redisOperator.get(REDIS_USER_TICKET + ":" + userTicket);
  38. if (StringUtils.isBlank(userId)) {
  39. return false;
  40. }
  41. // 2. 验证门票对应的user会话是否存在
  42. String userRedis = redisOperator.get(REDIS_USER_TOKEN + ":" + userId);
  43. if (StringUtils.isBlank(userRedis)) {
  44. return false;
  45. }
  46. return true;
  47. }
  48. /**
  49. * CAS的统一登录接口
  50. * 目的:
  51. * 1. 登录后创建用户的全局会话 -> uniqueToken
  52. * 2. 创建用户全局门票,用以表示在CAS端是否登录 -> userTicket
  53. * 3. 创建用户的临时票据,用于回跳回传 -> tmpTicket
  54. *
  55. *
  56. *
  57. */
  58. @PostMapping("/doLogin")
  59. public String doLogin(String username,
  60. String password,
  61. String returnUrl,
  62. Model model,
  63. HttpServletRequest request,
  64. HttpServletResponse response) throws Exception {
  65. model.addAttribute("returnUrl", returnUrl);
  66. // 0. 判断用户名和密码必须不为空
  67. if (StringUtils.isBlank(username) ||
  68. StringUtils.isBlank(password)) {
  69. model.addAttribute("errmsg", "用户名或密码不能为空");
  70. return "login";
  71. }
  72. // 1. 实现登录
  73. Users userResult = userService.queryUserForLogin(username,
  74. MD5Utils.getMD5Str(password));
  75. if (userResult == null) {
  76. model.addAttribute("errmsg", "用户名或密码不正确");
  77. return "login";
  78. }
  79. // 2. 实现用户的redis会话
  80. String uniqueToken = UUID.randomUUID().toString().trim();
  81. UsersVO usersVO = new UsersVO();
  82. BeanUtils.copyProperties(userResult, usersVO);
  83. usersVO.setUserUniqueToken(uniqueToken);
  84. redisOperator.set(REDIS_USER_TOKEN + ":" + userResult.getId(),
  85. JsonUtils.objectToJson(usersVO));
  86. // 3. 生成ticket门票,全局门票,代表用户在CAS端登录过
  87. String userTicket = UUID.randomUUID().toString().trim();
  88. // 3.1 用户全局门票需要放入CAS端的cookie中
  89. setCookie(COOKIE_USER_TICKET, userTicket, response);
  90. // 4. userTicket关联用户id,并且放入到redis中,代表这个用户有门票了,可以在各个景区游玩
  91. redisOperator.set(REDIS_USER_TICKET + ":" + userTicket, userResult.getId());
  92. // 5. 生成临时票据,回跳到调用端网站,是由CAS端所签发的一个一次性的临时ticket
  93. String tmpTicket = createTmpTicket();
  94. /**
  95. * userTicket: 用于表示用户在CAS端的一个登录状态:已经登录
  96. * tmpTicket: 用于颁发给用户进行一次性的验证的票据,有时效性
  97. */
  98. /**
  99. * 举例:
  100. * 我们去动物园玩耍,大门口买了一张统一的门票,这个就是CAS系统的全局门票和用户全局会话。
  101. * 动物园里有一些小的景点,需要凭你的门票去领取一次性的票据,有了这张票据以后就能去一些小的景点游玩了。
  102. * 这样的一个个的小景点其实就是我们这里所对应的一个个的站点。
  103. * 当我们使用完毕这张临时票据以后,就需要销毁。
  104. */

// return “login”; return “redirect:” + returnUrl + “?tmpTicket=” + tmpTicket; }

  1. /**
  2. * 效验用户回话
  3. *
  4. * 1、效验临时票据
  5. * 2、根据cookie中userTicket,获取用户回话
  6. * 3、根据userTicket从redis中获取userId
  7. * 4、根据userId获取用户信息
  8. * 5、能获取到则返回用户信息
  9. * @param tmpTicket
  10. * @param request
  11. * @param response
  12. * @return
  13. * @throws Exception
  14. */
  15. @PostMapping("/verifyTmpTicket")
  16. @ResponseBody
  17. public IMOOCJSONResult verifyTmpTicket(String tmpTicket,
  18. HttpServletRequest request,
  19. HttpServletResponse response) throws Exception {
  20. // 使用一次性临时票据来验证用户是否登录,如果登录过,把用户会话信息返回给站点
  21. // 使用完毕后,需要销毁临时票据
  22. String tmpTicketValue = redisOperator.get(REDIS_TMP_TICKET + ":" + tmpTicket);
  23. if (StringUtils.isBlank(tmpTicketValue)) {
  24. return IMOOCJSONResult.errorUserTicket("用户票据异常");
  25. }
  26. // 0. 如果临时票据OK,则需要销毁,并且拿到CAS端cookie中的全局userTicket,以此再获取用户会话
  27. if (!tmpTicketValue.equals(MD5Utils.getMD5Str(tmpTicket))) { // 不相等
  28. return IMOOCJSONResult.errorUserTicket("用户票据异常");
  29. } else {
  30. // 销毁临时票据
  31. redisOperator.del(REDIS_TMP_TICKET + ":" + tmpTicket);
  32. }
  33. // 1. 验证并且获取用户的userTicket
  34. String userTicket = getCookie(request, COOKIE_USER_TICKET);
  35. String userId = redisOperator.get(REDIS_USER_TICKET + ":" + userTicket);
  36. if (StringUtils.isBlank(userId)) {
  37. return IMOOCJSONResult.errorUserTicket("用户票据异常");
  38. }
  39. // 2. 验证门票对应的user会话是否存在
  40. String userRedis = redisOperator.get(REDIS_USER_TOKEN + ":" + userId);
  41. if (StringUtils.isBlank(userRedis)) {
  42. return IMOOCJSONResult.errorUserTicket("用户票据异常");
  43. }
  44. // 验证成功,返回OK,携带用户会话
  45. return IMOOCJSONResult.ok(JsonUtils.jsonToPojo(userRedis, UsersVO.class));
  46. }
  47. /**
  48. * 退出登录,清空redis中相关票据
  49. * 1、清楚cookie
  50. * 2、清楚userTicket
  51. * 3、清楚用户数据
  52. * @param userId
  53. * @param request
  54. * @param response
  55. * @return
  56. * @throws Exception
  57. */
  58. @PostMapping("/logout")
  59. @ResponseBody
  60. public IMOOCJSONResult logout(String userId,
  61. HttpServletRequest request,
  62. HttpServletResponse response) throws Exception {
  63. // 0. 获取CAS中的用户门票
  64. String userTicket = getCookie(request, COOKIE_USER_TICKET);
  65. // 1. 清除userTicket票据,redis/cookie
  66. deleteCookie(COOKIE_USER_TICKET, response);
  67. redisOperator.del(REDIS_USER_TICKET + ":" + userTicket);
  68. // 2. 清除用户全局会话(分布式会话)
  69. redisOperator.del(REDIS_USER_TOKEN + ":" + userId);
  70. return IMOOCJSONResult.ok();
  71. }
  72. /**
  73. * 创建临时票据
  74. *
  75. * @return
  76. */
  77. private String createTmpTicket() {
  78. String tmpTicket = UUID.randomUUID().toString().trim();
  79. try {
  80. redisOperator.set(REDIS_TMP_TICKET + ":" + tmpTicket,
  81. MD5Utils.getMD5Str(tmpTicket), 600);
  82. } catch (Exception e) {
  83. e.printStackTrace();
  84. }
  85. return tmpTicket;
  86. }
  87. private void setCookie(String key,
  88. String val,
  89. HttpServletResponse response) {
  90. Cookie cookie = new Cookie(key, val);
  91. cookie.setDomain("sso.com");
  92. cookie.setPath("/");
  93. response.addCookie(cookie);
  94. }
  95. private void deleteCookie(String key,
  96. HttpServletResponse response) {
  97. Cookie cookie = new Cookie(key, null);
  98. cookie.setDomain("sso.com");
  99. cookie.setPath("/");
  100. cookie.setMaxAge(-1);
  101. response.addCookie(cookie);
  102. }
  103. private String getCookie(HttpServletRequest request, String key) {
  104. Cookie[] cookieList = request.getCookies();
  105. if (cookieList == null || StringUtils.isBlank(key)) {
  106. return null;
  107. }
  108. String cookieValue = null;
  109. for (int i = 0; i < cookieList.length; i++) {
  110. if (cookieList[i].getName().equals(key)) {
  111. cookieValue = cookieList[i].getValue();
  112. break;
  113. }
  114. }
  115. return cookieValue;
  116. }

}

```