目录
0、HTTP是无状态的
2、服务端存储session库
2.1、我们先来简单聊聊session是什么?
2.2、session的存储方式
2.3、session的过期和销毁
2.4、session的分布式问题
2.5、session的缺点
3、token
3.1、理解token
3.2、客户端存储token
3.3、token过期
3.4、token的编码
4、JWT
4.1、简单介绍JWT
4.2、JWT的组成
4.2.1、JWT头
4.2.2、有效载荷【用户信息】
4.2.3、签名哈希【防伪标志】
Base64URL算法:
4.3、JWT的用法
4.4.JWT优缺点
4.5、refresh token
4.6、session token
5、单点登录
5.1、什么是单点登录?
5.2、“虚假”的单点登录(主域名相同)
5.3、“真实”的单点登录(主域名不同)
标注:
0、HTTP是无状态的
http协议是无状态的,也就是说http请求方和响应方无法维护状态,都是一次性的。但是在有的系统中,例如前面做过的OJ项目,都是刷题时,是需要用户登录后才能刷题。在这种情况下,我们是需要维护用户状态,否则用户就得不停地去登录登录,很影响用户体验感~
那么,我们如何进行去维护用户的状态呢?
标记!!!
也就是说,我们可以给已经登录的用户进行一个标记。当后端登陆成功后,就给这个用户做一个标记,携带着这个标记返回到前端;前端获取到这个标记后,把这个标记存储起来;当前端后续再有请求发送给后端时,我们把这个标记带着;后端再次收到请求后,先验证这个标记是否合法,合法则进行后续逻辑操作,不合法就表示这个用户还未登陆。
总结来说,就是:后端发标记——前端存标记——请求带标记
具体落实下来,有以下几个方案:cookie+session、token
1、前端标记cookie
前端存储,可以使用cookie、localStorage、sessionStorage等方式。如果采用cookie存储,cookie相比其他方式,借助http头、浏览器能力,cookie可以做到前端无感知。
大致过程:
- 在提供标记的接口(例如登录接口),通过HTTP返回头的Set-Cookie字段,直接存到浏览器上
- 浏览器发起请求后,会自动把cookie通过HTTP请求头的Cookie字段,带给接口
1.1、cookie限制空间范围
通过配置Domain / Path 来限制cookie的空间范围:Domain限制域,Path限制路径。
Domain说明:Domain属性指定浏览器在发出HTTP请求时,哪些域名要附带这个cookie,如果没有指定这个属性,浏览器会默认是将其设为当前URL(存cookie的这个URL)的一级域名,例如www.baidu.com,就会设为baidu.com,并且如果访问baidu.com的任何子域名,HTTP请求也会带上这个cookie。并且如果服务器在Set-Cookie字段指定的域名不属于当前域名,浏览器会拒绝这个Cookie。也就是说,你自己本来的域名是aaa,你在Set-Cookie字段指定的域名是bbb,浏览器就会拒绝这个cookie。
Path说明: Path属性指定浏览器发出HTTP请求时,
1.2、cookie限制时间范围
通过配置Expires / Max-Age来限制cookie的时间范围~
Expires说明:Expires属性指的是具体的到期时间,到了指定时间后,浏览器就不再保留这个cookie。这个值的格式为UTC格式~
注:浏览器的时间是根据本地时间计算的,由于本地时间是不精确的,所以没有办法保证cookie一定会在服务器指定的时间过期~
Max-Age说明:Max-Age属性,指的是这个cookie存在开始算起的秒数,秒数到了后,浏览器就不再存储这个cookie了
注:上述两个属性同时设置了,Max-Age优先生效。
注:如果不设置上述两个属性,或者将其设置为null,则表示cookie只在当前回话有效,浏览器关闭后,当前session结束,该cookie就会被删除~
1.3、cookie限制使用方式
通过Secure / HttpOnly来限制cookie的使用方式~
Secure说明:Secure属性指定浏览器只有在加密协议hTTPS下,才能将这个cookie发送给服务器。这个属性不需要我们自己来配置,他只是一个开关,如果当前协议是HTTP,浏览器就会默认关闭这个属性,如果是HTTPS就会自动打开这个开关~
HttpOnly说明:HttpOnly属性也可以看作是一个开关,当我们把开关打开时,则指定该cookie无法通过js脚本(js中的document.cookie等操作)拿到这个值,防止cookie被脚本读到,也就是只有浏览器发出HTTP请求时,才会带上该cookie。
2、服务端存储session库
2.1、我们先来简单聊聊session是什么?
- session是用来记录用户状态的;
- session是由服务器端创建的;
- 同一个浏览器发起的多次请求,是同属于一次会话session;
- 首次使用到Session时,服务器会自动创建Session,并创建Cookie存储SessionId【唯一的】发送回客户端;
- 一次会话是使用同一浏览器发送的多次请求。一旦浏览器关闭,则结束会话;
上述的简单了解后,我们知道他其实已经自动将session的SessionId放到了cookie中存储,也就是说前端的cookie中,存储了SessionId。在实际项目中,我们在使用session时,一般会在服务器端存储session会话,session回话中,包含sessionID,用户信息等,在我们有新的请求发送时,我们就可以直接获取cookie中存储的SessionID,然后在服务器端存储的session回话中查找,看有没有sessionID和请求中带过来的ID一样的会话,例如登录相关,有这个会话则表示已登录,无则表示用户未登录~
session的登录验证流程:
说明:
- 浏览器登录发送账号密码,服务端查用户库,校验用户
- 服务端把用户登录状态存为 Session,生成一个 sessionId
- 通过登录接口返回,把 sessionId set 到 cookie 上
- 此后浏览器再请求业务接口,sessionId 随 cookie 带上
- 服务端查 sessionId 校验 session
- 成功后正常做业务处理,返回结果
2.2、session的存储方式
前端只是存了一个sessionId,session的具体信息,还是需要我们自己存储一下滴~
存储方式:
- Redis(推荐):内存型数据库,redis中文官方网站。以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快;
- 内存:直接放到变量里。一旦服务重启就没了;
- 数据库:普通数据库,如MySQL。性能不高。
2.3、session的过期和销毁
删除存储的session中的数据即可~
2.4、session的分布式问题
通常服务端是集群,而用户请求过来会走一次负载均衡,不一定打到哪台机器上。那一旦用户后续接口请求到的机器和他登录请求的机器不一致,或者登录请求的机器宕机了,session 不就失效了吗?
这个问题现在有几种解决方式。
- 一是从「存储」角度,把 session 集中存储。如果我们用独立的 Redis 或普通数据库,就可以把 session 都存到一个库里。
- 二是从「分布」角度,让相同 IP 的请求在负载均衡时都打到同一台机器上。以 nginx 为例,可以配置 ip_hash 来实现。
但通常还是采用第一种方式,因为第二种相当于阉割了负载均衡,且仍没有解决「用户请求的机器宕机」的问题。
2.5、session的缺点
session的维护给服务器端会造成很大的困扰,我们必须找地方存放它,又要考虑分布式问题,甚至还要给他启用一套redis集群~
这个缺点,token就有效解决了~
3、token
3.1、理解token
例如在登录中,我们其实不需要往session中存太多东西,我们完全可以把要存的数据(轻量级数据)都打包到cookie中,这样一来,服务器就不同存了~ 而每次的请求来了,我们只需要验证cookie中的“证件”是否有效就可以了。
上述这种方式就被叫做token。
token的大致工作流程:
说明:
- 用户登录,服务端校验账号密码,获得用户信息
- 把用户信息、token 配置编码成 token,通过 cookie set 到浏览器
- 此后用户请求业务接口,通过 cookie 携带 token
- 接口校验 token 有效性,进行正常业务接口处理
3.2、客户端存储token
使用cookie或localStorage或sessionStorage等...
3.3、token过期
把过期时间和数据一起放过去,验证时判断就可以了
3.4、token的编码
token的编码是base64编码,我们可以搜索base64,随便打开一个编码网站,如下:
上述这种把某些信息放在一起,通过base64编码后,好像没什么用。我大可以把cookie中的token值复制出来,解码,然后修改为我想要的用户的信息,再进行编码,放到cookie中,发送给后端,是不是就可以获取其他的用户信息了呢?
答案是肯定的。那我们应该怎么办呢?
针对上述这种操作,我们需要防止对token的数据这个数据的篡改!
如何防篡改?给token签名!!!
具体的我们可以根据JWT来看看,看看JWT是如何生成token的~
4、JWT
4.1、简单介绍JWT
JWT是JSON WEB TOKEN的缩写,它是基于RFC7519标准定义的一种可以安全传输读的JSON对象,由于使用了数字签名,所以是可信任和安全全的。
之所以使用JWT,是因为Token是属于无状态的,每个人定制的规则不同,生成的的结果就会不同。所以目前,多数都是采用JWT为统一令牌标准~
4.2、JWT的组成
jwt实例可以在这个网站看看效果:JSON Web Tokens - jwt.io
例如,我们就按照网页上默认的点击生成看看:
我们可以看到,编码后的结果是有三个部分组成的,每个部分之间用 . 分割【上述中,各个部分对应的字体颜色是一致的】。三个部分的含义:
- jwt头信息
- 有效载荷【用户信息】
- 签名哈希【防伪标志】
4.2.1、JWT头
JWT头部分是一个描述JWT元数据的JSON对象,通常如下:
{"alg": "HS256","typ": "JWT"
}
说明:
- alg:表示签名使用的算法,默认为HMAC SHA256(HS256)
- typ:表示令牌的类型,JWT令牌统一写为JWT
最后,使用Base64URL算法将上述JSON对象转换为字符串保存
4.2.2、有效载荷【用户信息】
这一部分是JWT主体内容部分,也是一个JSON对象,包含需要传递的数据。JWT指定七个默认的字段供选择:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除了上述默认的字段外,我们还可以自定义私有字段,如:
{"sub": "1234567890","name": "John Doe","admin": true,"iat": 1516239022
}
注:默认情况下,JWT是未加密的,也就是说其他人可以解读你的内容,所以不要构建隐私信息字段。
最后,使用Base64URL算法将上述JSON对象转换为字符串保存
4.2.3、签名哈希【防伪标志】
签名是对上面两部分数据签名。通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密钥(secret)。该密钥存在服务器中,不向用户公开。然后使用JWT头中指定的签名算法(HS256)根据下列公式,生成签名:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims),
secret)
在计算出签名哈希后,JWT头、有效载荷、和签名哈希三个部分组成一个字符串,每个部分用“.”分隔,构成整个JWT对象
Base64URL算法:
如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是"+","/“和”=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是Base64URL算法。
4.3、JWT的用法
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。当跨域时,也可以将JWT被放置于POST请求的数据主体中。
4.4.JWT优缺点
- JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
- 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
- 存储在客户端,不占用服务端的内存资源
- JWT默认不加密,但可以加密。生成原始令牌后,可以再次对其进行加密。
- 当JWT未加密时,一些私密数据无法通过JWT传输。
- JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
- JWT本身包含认证信息,token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
- 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
4.5、refresh token
业务接口用来鉴权的 token,我们称之为 access token。越是权限敏感的业务,我们越希望 access token 有效期足够短,以避免被盗用。但过短的有效期会造成 access token 经常过期,过期后怎么办呢?
一种办法是,让用户重新登录获取新 token,显然不够友好,要知道有的 access token 过期时间可能只有几分钟。
另外一种办法是,再来一个 token,一个专门生成 access token 的 token,我们称为 refresh token。
- access token 用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活
- refresh token 用来获取 access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的 session 一样处理
有了 refresh token 后,几种情况的请求流程变成这样:
如果 refresh token 也过期了,就只能重新登录了。
4.6、session token
session和token的边界还是很模糊的,就像上面4.5提到的,refresh token也可能是以session的形式组织维护的。
狭义上,我们通常认为session是前端存在cookie上,数据存在服务器端;token是前端存在哪儿都行,数据存在token里。非要对比这两个,其实就是在对比前端存哪儿,数据存哪儿~
5、单点登录
5.1、什么是单点登录?
业务线越来越多,就会有更多业务系统分散到不同域名下,就需要「一次登录,全线通用」的能力,叫做「单点登录」。
5.2、“虚假”的单点登录(主域名相同)
简单的,如果业务系统都在同一主域名下,比如wenku.baidu.com
tieba.baidu.com
,就好办了。可以直接把 cookie domain 设置为主域名 baidu.com
,百度也就是这么干的。
5.3、“真实”的单点登录(主域名不同)
比如滴滴公司,同时拥有didichuxing.com
xiaojukeji.com
didiglobal.com
等域名,种 cookie 是完全绕不开的。
这要能实现「一次登录,全线通用」,才是真正的单点登录。
这种场景下,我们需要独立的认证服务(就是把登录业务单拎出来,放在一台服务器上),通常被称为 SSO(Single Sign On)。
一次从A系统引发登录,到B系统不用登录的完整流程:
说明:
- 用户进入 A 系统,没有登录凭证(ticket),A 系统给他跳到 SSO
- SSO 没登录过,也就没有 sso 系统下没有凭证(注意这个和前面 A ticket 是两回事),输入账号密码登录
- SSO 账号密码验证成功,通过接口返回做两件事:一是种下 sso 系统下凭证(记录用户在 SSO 登录状态);二是下发一个 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 A 接口
- 系统 A 校验 ticket,成功后正常处理业务请求
- 此时用户第一次进入系统 B,没有登录凭证(ticket),B 系统给他跳到 SSO
- SSO 登录过,系统下有凭证,不用再次登录,只需要下发 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 B 接口
「完整版本:考虑浏览器的场景」
上面的过程看起来没问题,实际上很多 APP 等端上这样就够了。但在浏览器下不见得好用。
看这里:
对浏览器来说,SSO 域下返回的数据要怎么存,才能在访问 A 的时候带上?浏览器对跨域有严格限制,cookie、localStorage 等方式都是有域限制的。
这就需要也只能由 A 提供 A 域下存储凭证的能力。一般我们是这么做的:
图中我们通过颜色把浏览器当前所处的域名标记出来。注意图中灰底文字说明部分的变化。
- 在 SSO 域下,SSO 不是通过接口把 ticket 直接返回,而是通过一个带 code 的 URL 重定向到系统 A 的接口上,这个接口通常在 A 向 SSO 注册时约定
- 浏览器被重定向到 A 域下,带着 code 访问了 A 的 callback 接口,callback 接口通过 code 换取 ticket
- 这个 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只为了传一下换 ticket,换完就失效
- callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
- 在后续请求中,只需要把 cookie 中的 ticket 解析出来,去 SSO 验证就好
- 访问 B 系统也是一样
标注:
本文是根据下文魔改过来的,加了一些个人的观点
文章: 鉴权必须了解的5个兄弟:cookie、session、token、jwt、单点登录 - 知乎
好啦,本文结束咯,下期见