Franz`s blog

sso模式之URL重定向传播会话

SSO是什么

SSO既SingleSignOn,用户在身份认证服务器上登录一次后,即可无需登录访问其他单点登录系统中的信息。

本文中认证服务器为 auth.fzzzzz.tech

客户端1 client.stp.com

客户端2 client2.stp.com

部署在不同的域名之下,但是后端可以连接同一个Redis

最终效果如图

screenshots

当在客户端2通过认证服务器认证后,客户端2获得登录状态,客户端1登录时用户可以无感登录。

原理

  1. 用户跳转到子系统登录接口

    http://auth.fzzzzz.tech:5500/sso2/auth/,并携带back参数记录初始页面URL。

    登录接口将获得新的cookie

    • 形如:http://{sso-client}/sso/login?back=xxx
  2. 子系统检测到此用户尚未登录,再次将其重定向至SSO认证中心,并携带redirect参数

    记录子系统的登录页URL。

    • 形如:http://{sso-server}/sso/auth?redirect=xxx?back=xxx
  3. 用户进入了 SSO认证中心 的登录页面,开始登录。

  4. 用户 输入账号密码 并 登录成功,SSO认证中心再次将用户重定向至子系统的登录接口 /sso/login,并携带ticket码参数。

    • 形如:http://{sso-client}/sso/login?ticket=xxxxxxxxx
  5. 子系统根据 ticket码SSO-Redis 中获取账号id,并在子系统登录此账号会话。

  6. 当其他系统进入统一登录接口时,因为存在cookie,直接如同第5步返回ticket

代码实现

后端认证代码实现

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package main

import (
"context"
"fmt"
"github.com/redis/go-redis/v9"
uuid "github.com/satori/go.uuid"
"log"
"net/http"
"time"
)

var rdb = redis.NewClient(&redis.Options{
Addr: "xxxxxxxxxx",
Password: "xxxxxxxxxx",
DB: 0, // 默认DB 0
})
var ctx = context.Background()

// 跨域中间件
func CrosMiddleware(handler http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
//放行所有OPTIONS方法
if r.Method == "OPTIONS" {
w.WriteHeader(200)
return
}
handler.ServeHTTP(w, r)
})
}

//通过用户名和密码 或者 cookie返回ticket
func auth(w http.ResponseWriter, r *http.Request) {
// 模拟登录
// 如果cookie不存在
if username := r.FormValue("username"); username == "admin" &&
r.FormValue("password") == "admin"{
tk := uuid.NewV4().String()
cookie := &http.Cookie{
Name: "token",
Value: tk,
}
if err := rdb.Set(ctx, "user.login.session."+username, tk, 0).Err();err != nil {
log.Fatal("redis error")
}
http.SetCookie(w, cookie)
ticket := uuid.NewV4().String()
if err1 := rdb.Set(ctx, "user.login.ticket."+ticket, username, 100*time.Second).Err();err1 != nil {
log.Fatal("redis error")
}
w.Write([]byte(fmt.Sprintf("{\"code\":200,\"msg\":\"success\",\"ticket\":\"%s\"}", ticket)))
} else if cookie, cookieErr := r.Cookie("token"); cookieErr == nil {
// 如果 cookie 存在
username := rdb.Get(ctx, "user.login.session."+cookie.Value).Val()
ticket := uuid.NewV4().String()
err1 := rdb.Set(ctx, "user.login.ticket."+ticket, username, 100*time.Second).Err()
if err1 != nil {
log.Fatal("redis error")
}
w.Write([]byte(fmt.Sprintf("{\"code\":200,\"msg\":\"success\",\"ticket\":\"%s\"}", ticket)))
} else {
w.Write([]byte("{\"code\":500,\"msg\":\"fail\",\"ticket\":\"null\"}"))
}
}

// 通过ticket返回token
func ticketAuth(w http.ResponseWriter, r *http.Request) {
// 根据用户名返回ticket
ticket := r.FormValue("ticket")
tk := uuid.NewV4().String()
cookie := &http.Cookie{
Name: "token",
Value: tk,
}
http.SetCookie(w, cookie)
if err := rdb.Set(ctx, "user.login.session."+rdb.Get(ctx, "user.login.ticket."+ticket).Val(), tk, 0).Err();err != nil {
log.Fatal("redis error")
}
}

func main() {
http.Handle("/auth", CrosMiddleware(auth))
http.Handle("/sso", CrosMiddleware(ticketAuth))
err := http.ListenAndServe(":80", nil) // 设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

前端认证代码

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sso-auth</title>
</head>

<body>

</body>
<script>
render()

document.querySelector("form").addEventListener('click', function (event) {
event.preventDefault();
});

document.querySelector("input[type='submit']").addEventListener('click', function (event) {
event.preventDefault();
login();
});

// 如果存在cookie 自动跳转回源页面
if (getCookie("token")) {
// console.log(getCookie("token"));
autoLoginIfLogin()
}

// 登录方法
async function login() {
let username = document.querySelector("input[name='username']").value;
let password = document.querySelector("input[name='password']").value;
let res = await fetch(`http://auth.fzzzzz.tech/auth?username=${username}&password=${password}`, {
method: "GET",
credentials: "include"
})
let body = await res.json()
document.querySelector(".msg").innerHTML = body['msg']
//重定向
redi(body['ticket'])
}

async function autoLoginIfLogin() {
let res = await fetch(`http://auth.fzzzzz.tech/auth`, {
method: "GET",
credentials: "include"
})
let body = await res.json()
document.querySelector(".msg").innerHTML = body['msg']
redi(body['ticket'])
}

function render(){
document.querySelector('body').innerHTML = `登录
<form>
<input type="text" name="username" placeholder="用户名"><br />
<input type="password" name="password" placeholder="密码"><br />
<input type="submit" value="登录">
<button onclick='clearCookie()'>清空cookie</button>
<div class="msg"></div>
</form>`
}

function redi(ticket) {
// 重定向回back
let back = getQueryString("back")
console.log(back);
back.includes("?") ? window.location.href = `${back}&ticket=${ticket}` : window.location.href = `${back}?ticket=${ticket}`

}

function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}

// 清除所有cookie
function clearCookie() {
var keys = document.cookie.match(/[^ =;]+(?=\=)/g);
if (keys) {
for (var i = keys.length; i--;) {
document.cookie = keys[i] + '=0;path=/;expires=' + new Date(0).toUTCString();
document.cookie = keys[i] + '=0;path=/;domain=' + document.domain + ';expires=' + new Date(0).toUTCString();
document.cookie = keys[i] + '=0;path=/;domain=kevis.com;expires=' + new Date(0).toUTCString();
}
}
console.log('已清除');
}

// 获取cookie方法
function getCookie(name) {
var cookieArr = document.cookie.split(";");
for (var i = 0; i < cookieArr.length; i++) {
var cookiePair = cookieArr[i].split("=");
if (name == cookiePair[0].trim()) {
return decodeURIComponent(cookiePair[1]);
}
}
return null;
}
</script>

</html>

测试客户端代码

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sso-client</title>
</head>

<body>

</body>
<script>

render()

if (getQueryString("ticket")) {
loginByTicket(getQueryString("ticket")).then(() => {
render()
})
}

async function loginByTicket(ticket) {
let url = window.location.origin
// 如果有端口移除端口
if (url.split(":").length > 2) {
url = url.split(":")[0] + ":" + url.split(":")[1]
}
let res = await fetch(`${url}/sso?ticket=${ticket}`, {
method: "GET",
credentials: "include"
})
// 移除ticket
window.history.replaceState({}, null, window.location.href.split("?")[0])
}

function clearCookie() {
var keys = document.cookie.match(/[^ =;]+(?=\=)/g);
if (keys) {
for (var i = keys.length; i--;) {
document.cookie = keys[i] + '=0;path=/;expires=' + new Date(0).toUTCString();
document.cookie = keys[i] + '=0;path=/;domain=' + document.domain + ';expires=' + new Date(0).toUTCString();
document.cookie = keys[i] + '=0;path=/;domain=kevis.com;expires=' + new Date(0).toUTCString();
}
}
console.log('已清除');
}

function render() {
document.querySelector("body").innerHTML =
getCookie("token") ?
`已登录 cookie = ${getCookie("token")}` :
`未登录 <a href='http://auth.fzzzzz.tech:5500/sso2/auth/?back=${window.location.href}'>去认证中心登录</a>`;
document.querySelector("body").innerHTML += "<br /><button onclick='clearCookie()'>清空cookie</button>"
}

function getCookie(name) {
var cookieArr = document.cookie.split(";");
for (var i = 0; i < cookieArr.length; i++) {
var cookiePair = cookieArr[i].split("=");
if (name == cookiePair[0].trim()) {
return decodeURIComponent(cookiePair[1]);
}
}
return null;
}

function getQueryString(name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return unescape(r[2]);
}
return null;
}

</script>
</html>