前言
很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影、豆瓣日记…真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?
接下来就实现单点登录的功能。
流程图
实现
开源sso的项目:https://gitee.com/xuxueli0323/xxl-sso
流程
- 给登录服务器留下痕迹
- 登录服务器要将token信息重定向的时候,带到url地址上
- 其他系统要处理url地址上的token,只要有将token对应的用户保存到自己的session
- 自己系统将用户保存在自己的session中
server:登录服务器、8080 、sso.com
web-sample1:项目1 、8081 、client1.com
web-sample2:项目1 、8082 、client2.com
假设发送这一个请求 sso.com:8080/login.html?redirect_url=http://client1.com:8081/employees
登录服务器sso.com
@GetMapping("/login.html")
//正常来说是调用服务而不是在参数填写返回地址的,为了图方便就这样了
public String loginPage(@RequestParam("redirect_url") String url, Model model, @CookieValue(value = "sso_token", required = false) String sso_token
//获取浏览器session信息) {
//判断浏览器是否有登录痕迹
if (!StringUtils.isEmpty(sso_token)) {
return "redirect:" + url + "?token=" + sso_token;
}
//无痕迹返回
model.addAttribute("url", url);
return "login";
}
@PostMapping(value = "/doLogin")
public String doLogin(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam("redirect_url") String url, HttpServletResponse response) {
//登录成功跳转
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
String uuid = UUID.randomUUID().toString().replace("_", "");
//保存痕迹
redisTemplate.opsForValue().set(uuid, username);
//保存痕迹的坐标
Cookie sso_token = new Cookie("sso_token", uuid);
response.addCookie(sso_token);
//登录成功跳转新地址带上保存痕迹的坐标
return "redirect:" + url + "?token=" + uuid;
}
//登录失败跳回到登录页
return "login";
}
@ResponseBody
@GetMapping("/userinfo") 获取保存的痕迹
public String userinfo(@RequestParam(value = "token") String token) {
//获取保存的痕迹
String s = redisTemplate.opsForValue().get(token);
return s;
}
登录页
<form action="/doLogin" method="post">
用户名:<input type="text" name="username" /><br />
密码:<input type="password" name="password" /><br />
<!--无痕迹登录所携带地址-->
<input type="hidden" name="redirect_url" th:value="${url}" />
<input type="submit" value="登录">
</form>
客户端client1.com
@GetMapping(value = "/employees")
public String employees(Model model, HttpSession session, @RequestParam(value = "token", required = false) String token) {
//拥有痕迹
if (!StringUtils.isEmpty(token)) {
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);
}
Object loginUser = session.getAttribute("loginUser");
判断痕迹是否存在
if (loginUser == null) {
//没有就跳回登录页面
return "redirect:" + "http://sso.com:8080/login.html"+"?redirect_url=http://localhost:8081/employees";
} else {
List<String> emps = new ArrayList<>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "employees";
}
}
成功页面
<!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>
以上流程走通以后已经给浏览器留下了痕迹
就算再来一个客户端来访问也会因为登录服务器的判断而直接去往成功页面,实现了单次登录的效果!!!
//判断浏览器是否有登录痕迹
if (!StringUtils.isEmpty(sso_token)) {
return "redirect:" + url + "?token=" + sso_token;
}
//无痕迹返回
model.addAttribute("url", url);
return "login";
}
通过session
和类似令牌机制来实现这功能的思想确实很牛啊,当然大型业务就没那么简单了…