应用鉴权就是当一个用户进入APP时,我们需要判断他所拥有的权利,根据权力来判断他所能进行的一个行为,最为常见的就是购物网站的登录以及购物支付等操作。
一.鉴权的需求背景
Http的请求是无状态的,就是说在一个Http请求中的请求方和响应方都是无法维护状态,是一次性的,所以我们就不知道请求前后都发生了什么。所以我们需要标记的功能,而浏览器的sessionStorage,localStorage,全局变量等限制太多,就有了cookie,session,token等鉴权的操作。
二.cookie
cookie也是一种前端存储的方式,但是他和sessionStorage,localStorage等本地存储的不同在于,浏览器向服务端发起请求的时候,cooike是自动传过去的,可以做到前端无感知,出错的概率更低
过程:
1.浏览器向服务器发起请求并传送数据,由服务器接收数据然后设置cooike放进响应头(Set-Cookie),浏览器接收到响应之后就会自动存储进cookie
2.在之后的每一次请求中浏览器都会自动的在请求头之中设置cookie字段,发送给服务端
配置:
1.Domain/Path
cookie 是要限制空间范围的,通过 Domain(域)/ Path(路径)两级。
Domain属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com 会设为 example.com,而且以后如果访问example.com的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在Set-Cookie字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。
Path属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH属性是/,那么请求/docs路径也会包含该 Cookie。当然,前提是域名必须一致。
2.Expires / Max-Age
cookie 还可以限制时间范围,通过 Expires、Max-Age 中的一种。
Expires属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式。如果不设置该属性,或者设为null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。
Max-Age属性指定从现在开始 Cookie 存在的秒数,比如60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。
如果同时指定了Expires和Max-Age,那么Max-Age的值将优先生效。
如果Set-Cookie字段没有指定Expires或Max-Age属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。
3.Secure / HttpOnly
cookie 可以限制使用方式。
Secure属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的Secure属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。
HttpOnly属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是Document.cookie属性、XMLHttpRequest对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。
Http头对cookie的读写:
响应会携带一个Set-Cookie头,一个Set-Cookie只能设置一条cookie,格式为cookie键值+配置键值,如果想要一次设置多个cookie,我们可以多写几个Set-Cookie在头里面。
Set-Cookie: username=jimu; domain=jimu.com; path=/blog; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
而当浏览器请求服务器的时候,就不再需要发送配置内容了,只需要发送键值对就可以
Cookie: username=jimu; height=180; weight=80
前端对cookie的读写操作:
如果服务端设置的cookie并没有设置httpOnly,那么我们就可以调用document.cookie来对cookie进行读写操作,但一次调用document.cookie就只能操作一个cookie
三.session
在上面我们介绍了cookie,可以了解到cookie其实是浏览器存储的一种实现,可以看作应用鉴权的基石。但是它只是一个存储信息的工具,我们还需要判断其中的信息是不是安全的操作者,这时候我们就需要session了。
典型的session登录流程:
1.浏览器登录发送账号密码,服务端查用户库,校验用户
2.服务端把用户登录状态存为 Session,生成一个 sessionId
3.通过登录接口返回,把 sessionId set 到 cookie 上
此后浏览器再请求业务接口,sessionId 随 cookie 带上
4.服务端查 sessionId 校验 session
5.成功后正常做业务处理,返回结果
session的存储与过期销毁:
由于session是用来验证的,所以服务端仅仅只是给浏览器的cookie中添加一个sessionid,所以也需要自己保存一下,存储的方式:
1.Redis(推荐):内存型数据库,redis中文官方网站。以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快。
2.内存:直接放到变量里。一旦服务重启就没了
3.数据库:普通数据库。性能不高。
而session也可以手动设置过期时间,一到过期时间就直接清空存储的内存就好了
session的分布式问题:
由于服务端是集群,而用户请求过来会走一次负载均衡,不一定会打到哪台机器上。一旦用户后续接口请求到的机器和他登录请求的机器不一样,或者登录请求的机器出问题了,那session就会失效。
常见的解决方式:
1.把session集中存储到独立的redis或者普通数据库中,就可以把session存储到一个库里(存储角度)
2.让相同 IP 的请求在负载均衡时都打到同一台机器上。以 nginx 为例,可以配置 ip_hash 来实现。(分布角度)
四.token
1.用户登录,服务端校验账号密码,获得用户信息
2.把用户信息、token 配置编码成 token,通过 cookie set 到浏览器
3.此后用户请求业务接口,通过 cookie 携带 token
4.接口校验 token 有效性,进行正常业务接口处理
目前主流的token存储还是在cookie中进行,但是为了防止伪造token造成的安全问题,我们还需要一些加密算法来生成签名,来保证数据安全性
JWT
由于以上的方法增加了cookie的数量,所以JSON web Token得以被使用。
refresh token
token,作为权限守护者,最重要的就是「安全」。
业务接口用来鉴权的 token,我们称之为 access token。越是权限敏感的业务,我们越希望 access token 有效期足够短,以避免被盗用。但过短的有效期会造成 access token 经常过期,过期后怎么办呢?
一种办法是,让用户重新登录获取新 token,显然不够友好,要知道有的 access token 过期时间可能只有几分钟。
另外一种办法是,再来一个 token,一个专门生成 access token 的 token,我们称为 refresh token。
1.access token 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活
2.refresh token 用来获取 access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的 session 一样处理
如果refresh token过期了,就只能重新登陆了。
五.session与token对比
客户端存cookie与存放于其他地方
1.出了浏览器环境之外就没有cookie了;
2.cookie是浏览器在域下自动携带的。很容易引发CSFR攻击
存放在别的地方可以解决部分问题
服务端存储数据于不存
1.存数据的话可以缩短认证字符串的长度,减小请求体积
2.不存数据就不会出现分布式处理的问题,降低硬件成本,避免查库带来的验证延迟
六.单点登录
前面我们已经知道了,在同域下的客户端/服务端认证系统中,通过客户端携带凭证,维持一段时间内的登录状态。
但当我们业务线越来越多,就会有更多业务系统分散到不同域名下,就需要「一次登录,全线通用」的能力,叫做「单点登录」。
在主域名相同的情况下,就可以直接把cookie设置为主域名就可以实现了。
如果主域名不同,我们就需要独立的认证服务,称为SSO。
1.用户进入 A 系统,没有登录凭证(ticket),A 系统给他跳到 SSO
2.SSO 没登录过,也就没有 sso 系统下没有凭证(注意这个和前面 A ticket 是两回事),输入账号密码登录
3.SSO 账号密码验证成功,通过接口返回做两件事:一是种下 sso 系统下凭证(记录用户在 SSO 登录状态);二是下发一个 ticket
4.客户端拿到 ticket,保存起来,带着请求系统 A 接口
5.系统 A 校验 ticket,成功后正常处理业务请求
6.此时用户第一次进入系统 B,没有登录凭证(ticket),B 系统给他跳到 SSO
7.SSO 登录过,系统下有凭证,不用再次登录,只需要下发 ticket
8.客户端拿到 ticket,保存起来,带着请求系统 B 接口
如果是在浏览器之下实现,我们需要考虑其他的东西
对浏览器来说,SSO 域下返回的数据要怎么存,才能在访问 A 的时候带上?浏览器对跨域有严格限制,cookie、localStorage 等方式都是有域限制的。
这就需要也只能由 A 提供 A 域下存储凭证的能力。一般我们是这么做的:
图中我们通过颜色把浏览器当前所处的域名标记出来。注意图中灰底文字说明部分的变化。
1.在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上,这个接口通常在 A 向 SSO 注册时约定
2.浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
3.这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
4.callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
5.在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好
6.访问 B 系统也是一样
七.总结
1.HTTP 是无状态的,为了维持前后请求,需要前端存储标记
2.cookie 是一种完善的标记方式,通过 HTTP 头或 js 操作,有对应的安全策略,是大多数状态管理方案的基石
3.session 是一种状态管理方案,前端通过 cookie 存储 id,后端存储数据,但后端要处理分布式问题
4.token 是另一种状态管理方案,相比于 session 不需要后端存储,数据全部存在前端,解放后端,释放灵活性
5.token 的编码技术,通常基于 base64,或增加加密算法防篡改,jwt 是一种成熟的编码方案
6.在复杂系统中,token 可通过 service token、refresh token 的分权,同时满足安全性和用户体验
7.session 和 token 的对比就是「用不用cookie」和「后端存不存」的对比
8.单点登录要求不同域下的系统「一次登录,全线通用」,通常由独立的 SSO 系统记录登录状态、下发 ticket,各业务系统配合存储和认证 ticket
阅读知乎文章:鉴权必须了解的5个兄弟:cookie、session、token、jwt、单点登录 - 知乎 (zhihu.com)