淘宝、天猫的单点登录-SSO

淘宝、天猫的单点登录-SSO

今天梳理了下单点登录,这个很早之前有接触过。单点登录的方案有很多,不同的方案也有不同的用户体验,最终实现的是集中管理登录,登录状态在所有允许的系统中通用。
最简单的是所有系统都在同源<协议://域名{子域名}:端口>,直接通过cookie就可以解决。非同源的常见方案有用地址栏加token实现,有过通过域名转发映射解决的、也有通过部署多套系统解决的😂。
兴起之下想看下阿里巴巴他们SSO是怎么玩的。

分析淘宝和天猫SSO逻辑

淘宝地址:https://www.taobao.com/
天猫地址:https://www.tmall.com/

  1. 查看淘宝域名下的所有cookie信息以及数据请求信息:
    cookie

  2. 查看淘宝控制台警告信息
    警告

  3. 其中的一个请求连接
    请求连接

  4. 查看淘宝控制台接口请求信息
    网络请求信息

看这个应该容易想起来淘宝是怎么玩的SSO了。没错,他是利用的浏览器同源策略加载静态资源时的非同源限制的问题。下面模仿者玩一下,确认下是不是这么回事。

验证猜测是否正确:

三个项目分别是:mysso<真正的登录入口>、myservice1<使用单点登录的服务1>、myservice2<使用单点登录的服务2>,我用maven继承关系把他们放到一个项目里了。

  1. 首先确定三个测试使用的域名:
    mysso.com : sso登录的项目; myservice1.com : 使用sso服务的系统1; myservice2.com : 使用sso服务的系统2;

配置hosts

  1. 配置hosts
    1
    127.0.0.1 mysso.com  myservice1.com myservice2.com

配置nginx

  1. 配置nginx反向代理
    3.1. mysso.com.conf:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    server {
    listen 80;
    server_name mysso.com;

    #charset koi8-r;

    access_log logs/mysso.access.log main;
    error_log logs/mysso.error.log;

    #root /Users/brucexie/Documents/work/myspace/retail/retail-wap;

    location / {
    add_header 'Access-Control-Allow-Origin' "$http_origin";
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, POST';
    add_header 'Access-Control-Allow-Headers' 'X-Requested-With';
    proxy_pass http://localhost:8068;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
    root html;
    }

    }

3.2. myservice1.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server {
listen 80;
server_name myservice1.com;

#charset koi8-r;

access_log logs/myservice1.access.log main;
error_log logs/myservice1.error.log;

#root /Users/brucexie/Documents/work/myspace/retail/retail-wap;

location / {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With';
proxy_pass http://localhost:8066;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

}

3.3. myservice2.com.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
server {
listen 80;
server_name myservice2.com;

#charset koi8-r;

access_log logs/myservice2.access.log main;
error_log logs/myservice2.error.log;

#root /Users/brucexie/Documents/work/myspace/retail/retail-wap;

location / {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST';
add_header 'Access-Control-Allow-Headers' 'X-Requested-With';
proxy_pass http://localhost:8067;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

}

搭建测试代码

工程目录结构:
目录结构

考虑了下,代码太多。选择一些有意义的贴出来,后面将整个代码都上传到github上。这个项目代码本身是为了验证阿里sso的一种思路,将来有时间的话再来整理一下。
mysso中的LoginController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.sso.mysso.ssoservice;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* Created by xiehui1956(@)gmail.com on 2020/3/13
*/
@Controller
public class LoginController {

final String LC = "loginCookie", MG = "msg", TK = "abc";

@GetMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response, Model model
, @RequestParam(required = false, defaultValue = "1") String name
, @RequestParam(required = false, defaultValue = "2") String pwd) {

Cookie[] cookies = request.getCookies();
if (null != cookies)
for (Cookie cookie : cookies) {
if (cookie.getName().equals(LC) && cookie.getValue().equals("121")) {
model.addAttribute(MG, "already-login");
model.addAttribute(TK, "login-success");
return "/index";

}
}

if (name.equals(pwd)) {
Cookie cookie = new Cookie(LC, "121");
cookie.setPath("/");
cookie.setDomain(".mysso.com");
response.addCookie(cookie);
model.addAttribute(MG, "first-login-success");
model.addAttribute(TK, "login-success");
return "/index";
}
return "/index";
}

}

mysso中的index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>main</title>
<script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.js"></script>
<script type="application/javascript">
let script = document.createElement('script');
script.setAttribute('src', 'http://myservice1.com/login?token=[[${abc}]]');
document.getElementsByTagName('head')[0].appendChild(script);

script = document.createElement('script');
script.setAttribute('src', 'http://myservice2.com/login?token=[[${abc}]]');
document.getElementsByTagName('head')[0].appendChild(script);
</script>
</head>
<body>
sso-[[${msg}]]
<br>
<span th:text="${msg}"></span>
</body>
</html>

由于只是测试,这里请求地址写死了。

myservice1中的LoginController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.sso.mysso.myservice1;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* Created by xiehui1956(@)gmail.com on 2020/3/13
*/
@RestController
public class LoginController {

final String LC = "loginCookie";

@GetMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response, String token) throws IOException {

Cookie[] cookies = request.getCookies();
if (null != cookies)
for (Cookie cookie : cookies) {
if (cookie.getName().equals(LC) && cookie.getValue().equals("121")) {
return "already-login";
}
}

if ("login-success".equals(token)) {
Cookie cookie = new Cookie(LC, "121");
cookie.setPath("/");
cookie.setDomain(".myservice1.com");
response.addCookie(cookie);
return "first-login-success";
}

response.sendRedirect("http://mysso.com/login");
return "error";
}

}

myservice2中的LoginController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.sso.mysso.myservice2;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* Created by xiehui1956(@)gmail.com on 2020/3/13
*/
@RestController
public class LoginController {

final String LC = "loginCookie";

@GetMapping("/login")
public String login(HttpServletRequest request, HttpServletResponse response, String token) throws IOException {

Cookie[] cookies = request.getCookies();
if (null != cookies)
for (Cookie cookie : cookies) {
if (cookie.getName().equals(LC) && cookie.getValue().equals("121")) {
return "already-login";
}
}

if ("login-success".equals(token)) {
Cookie cookie = new Cookie(LC, "121");
cookie.setPath("/");
cookie.setDomain(".myservice2.com");
response.addCookie(cookie);
return "first-login-success";
}

response.sendRedirect("http://mysso.com/login");
return "error";
}

}

跑一下看结果

sso登录后的结果
sso

sso异常警告信息,发现这个异常和淘宝控制台包的异常有些相似
异常警告

service1服务已经登录了
service1

service2服务已经登录了
service2

退出登录也是这样的。

总结:在一个服务上通过加载资源的形式去调用其他服务的登录接口,处理请求接口的服务根据标识去写入客户端cookie。
实际的项目中要考虑的问题远不止这些,像安全策略、埋点分析这个demo中都没有涉及。仅验证了一种友好的SSO登录实现方式,不会像有些项目一样走到哪里屁股后面都跟着一个token。
项目源码地址<欢迎沟通交流>:https://github.com/xieyucan/mysso