单点登录(Single Sign-On,简称 SSO)是一种在多个系统间共享登录状态的机制,使用户只需在一个地方登录一次,就能访问多个相互信任的系统,无需重复输入用户名和密码。
流程
服务器:
- app1.com (应用1服务器)
- app2.com (应用2服务器)
- sso.com (授权服务器)
客户端:
- 浏览器访问应用1 “https://app1.com“ [cookie:none]
- 因为浏览器没有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]
- 因为浏览器没有sso.com的cookie,授权服务器会返回登陆窗口 “https://sso.com/login“ [cookie:sso.com](GET请求 该地址由授权服务器自定义)
- 浏览器登陆,将用户名和密码提交给授权服务器。提交POST请求到 “https://sso.com/login“ (POST请求 该地址由授权服务器自定义)
- 登陆成功后会将[cookie:sso.com]保存在授权服务器,生成code并重定向到redirect_uri,即应用1服务器(app1.com)提供的监听端口。 “https://app1.com/callback?code=abcd1234“
- 应用1服务器(app1.com)接收到授权服务器(sso.com)的请求后,携带code向授权服务器请求token(如access_token, refresh_token, id_token) “https://sso.com/oauth2/token?code=abcd1234“
- 授权服务器(sso.com)验证code后,颁发对应的token
- 应用1服务器(app1.com)收到请求token的回复后,验证收到token的合法性。
- 应用1服务器(app1.com)从缓存中拿出浏览器最初的请求(步骤1)并为该请求生成session,浏览器重定向到该请求并带有cookie [cookie:app1.com],之后的浏览器与应用1服务器(app1.com)的交互都携带cookie [cookie:app1.com]进行身份认证
- 浏览器访问应用2 “https://app2.com“ [cookie:none]
- 因为浏览器没有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]
- 授权服务器(sso.com)验证[cookie:sso.com],可得到之前的登陆状态,直接返回code给应用2服务器(app2.com) “https://app2.com/callback?code=abcd1234“
- 应用2服务器(app2.com)向授权服务器(sso.com)发送请求换取token “https://sso.com/oauth2/token“
- 授权服务器(sso.com)验证code并颁发对应的token
- 应用2服务器(app2.com)收到token后,验证token的合法性
- 应用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"> <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>
<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>
<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}"/>
<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>
<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>
<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
| <!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>
|