关于我是怎么完成 SSO (单点登录)的这档事
因为很多原因,社团需要写一个单点登录,最好是可以让大部分开源项目也能用上。于是我盯上了 OAuth 2 / OIDC...
众所周知,iOS Club 现在有了越来越多的网站,但是我们还没有一个统一的登录接口。之前我倒是想过把社团官网当数据源,看能不能接入 Casdoor/Logto 这种 sso 平台,但很明显不行。于是自从官网重构以来,我都一直在想着能不能搞个专门的平台来搞定这件事。于是这就来了。
SSO是什么
SSO(单点登录),其实可以说是一种非常常见的登录方式,例如建大的统一身份认证平台,或者游戏里常出现的 微信/QQ 登录,都是 SSO 的操作 —— 让登录变成一个专门的事情,用一个登录接口来管理所有系列的登录事宜。这种登录方式说实话还是很方便的,至少可以不用记那么多密码,登录起来也方便的很。
所有对于 SSO 来说,玩的更多是跳转的艺术:先跳转到统一登录平台,然后再跳转回去,登录完了再拿个能够证明你是你的令牌。
所有关键变成了,我们该怎么进行身份验证,以及我们该怎么跳转。
我们该怎么去进行身份验证
从底子上就是,你现在要去申请一个身份证,得先去派出所,然后先验证一下你是不是你(通过对照数据库的数据来进行查找,判断你是不是你),证明成功之后给你颁发一个身份证(返回一个 token)。之后你就可以用这个身份证去干一些需要证明身份的事情了。
所以关键就在于如何去判断你是你、如何去生成这个 Token,以及如何去携带。
从判断来说的话,最简单的方式就是直接把用户名密码给写到 token 里,然后等到需要验证的时候,再来一次服务器查找就行了。
但是很明显这种方法太抽象了:一旦被其他人“截胡”了,就算你用 Base64,人一解析立马就能把你用户名和密码给套出来。
所以最好的办法就是,咱们“随机生成”一个 Token,然后把这个 Token 存到数据库里,然后当你每次进行验证的时候,就跑到数据库对比一下 Token 即可。
我们可以写一个这样的:将用户 ID + 随机 UUID + 时间 UTC 进行字符串合并,然后再进行加密即可。当然这样就必须让接口加入一个用户 ID 的接入:因为数据库需要查到他。
当然,任何的 Token 都应该有时间限制,要不然迟早都会被其他人拿走用来做坏事。
但是啊,有没有一种操作,我们可以把这种 Token 结构化,就像咱们的身份证号码一样,可以从号码中得到你的户籍地,生日信息。咱们也可以让 Token 存入 用户 Id,时间限制等信息。这种结构必须是一种可以被解析的,也可以扩展的。没错,用 Json。
让 Json 来存身份验证信息 —— JWT
Json 这个东西,先给各位看看 Json 长什么样子吧:
{
"name": "LuckyFish",
"hash": "asdfasdflkjajksbhdfoiuasgf",
"time": 12309719837
}这东西其实和 JS 的 Object 长得很像了,当然事实上 Json 的全称就是JavaScript对象表示法(JavaScript Object Notation)。用这东西来存结构化数据最好不过了。
那么把 Token 和 Json 一结合,再加上这东西主要用于 Web,于是就出现了 Json Web Token,也就是 JWT。
JWT其实分为三部分:Header(头部)、Payload(荷载) 和Signature(密钥)。第一个部分主要用来声明用何种哈希算法,第二个部分则是用来存储各种数据的:例如用户 ID、过期时间等,第三个部分则是签名,这个部分通常为头部、荷载与一个密钥产生的哈希值。这样的话可以最大限度的保证安全。当然你要是把密钥给泄露了那就好玩了。
这里也简单推荐一个随机生成密钥的网站:就是这个
当然如果担心密钥给泄露了,也可以加入一个简单的:在荷载中加入一个 UUID 值。这样整个密钥就彻底独一无二了。
目前来说社团官网的登录系统和 OAuth2/OIDC 系统就是用的 JWT。因为这个可以加入很多的荷载,从而完成很多操作(虽然也不推荐存太多东西)。当然事实上我也没学过其他的 Token 生成方式
从携带来看分两种:Cookie 和 Authorization:
Cookie
Cookie 其实用的蛮多的。这个的原意是服务器向客户端(浏览器)发送的一种小型信息文件,而浏览器也会进行存储,并把这些东西当随身物品自动附加到每次请求中。
于是就有人发现这东西当身份验证刚刚好 —— 浏览器自动帮你附加。于是就有人会这么干:当登录操作结束的时候,会把登录信息的令牌放到 Cookie 里,直接传给前端。浏览器就会自动存储,然后之后的所有请求上都会加上这个信息。
Authorization
这种和前面的 Cookie,其实都在请求头上,但是这个得你自己设置。在前端上一般这么设置:
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
},
});得你自己手动设置一下。但是这个的好处就在于你完全可以自己定义,这个验证或者不验证取决于前端人员。这种就叫做无状态。这种的好处就在于利于分布式系统(因为不依赖于某个服务器和客户端的会话)
OAuth2 到底是个啥
在了解了身份验证和令牌的基础知识后,我们现在可以回到最初的问题:如何设计一个安全、通用的跳转流程来实现 SSO。这正是 OAuth2 框架要解决的核心问题。
我们可以先思考一个简易的通用登录系统:
直接在第三方网站中写个类似的登录接口,然后就像模拟官网登录一样,调用官网登录 API,这之后就可以得到一个 Token 记录,然后验证成功之后就 OK 了。
但是很明显,当我想要获取到官网的其他数据时,就需要记录用户名和密码。此时就会出现泄露问题,而且还有一个更大的问题 —— 万一我官网登录发生了改变,就例如从用户名密码变成了手机号密码呢?此时第三方应用的前端和后端就得更改。
那么最好的办法就是重定向到官网接口,然后再从官网接口重定向回来,此时就可以携带相应的 Token 了。
现在我们可以设计一个跳来跳去的方法:
- 先访问 API接口,携带一个客户端 ID
- API 重定向到通用的前端接口
- 登录成功后携带着 token 重定向回第三方应用
这个其实也就是 OAuth 2 的基本操作了,但就是会麻烦一些。