sso單點登錄
gitee 源碼地址: https://gitee.com/zarchary/sso-single-sign-on
郵箱: 361400631@qq.com
一、sso?
1.1 什么是sso
單點登錄(SingleSignOn,SSO),就是通過用戶的一次性鑒別登錄。當用戶在身份認證服務器上登錄一次以后,即可獲得訪問單點登錄系統中其他關聯系統和應用軟件的權限,同時這種實現是不需要管理員對用戶的登錄狀態或其他信息進行修改的,這意味着在多個應用系統中,用戶只需一次登錄就可以訪問所有相互信任的應用系統。
1.2 為什么用sso
如果只是對於父子域名之間,完全可以通過 session共享,來完成登錄一次即可在系統任意處都有權限;
如果是跨系統,跨域名:例如兩個完全不同的域名 www.aa.com; www.bb.com 沒有絲毫關聯的系統怎么完成在一處登錄之后,另一處是直接登錄的呢?
這就用到了 sso單點登錄。單點登錄原理: 用戶通過客戶端訪問服務器,如果是第一次訪問,會跳轉公共的 認證服務器 並攜帶客戶端的url地址;登錄成功后,認證服務器會重定向到客戶端並攜帶認證的令牌(token),同時會給所在瀏覽器設置 Cookie。
當第二個客戶端再次訪問,因為瀏覽器保存了 Cookie,就會自動完成登錄
1.3 環境搭建
- 所需服務器:
- 域名:client1.com:8001;client1.com:8002; sso.com:8000
- 所需環境:
- 使用 springBoot完成服務快速搭建。 這里我是用的版本是 2.6.6
二、正式開始
1. 修改本機的dns解析的域名
- 修改hosts文件
2. 創建客戶端/ 認證服務器
2.1 創建空包項目
2.2 創建客戶端 / 認證服務器
使用Spring Initializr 初始化springBoot項目。
添加基本依賴
完成創建,兩個 客戶端,一個認證服務器,客戶端配置是一樣的。可以復制
2.4 編排配置 properties
- Client1 客戶端配置
- Client1 :配置文件設置 server.port=8001;
- Client1: 認證服務器地址配置:sso.auth.path=http://sso.com:8000/auth/login.html
- Client1:客戶端1地址配置:sso.client1.path=http://client1.com:8001/emps
- Client1:認證服務用戶信息配置:sso.auth.info.auth=http://sso.com:8000/auth/userInfo
- Client2客戶端配置
- Client2 :配置文件設置 server.port=8002;
- Client2: 域名配置:sso.auth.path=http://sso.com:8000/auth/login.html
- Client2:客戶端2地址配置:sso.client2.path=http://client1.com:8001/emps
- Client2:認證服務用戶信息配置:sso.auth.info.auth=http://sso.com:8000/auth/userInfo
- sso 認證服務器配置
- sso服務:server.port=8000
- sso.auth.path=http://sso.com
- redis 地址 :spring.redis.host=192.168.64.3
3. 編寫具體業務
簡介:
- 客戶端,認證中心的域名地址都配置在properties 文件中,通過 @Value 獲取具體值
- 客戶端流程:
- 先獲取token信息,如果有直接跳轉受保護的資源信息 ,並將查出來的用戶信息渲染到頁面
- 如果沒有token信息,則去認證服務器登陸,如果瀏覽器保存了登陸的cookie信息,也是不需要登陸的。如果沒有cookie信息,則需要登陸。一旦登陸成功,只要瀏覽器不去清除 cookie信息,則與認證服務器有關的登陸,都是可以通過已有的 Cookie做免登陸的。這就是 sso的一處登陸,處處免登陸
- 認證中心服務器:
- 客戶端首先訪問的就是 認證中心的 login頁面, 這里如果是瀏覽器已經存儲了登陸的 Cookie信息,則直接返回客戶端的受保護資源的地址;否則重定向到 doLogin的登陸頁面
- doLogin 登陸的處理:
- 首先就是前端頁面的 攜帶的 客戶端的url,這是上一步認證中心將重定向的客戶端url ,set到 request請求域中的。這樣一來,登陸提交 form表單就會把 url攜帶。這樣認證中心在通過認證之后才知道重定向到哪個客戶端
- 全局唯一的 用戶id,這個可以通過 UUID工具生成。這樣每個用戶都是唯一的。通過這個唯一的 id,可以將用戶信息存入到 redis緩存中。
- userInfo 用戶信息獲取:
- 這是認證服務器提供給 客戶端的開放接口: 用戶在認證成功之后會拿到屬於自己的 token令牌,這樣客戶端就可以根據 token去遠程查詢 認證中心。通過 http 的get請求,攜帶 token獲取用戶的完整信息
流程圖:
3.1 client 客戶端編寫
-
編寫controller
-
package com.sso.client1.controller; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpSession; import java.util.ArrayList; /** * @author:zzz * @data: 2022/4/1 * @product_name: SsoServer */ @Controller public class ResourceController { /** * 遠程調用次數 */ private static Integer FLAG = 3; @Value("${sso.auth.path}") String authPath; @Value("${sso.client1.path}") String client1; @Value("${sso.auth.info.auth}") String userInfoPath; /** * <p>受保護資源 ,需要登陸認證</p> * * @param token 認證服務器返回的token令牌,用於從數據庫查出用戶信息(required = false, 不是必須的) * @param model Model, springMVc 設置請求域的值, 頁面通過 thymeleaf獲取值 * @param session 登陸驗證,通過驗證的才能跳轉 emps頁面展示數據,否則 重定向授權服務器的授權頁面 sso.com * @return 成功 / 失敗 跳轉展示 / 授權頁面 */ @GetMapping("/emps") public String emps(@RequestParam(value = "token", required = false) String token, Model model, HttpSession session){ // 頁面可展示資源, 這里使用 ArrayList 生成假數據 ArrayList<String> emps = new ArrayList<>(); emps.add("張三"); emps.add("李四"); // Model, springMVc 設置請求域的值, 頁面通過 thymeleaf獲取值 model.addAttribute("emps", emps); if (StringUtils.hasText(token)){ // token 存在則從遠程的認證中心獲取用戶信息 String userInfo = getUserInfo(token); // 將用戶信息渲染到頁面 model.addAttribute("userLogin", userInfo); // 從session 獲取已經登陸的信息, 有則跳轉頁面展示 return "empList"; } // 登陸驗證,通過驗證的才能跳轉 emps頁面展示數據,否則 重定向授權服務器的授權頁面 sso.com Object loginUser = session.getAttribute("loginUser"); if (loginUser != null) { if (StringUtils.hasText(token)) { // token 存在則從遠程的認證中心獲取用戶信息 String userInfo = getUserInfo(token); // 將用戶信息渲染到頁面 model.addAttribute("userLogin", userInfo); // 從session 獲取已經登陸的信息, 有則跳轉頁面展示 return "empList"; } else { return "redirect:" + authPath + "?redirect_url=" + client1; } } else { // 當前沒用需用登陸, 跳轉統一的 認證服務器, 注意:需要帶上本機客戶端的地址 // 相當於 http://sso.com:8000/auth/login.html?redirect_url=http://client1.com:8001/emps.html return "redirect:" + authPath + "?redirect_url=" + client1; } } /** * <p>遠程調用嘗試三次, 失敗后返回null</p> * @param token * @return */ private String getUserInfo(String token) { if (FLAG == 0) { return null; } RestTemplate template = new RestTemplate(); String url = userInfoPath +"?token="+token; ResponseEntity<String> response = template.getForEntity(url, String.class); if (response.getStatusCode() == HttpStatus.OK) { // 成功獲取用戶信息, return response.getBody(); } else { // 獲取失敗,重新獲取 FLAG--; getUserInfo(token); } return null; } }
-
3.1.1 客戶端的html文件
這里使用 thmeleaf 進行前端頁面渲染
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>歡迎 [[${userLogin}]]</h1>
<ul>
<li th:each="emp : ${emps}">[[${emp}]]</li>
</ul>
</body>
</html>
3.2 認證服務器端
添加 redis依賴
<!-- 引入redis緩存 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
controller 編寫
package com.sso.ssoserverauth.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author:zzz
* @data: 2022/4/1
* @product_name: SsoServer
*/
@Controller
@RequestMapping("/auth")
public class SSOAuthController {
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 所有登陸認證的首頁,在此設置重定向的客戶端地址
* <li>從瀏覽器端獲取 Cookie, 如果Cookie包含token,怎不需要登陸</li>
*
* @param url 客戶端url
* @param model 模型對象
* @param cookie 客戶端 cookie
* @return 設置客戶端的url, 返回登陸頁面
*/
@GetMapping("/login.html")
public String login(@RequestParam("redirect_url") String url,
Model model,
@CookieValue(value = "sso_token",required = false) String cookie){
if (StringUtils.hasText(cookie)){
return "redirect:"+url + "?token="+cookie;
}
// 將客戶端重定向的 url, 渲染到 login.html頁面
model.addAttribute("url",url);
return "login";
}
/**
* <p>統一認證服務器</p>
* <li>驗證登陸成功設置用戶的唯一 id</li>
* <li>驗證登陸成功設置瀏覽器 Cookie</li>
*
* @param uname 登錄名
* @param passwd 密碼
* @param url 客戶端訪問url
* @param response 相應對象
* @return 成功 / 失敗
*/
@PostMapping("/doLogin")
public String doLogin(@RequestParam("uname") String uname,
@RequestParam("passwd") String passwd,
@RequestParam("url") String url,
HttpServletResponse response){
// 這里驗證用戶名密碼驗證做最簡單的 非空判斷; 不為空,即登陸成功
if (StringUtils.hasText(uname) && StringUtils.hasText(passwd)){
// 登陸成功之后首先給當前登陸用戶 生成唯一的id
String ssoToken = UUID.randomUUID().toString().replace("-", "");
// 將用戶信息,根據唯一id放入 redis (這里只簡單放入用戶名)
stringRedisTemplate.opsForValue().set(ssoToken, uname);
// 給瀏覽器設置 Cookie, 將 token 設置到 cookie中
Cookie cookie = new Cookie("sso_token", ssoToken);
response.addCookie(cookie);
return "redirect:"+ url +"?token=" + ssoToken;
}
// 失敗 返回登陸頁,帶上客戶端地址
return "redirect:login?redirect_url="+url;
}
/**
* 遠程調用,獲取用戶信息
*
* @param token 用戶唯一標識
* @return 用戶信息
*/
@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token){
String userInfo = stringRedisTemplate.opsForValue().get(token);
// 根據客戶端提供的 token 獲取用戶信息
if (StringUtils.hasText(userInfo)){
return userInfo;
}
return "";
}
}
3.2.1 認證服務器頁面
登陸頁面 攜帶 hidden 隱藏的 客戶端url。這里參照 controller 的login方法。攜帶重定向的 url發給認證服務器。這樣 服務器處理完登陸結果,也知道把信息返回給哪個服務器的地址
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" th:action="@{/auth/doLogin}">
用戶名: <input type="text" name="uname"><br/>
密 碼: <input type="password" name="passwd"><br/>
<input type="hidden" name="url" th:value="${url}">
<button type="submit" value="登陸">登陸</button>
</form>
</body>
</html>