知识前提
- 对分布式的场景有一定的了解
- 对网络域名有了解,这里需要知道一级或顶级域名不同的网址是不能共享cookie的
环境构建
这里我们创建三个springboot项目,均带上动态模板thymeleaf方便我们测试
1、首先我们修改本地的host文件,模拟三个机器
2、创建三个springboot项目
分别为sso-server(认证服务端)、sso-client(客户端1)、sso-client2(客户端2)
三个项目都需要spring-boot-starter-web和spring-boot-starter-thymeleaf依赖
其中认证服务端另外需要redis的依赖
2.1、创建三个页面
认证服务端
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="/doLogin" method="post">
<label>
用户名:
<input name="username"/>
</label><br/>
<label>
密码:
<input name="password" type="password"/>
</label><br/>
<input type="hidden" name="url" th:value="${url}"/>
<input type="submit" value="登录">
</form>
</body>
</html>
客户端1
<!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>
客户端2
<!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>
2.2、认证服务端配置
2.2.1、配置文件
# 配置redis
spring:
redis:
host: 127.0.0.1
2.2.2、认证服务端的登录处理
package com.athuigu.gulimall.ssoserver.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author wangyang
*/
@Controller
public class LoginController {
@Autowired
StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/userInfo")
public String userInfo(@RequestParam("token") String token) {
return redisTemplate.opsForValue().get(token);
}
@GetMapping("/login.html")
public String loginPage(@RequestParam("redirect_url") String url, Model model,
@CookieValue(value = "sso_token", required = false) String sso_token) {
if (!StringUtils.isEmpty(sso_token)) {
// 说明之前在别的系统已经登录过,浏览器留下了痕迹
return "redirect:" + url + "?token=" + sso_token;
}
model.addAttribute("url", url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam("username") String username,
@RequestParam("password") String password,
@RequestParam("url") String url,
HttpServletResponse response) {
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(password)) {
//登陆成功跳转,调回之前的页面;
// 把登录成功的用户保存起来
String uuid = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(uuid, username);
Cookie cookie = new Cookie("sso_token", uuid);
response.addCookie(cookie);
return "redirect:" + url + "?token=" + uuid;
}
return "login";
}
}
2.3、客户端配置
2.3.1、配置文件
# 这里配置跳转的认证服务器地址
sso:
server:
url: http://ssoserver.com:8080/login.html
2.3.2、客户端登录配置类
package com.atguigu.gulimall.ssoclient.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import org.thymeleaf.util.StringUtils;
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
/**
* @author wangyang
*/
@Controller
public class HelloController {
@Value("${sso.server.url}")
String ssoServerUrl;
/**
* 无需登录
*
* @return String
*/
@ResponseBody
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/employees")
public String employees(Model model, HttpSession session, @RequestParam(value = "token", required = false) String token) {
if (!StringUtils.isEmpty(token)) {
// 去认真服务器登录成功跳转回来的会带上token
// 去认真服务器获取当前token真正对应的用户信息
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> forEntity = restTemplate.getForEntity("http://ssoserver.com:8080/userInfo?token=" + token, String.class);
String body = forEntity.getBody();
session.setAttribute("loginUser", body);
}
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<String>();
emps.add("张三");
emps.add("李四");
model.addAttribute("emps", emps);
return "list";
}
}
}
流程说明
- 我们登录客户端1,首先我们先判断是否在其他客户端上已经有登陆过,也就是是否携带了token,如果没有携带,那么我们认定这是第一次登录,我们需要跳转到认证服务器的登录页面进行第一次登录,这次登录的时候我们需要将我们登录后返回的页面作为参数一并传递给认证服务器端
- 认证服务器端接收到请求,首先进行判断其他客户端是否登陆过,也就是sso_token是否为空,如果不为空我们就拿出客户端传递过来的地址拼接上sso_token参数进行跳转,客户端重复上一步操作,这次有token,我们就直接登录成功。如果为空,我们就跳转到认证服务端的登录界面,进行登录的时候,需要将用户数据存入redis中,redis的格式为(uuid,username),然后new一个Cookie(“sso_token”,uuid),最后将uuid作为token拼接到客户端携带过来的地址,并进行重定向返回。
- 重定向再次来到客户端,这次token不为空,然后通过访问认证服务的userInfo接口拿到用户信息,成功登录页面。