在上一篇文章中我们讲述了单系统如何使用cookie+session来实现登录的案例。但是在大一点的公司里面,往往需要实现多个系统之间的SSO,那么这又该如何实现呢?
关于SSO的知识点本文不再赘述,本文假设的场景是公司里面有多个网站:
- doc.zhangxun.com
- code.zhangxun.com
这些网站的一级域名都是相同的,仅仅是二级域名不同。而且各个系统独立部署,运行在不同的tomcat中,现在需要用户在公司任意一个系统完成登录后,在一定时间内,访问公司其它网站都不再需要登录,从而实现SSO。
问题一:Cookie的跨域
由于浏览器的同源策略限制,我们都知道Cookie是不能跨域的,即www.google.com的cookie是不能被www.baidu.com使用的。但是我们的场景是同一个公司内不同的系统,他们的一级域名是相同的,二级域名不同,这种情况下,cookie能够共用吗?我们可以来试验一下。
首先,我们修改hosts文件,位于C:\Windows\System32\drivers\etc
目录下,从而创建如上两个网站。
127.0.0.1 doc.zhangxun.com
127.0.0.1 code.zhangxun.com
然后,我们参考上一篇文章,分别创建两个不同的模块,分别为doc和code。它们的如下代码完全一样。
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
@Slf4j
@Controller
public class LoginController {
@GetMapping("/index")
public String index(@CookieValue(value = "userid", required = false) String userid, HttpSession session){
if(ObjectUtils.isEmpty(userid) || ObjectUtils.isEmpty(session.getAttribute(userid))){
// 1.cookie中没有userid,表示用户从来没有登录过
// 2.根据cookie中的userid在session中找不到用户信息,表示用户的session过期了,或者是伪造的userid
log.info("需要用户重新登录!");
return "login";
}
return "index";
}
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password, HttpSession session, HttpServletResponse response){
if(!"admin".equals(username) || !"123".equals(password)){
log.info("用户名/密码不正确!");
return "login";
}
// 服务器端保存用户的登录信息,并设置登录态2分钟内有效(以用户最后一次和服务器交互开始计算)
String userid = UUID.randomUUID().toString();
session.setAttribute("username", username);
session.setAttribute(userid, userid);
session.setMaxInactiveInterval(120);
// 设置用户浏览器端的cookie,用来识别用户的身份,并设置有效期为5分钟(以cookie创建时开始计算)
Cookie cookie = new Cookie("userid", userid);
cookie.setMaxAge(300);
response.addCookie(cookie);
return "index";
}
}
用户主页
用户主页
欢迎:[[${session.username}]]
登录页面
登录页面
此时,我们同时启动这两个应用:
- 访问
http://doc.zhangxun.com:8080/index
,由于第一次登录,没有cookie和session用户信息,因此进入登录页面; - 用户登录后,创建了session和cookie中的用户信息,成功进入用户页面;
- 此时再次访问
http://doc.zhangxun.com:8080/index
,无需登录直接进入用户页面,而且观察浏览器中的请求信息,是有cookie用户信息的; - 访问另外一个网站
http://code.zhangxun.com:8080/index
,发现没有任何cookie和session用户信息,因此进入登录页面;
如上步骤3和4的对比截图如下:
如此,说明,在默认情况下,多个网站即使一级域名相同,二级域名不同也是无法共享cookie的。现在我们来改动下如上代码,让cookie能在一级域名相同的情况下进行共享。
cookie.setDomain("zhangxun.com");
如上,只要在登录过程中,设置cookie的共享域为一级域名即可实现共享了。如下是实验截图:
但是结果显示,在执行步骤4的时候,虽然获取了共享的cookie用户信息,但是还是无法实现SSO,这是因为session不同导致的,仔细查看上图,发现cookie中的JSESSIONID是不同的,所以说明了这是两个不同的会话,无法实现session用户信息共享。
PS:如上两个应用应该端口号错开,比如分别使用8080和8081,图就不改了。
问题二:Session的共享
Session共享的实现方案一般来说有三种:
- 使用Tomcat集群Session全局复制,即集群内每个Tomcat中都保留全量session用户信息,这种方案明显低效且有严重的性能问题,不推荐;
- 根据用户请求的来源IP地址进行Hash映射到固定的服务器上,如此同一个用户会被固定在一台服务器上进行服务,如此就不需要session共享了,但问题是一旦某台服务器宕机,相当一部分用户的session信息就会丢失,而且后续需要弹性增减服务器的时候会比较麻烦,也不推荐。
- 使用一种分布式可扩展的工具代替tomcat中的session,使得所有服务器都能安全可靠地共享session,比如redis,比较推荐,本文下面也是基于这样方案进行演示。
首先本地需要安装redis并启动,这个就不赘述了。然后两个应用同时修改代码如下:
org.springframework.boot
spring-boot-starter-data-redis
spring.redis.host=localhost
spring.redis.port=6379
@Slf4j
@Controller
public class LoginController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/index")
public String index(@CookieValue(value = "userid", required = false) String userid, Model model){
if(ObjectUtils.isEmpty(userid) || ObjectUtils.isEmpty(redisTemplate.opsForValue().get(userid))){
// 1.cookie中没有userid,表示用户从来没有登录过
// 2.根据cookie中的userid在共享session中找不到用户信息,表示用户的session过期了,或者是伪造的userid
log.info("需要用户重新登录!");
return "login";
}
model.addAttribute("username", redisTemplate.opsForValue().get(userid));
return "index";
}
@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password, HttpServletResponse response, Model model){
if(!"admin".equals(username) || !"123".equals(password)){
log.info("用户名/密码不正确!");
return "login";
}
// 服务器端保存用户的登录信息,并设置登录态2分钟内有效(以用户最后一次和服务器交互开始计算)
String userid = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(userid, username, 120, TimeUnit.SECONDS);
// 设置用户浏览器端的cookie,用来识别用户的身份,并设置有效期为5分钟(以cookie创建时开始计算)
Cookie cookie = new Cookie("userid", userid);
cookie.setMaxAge(300);
cookie.setDomain("zhangxun.com");
response.addCookie(cookie);
model.addAttribute("username", username);
return "index";
}
}
用户主页
用户主页
欢迎:[[${username}]]
登录页面
登录页面
两个应用同时启动后,再重复上述4个步骤,就能发现cookie和session都能实现共享了。
下一篇介绍如果两个网站域名完全不同,即一级域名也不同的情况下如何实现SSO。