My Little World

sso单点登录实现

背景

多项目想要实现统一登录,即登录一个系统后,在打开其他相关系统页面后,是已经登录的状态,不再进行登录

原理

多个系统使用统一的顶级域名,利用cookie的domain属性,将登录后cookie保存在浏览器中,只要是该cookie的domain所包含的域名的站点,都可以拿到这个cookie

实现

选择原来多个站点中其中一个站点的登录页,作为公共跳转页
在登录之后将token塞到浏览器中
这里设置统一token在cookie中的key值为_a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
根据运行环境设置domain,sit,dev均为测试环境
function getDomain(name){
let testENVs = ['localhost','sit','dev']
let testENV = ''
testENVs.forEach(env=>{
if(location.hostname.includes(env)){ //根据域名判断环境
testENV = env
}
})
let temp = testENV?testENV +'.xxxx.com':'xxxx.com'
//本地开发不加domain,默认localhost,否则本地没办法进行开发
let domain = testENV === 'localhost' ? '': (name === '_a'? temp:location.hostname)//个别cookie字段仅用于本站点,所以domain设置为站点域名
return domain
}
setCookie=(name,value) => {
const Days = 0.5
const exp = new Date()
exp.setTime(exp.getTime() + Days*24*60*60*1000)
let str = name + '=' + escape(value) + ';expires=' + exp.toGMTString()
let domain = getDomain(name)
if(domain){
str +=';domain=' + domain
}
document.cookie = str
},

之所以要根据不同运行环境设置不同domain,是因为要在不同测试环境也要实现单点登录
比如测试环境在sit环境的a.sit.xxxx.com和b.sit.xxxx.com
a.sit.xxxx.com登录页作为公共登录页
在a.sit.xxxx.com登录后,token的cookie会被设置成
key是_a,domain是sit.xxxx.com(浏览器会自动在前面加一个点),这样打开b.sit.xxxx.com的页面,浏览器中还会存在_a这个cookie,b.sit.xxxx.com就可以直接拿这个cookie向服务器发请求了

以此类推,在dev环境,这两个站点域名会变成
a.dev.xxxx.com和b.dev.xxxx.com
_a的domain会被设置成dev.xxxx.com
这时如果去b.dev.xxxx.com,_a是会有的
但去a.sit.xxxx.com,是不会有的,因为_a此时仅限于dev.xxxx.com域名及其子域名

等到了生产环境,二者域名变为
a.xxxx.com和b.xxxx.com
_a的domain会被设置成xxxx.com
此时就会出现一个问题,假如我先登录了线上环境
有了domain为xxxx.com的_a
再去测试环境登录,此时会加进去domain为sit/dev.xxx.com的_a
两个_a如何区分哪个才是当前环境的_a?

遇到的问题一 :测试环境_a加不进去,出现一闪而过
开始以为是浏览器机制问题,后来才发现,是代码逻辑问题
其实cookie是有写进浏览器的,跟cookie本身处理机制无关,可以写进去,问题出在了取cookie _a再给后台发请求的时候,以前是保存token的只有一个特殊key名,直接取那个key名就行,现在有两个_a,逻辑还是按照第一个来取,结果取错了,后台给发过来401,前端代码处理http状态码的逻辑又是401清cookie跳登录,所以domain为. sit/dev. xxxx. com的cookie就又消失了😅

遇到问题二 :两个_a如何区分哪个才是当前环境的_a?
因为后台实现分层,与前端直接对接的后台,无法访问后台的用户cookie表,能拿到cookie表的后台站在大局角度看问题,这个逻辑又觉得不是有必要加的,所以不能要求后台拿到报文中cookie后去表里面做判断
所以由前端通过读取document.cookie,整理拿到两个_a后,依次用每个_a去给后台发请求,如果成功了,说明当前_a是正确的,就可以用这个_a去发真正的请求,同时记录下这个_a,避免之后的重复探测

小结

1.碰到问题一时,很无厘头,但分析出原因后,让人感觉很无语,正确的说是,让人很没面子,遇到问题应该善于分析,戒骄戒躁
2.问题二,有时候,你认为很恶心的做法,恰巧可能会被采纳

后记

发版到线上后,发现部分同事登陆成功后又跳回登录页面,中间发起的请求报401

经过查看同事浏览器的cookie发现,存在设置了httponly的_a,即我们所有系统统一使用的token标识,
cookie设置了httponly意味着js没有权限进行获取更改甚至不能删除,所以新登录的token是没有写进去的,拿到的token也就是错的
解决办法就是让后台同事强制写一个没有httponly的_a进去,让后台覆盖后台,
说明浏览器优先级是先处理set-cookie的cookie逻辑,再看是否有httponly,决定js是否有权限处理cookie

相关链接
Cookie写不进去问题深入调查 https Secure Cookie
js创建cookie时获取一级域名设置domain解决跨域问题
js与cookie的domain和path之间的关系
前端单点登录(SSO)实现方法(二级域名与主域名)