什麼是單點登錄
單點登錄(SingleSignOn,SSO),就是通過用戶的一次性鑑別登錄。當用戶在身份認證服務器上登錄一次以後,即可獲得訪問單點登錄系統中其他關聯繫統和應用軟件的權限,同時這種實現是不需要管理員對用戶的登錄狀態或其他信息進行修改的,這意味着在多個應用系統中,用戶只需一次登錄就可以訪問所有相互信任的應用系統。這種方式減少了由登錄產生的時間消耗,輔助了用戶管理,是目前比較流行的。
cookie+session實現單點登錄
之前的文章有記錄過使用CAS開源項目來實現單點登錄,也有通過JWT來實現單點登錄的。本文是通過cookie和session來實現單點登錄,參考許雪裏的SSO解決方案:許雪裏SSO碼雲地址
本文基於cookie、session、SpringBoot、redis進行,其中redis只是做一個簡單的存儲功能,因此搭建項目只需搭建一個SpringBoot項目並引入Thymeleaf和SpringDataRedis的依賴即可。
一、服務劃分
分爲3個服務:1個登錄服務器,2個客戶端服務器。
/xxl-sso-server 登錄服務器 8080 sso.com
/xxl-sso-web-sample-springboot 項目一 8081 client1.com
/xxl-sso-web-sample-springboot 項目二 8083 client2.com
在本地修改host文件,對不同服務進行區分:
實現核心:三個系統即使域名不一樣,想辦法給三個系統同步同一個用戶的票據。
1、中央認證服務器:sso.com
2、其他系統想要登錄,就要去sso.com登錄,登錄成功跳轉回來
3、只要有一個登錄,其他都不用登錄
4、所有系統可能域名都不一樣,但是所有的系統都統一使用一個cookie(保存sso-sessionid)
簡易時序圖
二、項目搭建及實現
①創建一箇中央認證服務(http:sso.com:8080)
1、properties配置文件
server.port=8080
# 這裏將用戶信息存儲在Redis中
spring.redis.host=192.168.200.134
spring.redis.port=6379
2、創建一個login.html頁面(Thymeleaf模板引擎)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄頁</title>
</head>
<body>
<form action="/doLogin" method="post">
用戶名:<input type="text" name="username"><br>
密 碼:<input type="password" name="password"><br>
<!-- 隱藏域用來保存登錄成功之後回調的地址 - 即從哪個頁面來,就要回到哪個頁面去 -->
<input type="hidden" name="url" th:value="${url != null?url:''}">
<input type="submit" value="登錄" style="margin-left: 190px">
</form>
</body>
</html>
3、創建LoginController來處理認證相關的請求
@Controller
public class LoginController {
// 注入RedisTemplate
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 通過傳過來的令牌,獲取用戶信息
*/
@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token){
// 從Redis中,通過token獲取用戶信息
String username = redisTemplate.opsForValue().get(token);
return username;
}
/**
* 登錄頁面
* @param url 這個URL就是重定向頁面,從哪個頁面來,最後要回到哪個頁面
* @param sso_token Cookie中存儲的sso令牌
*/
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url, Model model, @CookieValue(value = "sso_token",required = false) String sso_token){
// 判斷Cookie中是否保存了令牌
// 有令牌表明之前已經登錄過了,直接回到之前的頁面並帶上令牌信息
if(!StringUtils.isEmpty(sso_token)){
return "redirect:" + url + "?token=" + sso_token;
}
model.addAttribute("url",url);
return "login";
}
/**
* 處理登錄請求
* @param username 賬號
* @param password 密碼
* @param url 這個URL就是重定向頁面,從哪個頁面來,最後要回到哪個頁面
*/
@PostMapping("/doLogin")
public String doLogin(String username, String password, String url, HttpServletResponse response){
// 這裏模擬登錄成功,只要賬號和密碼不爲空,即登陸成功
if(!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)){
// 使用UUID創建一個令牌
String uuid = UUID.randomUUID().toString().replace("-","");
// Redis中以令牌爲key,用戶名爲value進行存儲
redisTemplate.opsForValue().set(uuid,username);
// 同時將令牌保存到Cookie中
Cookie sso_token = new Cookie("sso_token",uuid);
response.addCookie(sso_token);
// 回到之前的頁面並帶上令牌信息
return "redirect:" + url + "?token=" + uuid;
}else{
// 登錄失敗,跳轉到登錄頁面
return "login";
}
}
}
②創建一個客戶端服務(client1.com:8081)
1、properties配置文件
server.port=8081
# 中央認證的地址
sso.server.url=http://sso.com:8080/login.html
2、創建一個受保護的資源頁面list.html(Thymeleaf模板引擎)
<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>員工列表頁</title>
</head>
<body>
<!-- 登錄信息 -->
<h1>歡迎:[[${session.loginUser}]]</h1>
<!-- 資源信息 -->
<ul>
<li th:each="emp : ${emps}">姓名:[[${emp}]]</li>
</ul>
</body>
</html>
3、創建一個受保護的資源請求處理器(HelloController)
@Controller
public class HelloController {
// 認證中心的地址
@Value("${sso.server.url}")
private String ssoServerUrl;
/**
* 無需登錄即可訪問
*/
@GetMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}
/**
* 模擬獲取員工列表 - 需要登錄之後才能獲取
*/
@GetMapping("/employees")
public String employees(Model model, HttpSession session, @RequestParam(value = "token",required = false) String token){
// 判斷是否帶有token
// 因爲認證之後,會跳到這個請求,如果帶了token,就說明登錄成功了的
if(!StringUtils.isEmpty(token)){
// 登錄成功,獲取用戶信息
// 通過RestTemplate獲取,也可以通過Feign客戶端
// 但是如果認證服務器是其他語言(比如PHP)寫的,就沒辦法通過Feign了
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://sso.com:8080/userInfo?token=" + token, String.class);
String body = forEntity.getBody();
// 將用戶信息存入session
session.setAttribute("loginUser",body);
}
// 從session中查詢是否有用戶登錄了
Object loginUser = session.getAttribute("loginUser");
if(loginUser == null){
// 沒有登錄,跳轉到登錄服務器進行登錄
// 使用url上的查詢參數標識我們自己是哪個頁面
return "redirect:" + ssoServerUrl + "?redirect_url=http://client1.com:8081/employees";
}else{
// 登錄成功的模擬數據
List<String> emps = new ArrayList<>();
emps.add("柳成蔭");
emps.add("九月清晨");
model.addAttribute("emps",emps);
return "list";
}
}
}
③創建一個客戶端服務(client2.com:8083)
直接複製上面這個服務即可,改一下請求路徑、端口即可。
④測試
1、訪問受保護的資源(http://client1.com:8081/employees)
因爲沒有登錄,跳轉到認證中心。
2、輸入賬號密碼進行登錄
認證成功,直接跳轉到資源頁,可以看到確實帶了一個token
查看Redis中是否保存了數據:
3、去認證中心看Cookie是否保存了一個令牌(http://sso.com:8080/)
可以看到確實保存了一個名爲sso_token
的令牌
4、訪問另一個服務的受保護的資源(http://client2.com:8083/boss)
這個服務就是複製的第一個的服務,只是把請求路徑就修改成了boss進行區分,其他都一樣。
因爲第一個服務登錄成功了,第二個服務訪問這個資源,這個資源判定沒有登錄,就去中央認證中心,發現cookie中存儲了一個token,說明之前有人登錄過,因此直接返回token,表明已經登錄過。
代碼流程圖
1、第一個去認證的服務
2、第二個去認證的服務