小明是个个人开发者,自己准备写了个论坛,基本业务快写完了,要来处理下用户认证逻辑。 之前没搞过啊,上网找找看。好,有个 basic auth,看起来很简单啊,就这么干
basic auth
认证流程
- 客户端访问服务端,服务端认证失败放回 www-authenticate 请求头
- 客户端接收到 response 要求用户输入 账号、密码
- 用户输入自己的账号密码,由浏览器再次发送请求并添加 Authentication: Basic base64(account:password) 格式请求头
- 服务端验证 账号密码 没问题,返回相应资源
实施
npm i basic-auth
import basicAuth from 'basic-auth'
command c + command v
npm run start
十行代码,搞定。测试一下能不能跑
- 打开浏览器 localhost:8080
- 输入用户密码,登录成功
- 打开控制台,刷新页面,OK 后续的请求都会带上 authenrization 请求头,用户登录一次之后就不用输入用户名密码了。搞定
- 关闭浏览器,写写业务,过一下再次打开浏览器访问,诶,怎么又要我输入账号密码,刚刚不是弄过了吗,好烦
5. 不好使啊,上网看看咋回事。一看 basic auth 虽然简单,但是也有不少缺点
缺点
- 账号、密码通过 base64 编码传输
- 关闭浏览器会造成登录态失效,下次再打开网页需要重新输入账号密码
- 没法从服务端进行 logout 处理,只能由用户关闭浏览器触发 logout 逻辑
行吧,这样不行啊,那再找找其他办法。放狗搜一下,诶,又找到好东西了,cookie。用户登录完给用户数据放在 cookie 里面,下次请求再带过来不就知道他是谁了吗
cookie
流程
- 第一次通过账号密码校验之后小明给用户信息在服务端加密后进行 set-cookie 操作
- 下次访问的时候小明通过 cookie 里面的信息验证用户是否登录并获取相关用户信息
不错不错,小明心满意足,发布上线并推广自己的论坛,好多用户开始使用小明的论坛。
论坛功能不断迭代,cookie 里面记录的信息越来越多,每个请求都会携带体积不小的 cookie,服务端对 cookie 的加密解密也是一笔不小的开销,小明决心进行一定的优化。
这次小明准备比较充分,早就想好了应对方案。决定采用基于 session + cookie 的认证方式
cookie + session
注册流程
鉴权流程
区别
这种形式呢,不再将用户信息通过 cookie 存储在客户端,而是将用户数据放在服务端存储,cookie 中只记录相关的 sessionid。
每次通信的时候根据 cookie 里的 sessionid 进行用户身份识别,然后从服务端内存中获取相应的用户数据。
注意事项
主要是在分布式场景下的 session 同步、读写问题
做大做强
小明的论坛发展迅猛,业务不断扩展,从论坛衍生出很多的子业务,每个业务都有自己对应的域名。
发展虽好,但是也不断的收到用户投诉,用户反应你家不同产品每个都需要我登录很麻烦啊,我能不能登录一个产品之后其他产品就不要让我再输入用户密码了,能不能整个 SSO 啊,很烦。
小明一看 SSO 啊,赶紧调研一下
SSO
单点登录(SingleSignOn,SSO),就是通过用户的一次性鉴别登录。当用户在身份认证服务器上登录一次以后,即可获得访问单点登录系统中其他关联系统和应用软件的权限,同时这种实现是不需要管理员对用户的登录状态或其他信息进行修改的,这意味着在多个应用系统中,用户只需一次登录就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理,是目前比较流行的。
同域下的单点登录
哦哦,这样啊,登录一次技能访问我这边所用的应用对吧。
这简单的,我这边产品都是公用的同一个二级域名 .xiaoming.com 对吧, 那我
- 把 sessionid 的 cookie 种在二级域名下,所有的子域名之间共享
- 在给 session 服务抽成通用的,你们所有的子应用都拿 sessionid 跟 common session 服务通信
好了,问题解决。
更大更强
小明的公司越做越大,部分业务线拥有了自己的独立的域名,上面这种通过上层域共享 cookie 的方式已经不合适了,公司要为这部分业务探寻 SSO 的升级方案。
很快小明团队很快找到了 CAS 方案以及 OAuth 2.0 方案,并展开讨论
CAS
Central Authentication Service : 中央认证服务。
CAS 认证流程
xxm.com
xm.com
用户首次访问 xxm 应用
- 用户通过浏览器打开 xxm.com, 后端 xxm 服务发现用户未携带登录信息,重定向到 sso.xm.com/cas/login?service=xxm.com a. service 标识用户从那个服务来,完成登录之后需要冲定向到哪
- SSO server 发现也没有登录信息,弹出用户登录页。
- 用户填写用户名、密码 后提交,SSO系统进行认证后,将登录状态写入 SSO 的session,并在 sso.xiaoming.com cookie 里种下对应 cookie。
- SSO 系统登录完成后会生成一个ST(Service Ticket),浏览器携带 ST 参数到 xxm.com?ticket=ST-xxxx
- xxm 系统拿到 ST 后,从后台向 CAS 服务发送请求,验证 ST 是否有效,CAS 服务验证 ST 有效会将 sessionid 以及对应的用户信息返回 xxm 系统。
- xxm 系统将登录状态写入 session 并设置 xxm.com 下的 cookie 后重定向到用户最初访问的页面。
用户再次访问 xxm 应用
- 用户通过浏览器打开 xxm.com, xxm 服务端发现用户已经携带对应的 sessionid
- 结合用户信息,返回相应资源
用户访问 ddm 应用,小明公司的另一个子应用
- 用户通过浏览器打开 ddm.com, ddm.com cookie 中还没有记录用户相关的 sessionid,于是重定向到 sso.xm.com/cas/login?service=ddm.com
- sso server 接收到客户端请求,发现 sso.xiaoming.com 已经种上了用户的合法的 sessionid,这次没有让用户再次进行登录操作,直接生成了 ST,并重定向回到 ddm.com?ticket=ST-xxxx
- ddm 系统通过 url 拿到 ST,从后台向 SSO 发请求,验证 ST 是否有效
- 验证通过后,ddm 系统将登录状态写入自己的 session 并设置 ddm.com 下的 cookie
完整流程图
总结
- 所有的登录过程都依赖于 CAS 服务,包含用户登录页面、ST 生成、验证;
- 为了保证 ST 的安全性,一般 ST 都是随机生成的,没有规律性。CAS 规定 ST 只能保留一定的时间,之后 CAS 服务会让它失效,而且,CAS 协议规定 ST 只能使用一次,无论 ST 验证是否成功,CAS 服务都会清除服务端缓存中的该 ST,从而规避同一个 ST 被使用两次或被窃取的风险。
OAuth2.0
oauth2.0 的资源概念
- 资源拥有者:指的是一个可以授权访问被保护资源的个体,比如在座的每个人。
- 资源服务器:的是存储被保护资源的服务器,这些资源可以包括视频,相片,用户信息等等。
- 认证服务器:在资源拥有者授权后,向客户端授权(颁发 access token)的服务器。
- 客户端:指的是利用资源拥有者的授权信息去请求被保护资源的应用程序,例如第三方服务机构。
基本概念
有别于 CAS 在认证通过后直接下发用户信息,oauth 2.0 并不直接返回用户信息,而是根据资源拥有者的示意给客户端颁发令牌,客户端通过对应令牌向资源服务器请求对应资源。
举个例子就是小红住在一个门卫安保非常严谨的小区,进入小区需要输入用户密码。小红平时又特别喜欢点外卖,这可难为外卖小哥了,没密码进不去啊,小红也不能每次都下楼开门。这咋整呢?
于是小红就跟门卫室商量,咱们弄个令牌机制吧。下次外卖小哥来的时候说是给我送外卖的,你就打电话给我确认下,没问题的话你就给他个三天有效的令牌,通过这个令牌也能进来咱们小区,保安一听好啊。
当天晚上,外卖小哥又来了,向保安大哥汇报说是给小红送外卖的,保安大哥立马打电话向小红询问是不是真的,小红说:对对对,就是我点的外卖。保安一看没问题给了外卖小哥一张令牌。外卖小哥拿着令牌到了小区里面该干嘛干嘛。。。
四种模式
- 授权码模式(authorization code):最常见的模式
- 简化模式(implicit):直接将 token 返回给前端
- 密码模式(resource owner password credentials):通过账户密码验证身份
- 客户端模式(client credentials):用于没有前端的模式,使用 client_id client_secret 直接获取
咱们这里只讨论第一种:授权码模式
认证与授权流程
-
客户端向认证服务器申请 client_id 以及 client_secret 用于后续通信,并制定授权后的 redirect 地址
-
用户访问 xxm.com , xxm.com 应用呢需要基于用户信息对用户提供服务,用户信息在指定的资源服务上,需要通过 sso 认证服务下发的 access token 才能拿到用户数据。于是 xxm.com 的服务将访问路径重定向到 sso.xm.com/login?next=/oauth2/authorize?xxxxx
- sso login 这里已经登陆的话直接进入 next 询问用户是否授权,没有登录需要用户登陆之后再进入 next 询问用户是否授权
- 用户点击确认按钮,同意对 xxm.com 的授权。sso 认证服务将页面重定向到 xxm.com 之前指定的回调地址并携带对应的 code。
- xxm.com 通过重定向请求拿到 sso.xm.com 为其生成的 code,接着 xxm.com 将拿到的 code 以及之前申请的 client_id client_secret 一起发完认证服务器请求 access_token,也就是真正可以进行资源申请的令牌。
- xxm.com 这时候有令牌了,通过服务端 access_token 发往对应的用户资源服务器请求对应的用户信息
- 用户资源服务器接收到 access_token 也不知道真假,拿着 access_token 又像认证服务器询问,这是你给的吗
- 认证服务器确定是自己下发的 access_token 并告知用户资源服务器
- 用户资源服务器确定 access_token 合法后,将用户信息返回给 xxm.com
- xxm.com 拿到了想要的用户信息,并根据这些数据想用户提供了对应的服务
完整流程图
小明团队开始讨论了
小刚说:CAS 好啊,多简单啊,登录一下各个应用就能拿到用户信息。
老王说:OAuth2.0 好啊,多安全,就算其他人拿到了 code 他没有 client_secret 也拿不到 access_token,全部都在服务端。
角落里面的小李弱弱的说了句:我就是要个 username 啊,整这么复杂的吗?弄个 sso.xm.com 处理用户登录逻辑,然后通过 sso.xm.com 下发 jwt 不好吗,我只是想简单的获取个 username 啊
JWT
小李开始给大家普及 JWT 的概念。
三段组成
JWT 是由三段信息构成的,将这三段信息文本用.链接一起就构成了 JWT 字符串。
三部分分别为
header
头部承载两部分信息:
- 声明类型,这里是 jwt
- 声明加密的算法
{
"alg": "HS256",
"typ": "JWT"
}
然后将头部进行 base64 编码构成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload
payload 作为存放有效信息的地方,包括:
- 标准中注册的声明,可选添加
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt 的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
- 公共 + 私有部分
存放一些 提供者 和 消费者 共同定义的内容,即使泄露也不会造成损失的非敏感信息。不要存放敏感信息。
比如下方的
{ "name": "jufei" }
通过 base64 编码之后成了第二段
eyJuYW1lIjoianVmZWkifQ==
signature
使用 JWT 提供服务的秘钥以及 header 部分中定义的算法对 `${header}.${payload}`进行加密就构成了 jwt 的第三部分。
咋用呢
前端
- 前端通过访问 sso.xm.com/jwt 向统一服务申请获取 jwt
- sso.xm.com 判断当前用户是否登录,没有登录跳转 login 页面,已经登录直接返回 jwt
- 前端对 jwt 进行保存,并在每次需要使用 jwt 时进行过期校验,过期了重新获取新的 jwt
服务端
- 接收到前端传过来的 jwt 并通过认证服务处拿到的密钥或者公钥进行验证
- 合法 jwt 从 payload 拿取用户信息并想用户提供服务
- 不合法 jwt 拒绝服务
就这么简单,随便整整就满足我的要求了。