单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的。
之前的文章有记录过使用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)
1、properties配置文件
server.port=8080
# 这里将用户信息存储在Redis中
spring.redis.host=192.168.200.134
spring.redis.port=6379
2、创建一个login.html页面(Thymeleaf模板引擎)
<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";
}
}
}
1、properties配置文件
server.port=8081
# 中央认证的地址
sso.server.url=http://sso.com:8080/login.html
2、创建一个受保护的资源页面list.html(Thymeleaf模板引擎)
<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";
}
}
}
直接复制上面这个服务即可,改一下请求路径、端口即可。
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,表明已经登录过。