單點登錄,就是在一個系統登錄後,在它的關聯繫統也不用重新登錄了。例如:你成功登錄了淘寶,那麼在天貓也就成功登錄了;同理,退出也是一樣的。在天貓退出賬號,在淘寶的賬號也就退出了。(咳咳,同一瀏覽器內)
實現單點登錄,主要就是利用同域名傳遞 cookie 中的登錄用戶信息。以下是一種實現方式,僅供參考!
準備工作
1)
系統:win10
IDE:sts4
springboot2.2.4.RELEASE、 jdk8、 maven3.3.9
在本例中,創建了4個 springboot 項目,一個專用來處理登錄(login.sso.com),其他都是關聯繫統,把 sys1.sso.com 當做是主系統。
如果用戶首先在 sys1 系統的首頁瀏覽,此時點擊登錄,則跳轉到登錄系統處理登錄邏輯。登錄成功後,會跳轉到 sys1 系統的首頁,並且是已登錄狀態。
同理用戶在 sys2 系統的首頁瀏覽,此時點擊登錄,則跳轉到登錄系統處理登錄邏輯。登錄成功後,會跳轉到 sys2 系統的首頁,並且是已登錄狀態。
那如果一開始就是沒有在其他系統瀏覽,而是直接到登錄系統進行登錄呢。那這種情況登錄成功後,本例會跳轉到 sys1 的首頁,即設定 sys1 爲主系統的意思
2)域名映射
修改 hosts 文件,路徑:C:\Windows\System32\drivers\etc
登錄系統
登錄系統只有一個登錄界面和處理登錄信息的邏輯
1)pom 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
沒有使用數據庫,用戶信息是寫死的,所以只需要這兩個依賴就夠了。
2)登錄界面 login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>sso-login</title>
</head>
<body>
<h3>這裏是登錄頁面</h3>
<p style="color:red;" th:text="${session.msg}"></p>
<form action="/login" method="post">
用戶名:<input type="text" name="uname"/><br/>
密碼:<input type="password" name="upwd"/><br/>
<button type="submit">登錄</button>
</form>
</body>
</html>
3)控制層及其他
public class User {
private String uname;
private String upwd;
public User(String uname, String upwd) {
super();
this.uname = uname;
this.upwd = upwd;
}
public String getUname() {
return uname;
}
public void setUname(String uname) {
this.uname = uname;
}
public String getUpwd() {
return upwd;
}
public void setUpwd(String upwd) {
this.upwd = upwd;
}
}
/**
* 頁面跳轉控制
* @author Xiaogf
*/
@Controller
@RequestMapping("/page")
public class PageController {
/**
* 跳轉到登錄頁
* @return
*/
@RequestMapping("/login")
public String toLogin(@RequestParam(required = false, defaultValue = "")String target,
HttpSession session, @CookieValue(required = false, value = "TOKEN")Cookie cookie) {
if(StringUtils.isEmpty(target)) {
//如果是直接從登錄系統登錄的,校驗成功後默認跳轉到主系統 sys1 的首頁
target = "http://sys1.sso.com:8080/index";
}
// 如果是已登錄的用戶再次訪問登錄頁面,就要重定向
if(cookie != null) {
String value = cookie.getValue();
User user = LoginCacheUtil.loginUser.get(value);
if(user != null) {
return "redirect:" + target;
}
}
// 用於登錄成功後重定向地址
session.setAttribute("target", target);
return "login";
}
}
@Controller
public class LoginController {
@PostMapping("/login")
public String doLogin(User user, HttpSession session, HttpServletResponse response) {
// 校驗用戶名密碼
if(user.getUname().equals("xiao")&&user.getUpwd().equals("123")) {
// 保存用戶登錄信息
String token = UUID.randomUUID().toString();
System.out.println("login.token===" + token);
Cookie cookie = new Cookie("TOKEN", token);
//設置域名,實現數據共享
cookie.setDomain("sso.com");
// 把cookie寫到客戶端
response.addCookie(cookie);
LoginCacheUtil.loginUser.put(token, user);
}else {
session.setAttribute("msg", "用戶名或密碼錯誤");
return "login";
}
//登錄信息校驗成功,重定向到原來的系統
String targetUrl = (String) session.getAttribute("target");
return "redirect:" + targetUrl;
}
/**
* 其他同域名的系統通過該接口獲取登錄用戶的信息
* @param token
* @return
*/
@RequestMapping("/info")
@ResponseBody
public ResponseEntity<User> getUserInfo(String token){
if(!StringUtils.isEmpty(token)) {
User user = LoginCacheUtil.loginUser.get(token);
return ResponseEntity.ok(user);
}else {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
}
}
public class LoginCacheUtil {
//模擬系統的數據緩存,如 Redis 之類的
public static Map<String, User> loginUser = new HashMap<>();
}
子系統
本例中 sys1,sys2,sys3 這幾個系統的頁面和登錄處理邏輯都是一樣的,注意地址和端口改改就行。這裏的 login 是 8080,sys1 是 8081,sys2 是 8082,sys3 是 8083
1)pom 文件同上
2)系統首頁界面 index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>sso-sys1</title>
</head>
<body>
<h1>這裏是 sso-sys1 首頁</h1>
<p th:unless="${session.loginUser==null}">
<span style="color:deepskyblue;" th:text="${session.loginUser.uname}"></span> 已登錄
</p>
<a th:if="${session.loginUser==null}" href="http://login.sso.com:8080/page/login?target=http://sys1.sso.com:8081/index">登錄</a>
<a th:unless="${session.loginUser==null}" href="/loginOut">退出</a>
</body>
</html>
登錄的路徑中,需要攜帶本系統的地址,以便登錄成功後可以跳轉回本系統首頁
3)控制層
@Controller
public class Sys1Controller {
@Autowired
private RestTemplate restTemplate;
//獲取登錄用戶信息的 URL,就是訪問登錄系統的 info 接口
private final String LOGIN_INFO_URL="http://login.sso.com:8080/info?token=";
@RequestMapping("/index")
public String toIndex(@CookieValue(required = false, value = "TOKEN")Cookie cookie, HttpSession session) {
if(cookie != null) {
//獲取cookie 中 TOKEN 的值
String token = cookie.getValue();
System.out.println("sys1.token===" + token);
if(!StringUtils.isEmpty(token)) {
//如果 TOKEN 中有值,則發送請求獲取登錄用戶的信息,並存到該系統的 session 緩存中
Map object = restTemplate.getForObject(LOGIN_INFO_URL + token, Map.class);
System.out.println("sys1.object===" + object);
session.setAttribute("loginUser", object);
}else {
//如果 TOKEN 沒有值,代表沒有登錄或者已經退出,應該清除緩存中的登錄用戶信息
session.setAttribute("loginUser", null);
}
}
return "index";
}
@RequestMapping("/loginOut")
public String loginOut(HttpSession session, HttpServletResponse response) {
//清除session中用戶登錄信息
session.setAttribute("loginUser", null);
//清除cookie中 TOKEN 的值
Cookie cookie = new Cookie("TOKEN", "null");
cookie.setDomain("sso.com");
response.addCookie(cookie);
//重定向到該系統的首頁
return "redirect:/index";
}
}
4)啓動類需要加入一個 Bean
@SpringBootApplication
public class SsoSys1Application {
public static void main(String[] args) {
SpringApplication.run(SsoSys1Application.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
sys2 與 sys3 的 index.html 中地址和端口需要稍微改一下,其他一樣,這裏就不重複貼出來了。
測試
啓動登錄系統、sys1系統,sys2 系統和 sys3 系統,分別在不同標籤頁訪問 sys1.sso.com:8081/index 、sys2.sso.com:8082/index 、sys3.sso.com:8083/index ,
點擊登錄,都跳轉到了登錄系統 login.sso.com
當在其中一個系統登錄成功時,刷新另外一個系統,會發現也已經登錄了。
當在其中一個系統退出後,刷新另外一個系統,會發現也已經退出了。