单点登录(Single Sign On, SSO)是指在同一帐号平台下的多个应用系统中,用户只需登录一次,即可访问所有相互信任的应用系统。Cookie 的作用域由 domain 属性和 path 属性共同决定。在 Tomcat 中,domain 属性默认为当前域的域名/IP地址。path 属性的有效值是以"/"开头的路径,在 Tomcat 中,path 属性默认为当前 Web 应用的上下文路径。如果将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。Cookie 有一个特点,即父域中的 Cookie 被子域所共享,换言之,子域会自动继承父域中的Cookie。利用 Cookie 的这个特点,不难想到,将 Session ID(或 Token)保存到父域中不就行了。没错,我们只需要将 Cookie 的 domain 属性设置为父域的域名(主域名),同时将 Cookie 的 path 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了。不过这要求应用系统的域名需建立在一个共同的主域名之下,如 http://tieba.baidu.com 和 http://map.baidu.com,它们都建立在 http://baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。
配置host文件
项目结构
引入依赖
父项目引入依赖,子项目无依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.6.7</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
login系统
前端部分
login.html页面,负责显示登录页面并提交登录
html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Login Module</title>
</head>
<body>
<h1>用户登录 </h1>
<p style="color: red;" th:text="${session.msg}"></p>
<form action="/login" method="POST">
姓名: <input name="username" value="">
<br></br>
密码: <input name="password" value="">
<br></br>
<button type="submit" style="width:60px;height:30px"> login </button>
</form>
</body>
</html>
后端部分
跳转登录页面
java
import com.sso.login.pojo.User;
import com.sso.login.utils.LogCacheUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/view")
public class ViewController {
/**
* 跳转到登录页面
* @return
*/
@GetMapping("/login")
public String toLogin(@RequestParam(required = false, defaultValue = "") String target,
HttpSession session,
@CookieValue(required = false, value = "TOKEN") Cookie cookie){
if(StringUtils.isEmpty(target)){
target = "login.codeshop.com:9010";
}
if(cookie != null){
// 从父域拿到token
String value = cookie.getValue();
User user = LogCacheUtil.loginUser.get(value);
if(user != null){
return "redirect:" + target;
}
}
session.setAttribute("target",target);
return "login";
}
}
负责登录校验以及解析token
java
import com.sso.login.pojo.User;
import com.sso.login.utils.LogCacheUtil;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@Controller
@RequestMapping("/login")
public class LoginController {
private static Set<User> dbUser;
// 模拟数据库
static {
dbUser = new HashSet<>();
dbUser.add(new User(1, "zxb", "1234567"));
dbUser.add(new User(2, "admin", "123456"));
}
@PostMapping
public String doLogin(User user, HttpSession session, HttpServletResponse response) {
String target = (String) session.getAttribute("target");
// 验证登录
Optional<User> first = dbUser.stream().filter(dbUser -> dbUser.getUsername().equals(user.getUsername()) &&
dbUser.getPassword().equals(user.getPassword()))
.findFirst();
if (first.isPresent()) {
String token = UUID.randomUUID().toString();
// 保存token到父域 Cookie
Cookie cookie = new Cookie("TOKEN", token);
cookie.setPath("/");
cookie.setDomain("codeshop.com");
response.addCookie(cookie);
// 登录成功保存到LogCacheUtil工具类中,这个部分可以用redis去实现
LogCacheUtil.loginUser.put(token, first.get());
} else {
session.setAttribute("msg", "用户名或密码错误!");
// 错误跳转到登录页面
return "login";
}
// 重定向到目标地址
return "redirect:" + target;
}
@GetMapping("info")
@ResponseBody
public ResponseEntity<User> getUserInfo(String token) {
if (!StringUtils.isEmpty(token)) {
// 验证token并从token获取用户信息并返回
User user = LogCacheUtil.loginUser.get(token);
return ResponseEntity.ok(user);
} else {
return new ResponseEntity<>(null, HttpStatus.BAD_REQUEST);
}
}
@GetMapping("/logout")
public String loginOut(@CookieValue(value = "TOKEN") Cookie cookie, HttpServletResponse response, String target) {
cookie.setMaxAge(0);
LogCacheUtil.loginUser.remove(cookie.getValue());
response.addCookie(cookie);
return "redirect:" + target;
}
}
model
java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Data //添加getter/setter
@NoArgsConstructor //添加无参构造器
@AllArgsConstructor //添加全参构造器
@Accessors(chain = true) //添加链式调用
public class User {
private Integer id;
private String username;
private String password;
}
保存User工具类
java
import com.sso.login.pojo.User;
import java.util.HashMap;
import java.util.Map;
public class LogCacheUtil {
public static Map<String, User> loginUser = new HashMap<>();
}
Main系统
前端部分
index.html页面
java
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Index Module</title>
</head>
<body>
<h1>欢迎登录!</h1>
<span>
<a th:if="${session.loginUser == null}" href="http://login.codeshop.com:9000/view/login?target=http://login.codeshop.com:9010/view/index">login</a>
<a th:unless="${session.loginUser == null}" href="http://login.codeshop.com:9000/login/logout?target=http://www.codeshop.com:9010/view/index">logout</a>
</span>
<p th:unless="${session.loginUser == null}">
<span style="color:deepskyblue" th:text="${session.loginUser.username}"></span> 已登录!<br></br>
<br></br>
id: <span style="color:deepskyblue" th:text="${session.loginUser.id}"></span><br></br>
username: <span style="color:deepskyblue" th:text="${session.loginUser.username}"></span><br></br>
</p>
</body>
</html>
后端部分
Viewcontroller
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import java.util.Map;
@Controller
@RequestMapping("/view")
public class ViewController {
@Autowired
private RestTemplate restTemplate;
private final String USER_INFO_ADDRESS = "http://login.codeshop.com:9000/login/info?token=";
@GetMapping("/index")
public String toIndex(@CookieValue(required = false, value = "TOKEN") Cookie cookie,
HttpSession session) {
if (cookie != null) {
String token = cookie.getValue();
if (!StringUtils.isEmpty(token)) {
Map result = restTemplate.getForObject(USER_INFO_ADDRESS + token, Map.class);
session.setAttribute("loginUser", result);
}
}
return "index";
}
}
配置restTemplate的HTTP客户端工具
java
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
cart系统
前端部分
index.html页面
java
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Blog Module</title>
</head>
<body>
<h1>登录页面 !</h1>
<span>
<a th:if="${session.loginUser == null}" href="http://login.codeshop.com:9000/view/login?target=http://cart.codeshop.com:9012/view/index">login</a>
<a th:unless="${session.loginUser == null}" href="http://login.codeshop.com:9000/login/logout?target=http://cart.codeshop.com:9012/view/index">logout</a>
</span>
<p th:unless="${session.loginUser == null}">
<span style="color:deeppink" th:text="${session.loginUser.username}"></span> 已登录!<br></br>
<br></br>
id: <span style="color:deeppink" th:text="${session.loginUser.id}"></span><br></br>
username: <span style="color:deeppink" th:text="${session.loginUser.username}"></span><br></br>
</p>
</body>
</html>
后端部分
view的controller
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.RestTemplate;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import java.util.Map;
@Controller
@RequestMapping("/view")
public class ViewController {
@Autowired
private RestTemplate restTemplate;
private final String USER_INFO_ADDRESS = "http://login.codeshop.com:9000/login/info?token=";
@GetMapping("/index")
public String toIndex(@CookieValue(required = false, value = "TOKEN") Cookie cookie,
HttpSession session){
if(cookie != null){
// 从cookie拿到的token去请求获取用户信息需要去实现验证
String token = cookie.getValue();
if(!StringUtils.isEmpty(token)){
Map result = restTemplate.getForObject(USER_INFO_ADDRESS+token,Map.class);
session.setAttribute("loginUser",result);
}
}
return "index";
}
}
配置restTemplate的HTTP客户端工具
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class CartApp {
public static void main(String[] args) {
SpringApplication.run(CartApp.class,args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
测试
启动
访问 mainApp
登录
登录成功