单点登录,就是在一个系统登录后,在它的关联系统也不用重新登录了。例如:你成功登录了淘宝,那么在天猫也就成功登录了;同理,退出也是一样的。在天猫退出账号,在淘宝的账号也就退出了。(咳咳,同一浏览器内)
实现单点登录,主要就是利用同域名传递 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(cookie != null) {
String value = cookie.getValue();
User user = LoginCacheUtil.loginUser.get(value);
if(user != null) {
return "redirect:" + target;
}
}
// 用于登录成功后重定向地址
session.setAttribute("target", target);
return "login";
}
/**
* 用户直接访问登录页
* @return
*/
@RequestMapping("/index")
public String loginIndex() {
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");
//如果是直接从登录系统登录的,校验成功后默认跳转到主系统 sys1 的首页
if(StringUtils.isEmpty(targetUrl)) {
targetUrl = "http://sys1.sso.com:8081/index";
}
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
当在其中一个系统登录成功时,刷新另外一个系统,会发现也已经登录了。
当在其中一个系统退出后,刷新另外一个系统,会发现也已经退出了。