单点登陆原理

单点登录(Single Sign-On,简称 SSO)是一种在多个系统间共享登录状态的机制,使用户只需在一个地方登录一次,就能访问多个相互信任的系统,无需重复输入用户名和密码。

流程

服务器:

  • app1.com (应用1服务器)
  • app2.com (应用2服务器)
  • sso.com (授权服务器)

客户端:

  • client (浏览器)
  1. 浏览器访问应用1 “https://app1.com“ [cookie:none]
  2. 因为浏览器没有app1.com的cookie,应用1需要重定向到授权服务器(sso.com) “https://sso.com/oauth2/authorize?response_type=code&client_id=xxx&redirect_uri=https://app1.com/callback“ [cookie:none]
  3. 因为浏览器没有sso.com的cookie,授权服务器会返回登陆窗口 “https://sso.com/login“ [cookie:sso.com](GET请求 该地址由授权服务器自定义)
  4. 浏览器登陆,将用户名和密码提交给授权服务器。提交POST请求到 “https://sso.com/login“ (POST请求 该地址由授权服务器自定义)
  5. 登陆成功后会将[cookie:sso.com]保存在授权服务器,生成code并重定向到redirect_uri,即应用1服务器(app1.com)提供的监听端口。 “https://app1.com/callback?code=abcd1234
  6. 应用1服务器(app1.com)接收到授权服务器(sso.com)的请求后,携带code向授权服务器请求token(如access_token, refresh_token, id_token) “https://sso.com/oauth2/token?code=abcd1234
  7. 授权服务器(sso.com)验证code后,颁发对应的token
  8. 应用1服务器(app1.com)收到请求token的回复后,验证收到token的合法性。
    • JWT Token: 向授权服务器(sso.com)获取公钥,然后在本地验证 “https://sso.com/oauth2/jwks
    • Opaque Token: 将token发给授权服务器(sso.com)并由授权服务器(sso.com)验证token是否合法
  9. 应用1服务器(app1.com)从缓存中拿出浏览器最初的请求(步骤1)并为该请求生成session,浏览器重定向到该请求并带有cookie [cookie:app1.com],之后的浏览器与应用1服务器(app1.com)的交互都携带cookie [cookie:app1.com]进行身份认证
  10. 浏览器访问应用2 “https://app2.com“ [cookie:none]
  11. 因为浏览器没有app2.com的cookie,应用2服务器需要重定向到授权服务器(sso.com),因为在步骤3 浏览器已经缓存了授权服务器(sso.com)的cookie,故在浏览器访问授权服务器(sso.com)时,会携带之前的cookie “https://sso.com/oauth2/authorize?response_type=code&client_id=xxx&redirect_uri=https://app2.com/callback“ [cookie:sso.com]
  12. 授权服务器(sso.com)验证[cookie:sso.com],可得到之前的登陆状态,直接返回code给应用2服务器(app2.com) “https://app2.com/callback?code=abcd1234
  13. 应用2服务器(app2.com)向授权服务器(sso.com)发送请求换取token “https://sso.com/oauth2/token
  14. 授权服务器(sso.com)验证code并颁发对应的token
  15. 应用2服务器(app2.com)收到token后,验证token的合法性
  16. 应用2服务器(app2.com)从缓存中拿出浏览器最初的请求(步骤10)并为该请求生成session,浏览器重定向到该请求并带有cookie [cookie:app2.com],之后的浏览器与应用2服务器(app2.com)的交互都携带cookie [cookie:app2.com]进行身份认证

Spring Security 自定义的登陆和授权页面(Thymleaf)

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>授权确认</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center">

<div class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md">
<!-- Header -->
<div class="text-center mb-6">
<h2 class="text-2xl font-semibold text-gray-800">授权请求</h2>
<p class="text-gray-600 mt-2">
应用
<span class="font-medium text-indigo-600" th:text="${clientId}"></span>
请求访问你的账户
</p>
</div>

<!-- User Info -->
<div class="bg-gray-100 rounded-lg p-3 mb-6 text-sm text-gray-700">
当前登录用户:
<span class="font-semibold text-gray-900" th:text="${principalName}"></span>
</div>

<!-- Consent Form -->
<form method="post" th:action="@{/oauth2/authorize}" class="space-y-6">
<input type="hidden" name="client_id" th:value="${clientId}"/>
<input type="hidden" name="state" th:value="${state}"/>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

<!-- Scope List -->
<div>
<p class="font-medium text-gray-800 mb-2">该应用请求以下权限:</p>
<div class="space-y-2">
<div th:each="scope : ${scopes}" class="flex items-center space-x-2">
<input type="checkbox" name="scope"
th:value="${scope}" checked
class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
<label th:text="${scope}" class="text-gray-700"></label>
</div>
</div>
</div>

<!-- Buttons -->
<div class="flex justify-around mt-6">
<button type="submit" name="consent_action" value="cancel"
class="px-4 py-2 text-gray-700 bg-gray-200 hover:bg-gray-300 rounded-lg font-medium">
拒绝
</button>
<button type="submit" name="consent_action" value="approve"
class="px-4 py-2 bg-indigo-600 text-white hover:bg-indigo-700 rounded-lg font-medium">
同意授权
</button>
</div>
</form>

<!-- Footer -->
<p class="text-center text-xs text-gray-400 mt-8">
安全认证由 Amber IDP 提供
</p>
</div>

</body>
</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
<!-- src/main/resources/templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 flex items-center justify-center h-screen">
<div class="bg-white shadow-2xl rounded-2xl p-10 w-96">
<h2 class="text-2xl font-bold text-center text-gray-800 mb-6">欢迎登录</h2>

<form th:action="@{/login}" method="post" class="space-y-5">
<div>
<label for="username" class="block text-gray-600 mb-1">用户名</label>
<input type="text" id="username" name="username"
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-400 focus:outline-none"
placeholder="请输入用户名" required>
</div>

<div>
<label for="password" class="block text-gray-600 mb-1">密码</label>
<input type="password" id="password" name="password"
class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-indigo-400 focus:outline-none"
placeholder="请输入密码" required>
</div>

<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>

<button type="submit"
class="w-full bg-indigo-600 text-white font-semibold py-2 rounded-lg hover:bg-indigo-700 transition">
登录
</button>

<div th:if="${param.error}" class="text-red-600 text-sm text-center mt-2">
用户名或密码错误,请重试。
</div>

<div th:if="${param.logout}" class="text-green-600 text-sm text-center mt-2">
您已成功登出。
</div>
</form>
</div>
</body>
</html>