分布式单点登陆解决方案
一. 简介
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,包含单点登录和单点注销两部分
相比于单系统登录,sso需要一个专门的登录中心,多个系统应用中的登录都在登陆中心实现,sso登录中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,令牌作为参数发送给各个子系统,子系统拿到令牌,可以借此创建局部会话,局部会话登录方式与单系统的方式相同
二. 二级域名实现
三. 多个不同域名实现
对于二级域名的单点登录,我们可以非常方便的通过共享cookie来实现,简单的说,就是增加cookie的作用域,这种方式不涉及跨域,当cookie的作用域设置为顶级域名之后,所有的二级域名都可以访问到身份验证的cookie,在服务器端只要验证了这个cookie就可以实现身份的验证
对于多个域名,这个时候就不能共享cookie了,所以上面的解决方案就会失效。那么,要实现跨域的单点登录该如何做呢?
流程图:
2.1 登陆系统实现
SSO-Server:主要负责用户登录、注销、为SSO-Client分配token、验证token的工作
LoginController :
@Controller
public class LoginController {
// 没有redis的序列化配置,这里使用 StringRedisTemplate
@Autowired
private StringRedisTemplate redisTemplate;
@ResponseBody
@GetMapping("/getLoginUserInfo")
public String userInfo(@RequestParam("token") String token){
Object s = redisTemplate.opsForValue().get(token);
return s.toString();
}
@GetMapping("/login.html")
public String loginPage(@RequestParam(value = "url",required = true) String url,
HttpServletRequest request,
Model model){
Cookie[] cookies = request.getCookies();
if (null != cookies) {
for(Cookie cookie : cookies){
// 判断cookie中有没有用户登陆信息
if (cookie.getName().equals("login_user_info") && null != redisTemplate.opsForValue().get(cookie.getValue())) {
return "redirect:" + url + "?token=" + cookie.getValue();
}
}
}
// 没有登陆就跳到登陆页
model.addAttribute("url",url);
return "login";
}
@PostMapping("/doLogin")
public String doLogin(@RequestParam(value = "username",required = true) String username,
@RequestParam(value = "password",required = true) String password,
@RequestParam(value = "url",required = true) String url,
HttpServletResponse response) {
// get username and password form dataBase ...
// 从数据库获取到用户信息 校验,登陆失败就返回登陆页面,在这里就不做判断了
String token = UUID.randomUUID().toString().replace("-", "");
response.addCookie(new Cookie("login_user_info", token));
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("username",username);
userInfo.put("password","password");
userInfo.put("otherInfo","otherInfo");
redisTemplate.opsForValue().set(token, userInfo.toString(), 60L, TimeUnit.MINUTES);
return "redirect:" + url + "?token=" + token;
}
}
login.html:
<form action="/doLogin" method="post">
<input type="hidden" name="url" th:value="${url}">
用户名:<input name="username"><br/>
密码:<input name="password" type="password">
<input type="submit" value="登录">
</form>
2.2 客户端实现
@Controller
public class IndexController {
// 没有redis的序列化配置,这里使用 StringRedisTemplate
@Autowired
private StringRedisTemplate redisTemplate;
@RequestMapping(value = {
"index.html","/"})
public String index(Model model) {
model.addAttribute("userInfo",null);
return "index";
}
@RequestMapping("login_callback")
public String loginCallback(@RequestParam(value = "token",required = true) String token,
HttpSession session) {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> entity = restTemplate.getForEntity("http://ssoserver.com:2000/getLoginUserInfo?token=" + token, String.class);
String body = entity.getBody();
if (!StringUtils.isEmpty(body)) {
Map<Object, Object> userInfoMap = new HashMap<>();
userInfoMap.put("username","haiyang");
session.setAttribute("userInfo",userInfoMap);
}
return "redirect:http://ssoclient1.com:2001";
}
}
index.html:
<a th:if="${session.userInfo != null}">欢迎:[[${session.userInfo == null ? '' : session.userInfo.username}]]</a>
<a href="http://ssoserver.com:2000/login.html?url=http://ssoclient1.com:2001/login_callback" th:if="${session.userInfo == null}">去登录</a>