单点登录

单点登录

概念

SSO,是一种身份验证机制,运行用户通过一次登录可以访问多个相关但独立的软件系统

一旦用户登录一个系统,所有相关系统都将识别该用户为已认证状态

实现

背景

认证中心:颁发令牌

应用A - 应用B:两个独立应用,共享同一个认证中心

技术栈

JS、Nodejs、OAuth 2.0协议

流程

前端部分

用户访问A

A检测登录状态 -> (未登录重定向认证中心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 检查本地是否有有效的访问令牌
function checkLoginStatus() {
const token = localStorage.getItem('accessToken');
if (!token) {
// 如果没有令牌,重定向到认证中心
redirectToAuthServer();
} else {
console.log('用户已登录');
}
}

function redirectToAuthServer() {
const clientId = 'your-client-id'; // 应用的客户端ID
const redirectUri = encodeURIComponent('https://app-a.com/callback'); // 应用A的回调地址
const authUrl = `https://auth-server.com/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}`;
window.location.href = authUrl;
}

在认证中心登录

认证中心生成认证码 + 重定向A

1
2
3
4
5
// 获取URL中的授权码
function getAuthorizationCode() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('code');
}

A用授权码向认证中心换取访问令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用授权码换取访问令牌
async function exchangeToken(code) {
const response = await fetch('https://auth-server.com/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: 'your-client-id',
client_secret: 'your-client-secret',
redirect_uri: 'https://app-a.com/callback'
})
});

const data = await response.json();
const accessToken = data.access_token;

// 保存访问令牌
localStorage.setItem('accessToken', accessToken);
console.log('登录成功,访问令牌已保存');
}

A保存令牌

用户访问B

B检查是否已登录 -> (未登录重定向认证中心)

1
2
3
4
5
6
7
>// 页面加载时检查授权码
>const code = getAuthorizationCode();
>if (code) {
>exchangeToken(code);
>} else {
>checkLoginStatus();
>}

认证中心识别已登录,直接返回B并附带授权码

B用授权码向认证中心换取访问令牌

后端部分

认证中心登录页面

1
2
3
4
5
6
<!-- 登录页面 -->
<form action="/login" method="POST">
<input type="text" name="username" placeholder="用户名" required />
<input type="password" name="password" placeholder="密码" required />
<button type="submit">登录</button>
</form>

认证中心接收到登录请求后,验证用户信息,并生成授权码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.post('/login', (req, res) => {
const { username, password } = req.body;

// 验证用户信息(这里仅作演示)
if (username === 'admin' && password === '123456') {
const code = generateAuthorizationCode(); // 生成随机授权码
const redirectUri = 'https://app-a.com/callback'; // 应用A的回调地址
res.redirect(`${redirectUri}?code=${code}`);
} else {
res.status(401).send('用户名或密码错误');
}
});

function generateAuthorizationCode() {
return Math.random().toString(36).substring(2, 15); // 随机字符串
}

应用A或应用B使用授权码向认证中心兑换访问令牌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.post('/token', (req, res) => {
const { grant_type, code, client_id, client_secret, redirect_uri } = req.body;

// 验证授权码和客户端信息
if (grant_type === 'authorization_code' && isValidCode(code)) {
const accessToken = generateAccessToken(); // 生成随机访问令牌
res.json({ access_token: accessToken });
} else {
res.status(400).send('无效的授权码或客户端信息');
}
});

function generateAccessToken() {
return Math.random().toString(36).substring(2, 15); // 随机字符串
}

function isValidCode(code) {
// 验证授权码是否有效(这里仅作演示)
return true;
}

总结

  1. 用户访问应用A,前端检查本地是否有访问令牌。如果没有,则重定向到认证中心。
  2. 用户在认证中心登录成功后,认证中心生成授权码并重定向回应用A。
  3. 应用A使用授权码向认证中心兑换访问令牌,并保存到本地。
  4. 用户访问应用B,重复上述流程。由于用户已在认证中心登录,认证中心会直接返回授权码,无需再次输入用户名和密码。