狭义上,我们通常认为
(1) session 是「种在 cookie 上、数据存在服务端」的认证方案,
(2)token 是「客户端存哪都行、数据存在 token 里」的认证方案。
对 session 和 token 的对比本质上是「客户端存 cookie / 存别地儿」、「服务端存数据 / 不存数据」的对比。
疑问:session认证方案中,是否可以将用户id存储在headers呢?
一、背景知识
对比cookie、session的原理和使用:juejin.cn/post/728790…
后端session 的维护给服务端造成很大困扰,
(1)跨域问题,前后端不分离。
(2)我们必须找地方存放它,又要考虑分布式的问题,甚至要单独为了它启用一套 Redis 集群。有没有更好的办法?
回过头来想想,一个登录场景,也不必往 session 存太多东西,那为什么不直接打包到 客户端呢?这样服务端不用存了,每次只要核验 客户端带的「证件」有效性就可以了,也可以携带一些轻量的信息。
这种方式通常被叫做 token。
二、后端token的原理
- 客户端初次访问,服务器端在进行认证之后会生成一个包含用户数据的 JSON 对象。为防止该JSON对对象被篡改,服务器会在生成该对象的时候对其进行签名,这个加密后的数据就是token。
- 服务器将token发送给客户端。
- 客户端将token存放在本地。
- 在以后的访问中,客户端每次通信,都会在请求头中携带该JSON对象,服务器端通过该JSON认定用户身份。
- 因此,服务器端不再保存session数据,变成了无状态的情况。从而比较容易实现扩展。
二、客户端传递方式cookie
调用鉴权接口返回token后,将token信息种在cookie中。
可以分为(1)由服务端种cookie到客户端,(2)客户端自己种cookie。
优点:
服务端种cookie的话,对于前端开发者省事,不需要自己手动设置(对比headers、客户端自己种cookie)。
缺点:
(1)cookie 方式需要防csrf攻击。
(2)在浏览器端,可以用 cookie(实际上 token 就常用 cookie),但出了浏览器端(比如app端),没有 cookie无法使用。
(3)所有本域名的请求,包括静态资源请求、ajax请求都会携带上cookie,浪费带宽。
三、客户端传递方式headers
调用鉴权接口返回token后,将token信息种在请求的headers中,一般使用自定义的Authorization。
优点:
避免了csrf攻击。
缺点:
需要手动设置,比如使用axios的时候,可以使用。
const instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
// 备注:token从后端处调用获取
instance.defaults.headers.Authorization = token;
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。需要与后端约定。
四、token生成方案:最基础的加密 base64
用户信息如{"userid":"abb”},base64转化为eyJ1c2VyaWQiOiJhIn0=。
这里的 eyJ1c2VyaWQiOiJhIn0=,就是 {"userid":"abb”} 的 base64 而已。
其他人可以随意伪造token,从而伪造身份。比如我造一个{"userid":"admin”} 的 base64 token,后端会以为我是admin用户。
这样就需要给token加上 secret 签名, 只有后端生成的token才能真正被认证通过。
五、token生成方案:token加密的通用方案 jwt
简单版本防篡改,使用额外的secret 签名
成熟的token字符串生成方案,所有的信息放在一个token里。
- jwt基于json,数据处理方便。
- 可以在令牌(token)中自定义内容,容易扩展。
- 使用非对称加密和签名技术,安全性高。
- 资源服务使用JWT,可不依赖认证服务即可完成授权。
JSON Web Token 入门教程 - 阮一峰的网络日志
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{"alg": "HS256","typ": "JWT"}
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{"sub": "1234567890","name": "John Doe","admin": true}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。
六、当前token的使用安全考虑
目前token存储在localStorage中,localStorage 无法防止xss攻击访问 token;cookie倒是可以通过设置httponly 防止 盗窃脚本 访问 cookie。
所以呢,当前需要杜绝xss攻击。
对于xss攻击,一个有效的手段就是设置网站csp白名单策略:Content Security Policy 入门教程 - 阮一峰的网络日志
其他、细节
token 的过期
那我们如何控制 token 的有效期呢?很简单,把「过期时间」和数据一起塞进去,验证时判断就好。
eg:
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
refresh token
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 后,几种情况的请求流程变成这样:
参考:基于 Axios 封装一个完美的双 token 无感刷新 - 掘金
延伸、单点登录
在同域下的客户端/服务端认证系统中,通过客户端携带凭证,维持一段时间内的登录状态。
但当我们业务线越来越多,就会有更多业务系统分散到不同域名下,就需要「一次登录,全线通用」的能力,叫做「单点登录」。
使用cookie 单点登录(主域名相同)
简单的,如果业务系统都在同一主域名下,比如wenku.baidu.com tieba.baidu.com,就好办了。可以直接把 cookie domain 设置为主域名 baidu.com,百度也就是这么干的。
还有就是使用三方cookie。但是三方cookie未来肯定会被禁用、限制。
使用sso 单点登录(主域名不同)
比如滴滴公司,同时拥有didichuxing.com xiaojukeji.com didiglobal.com等域名,种 cookie 是完全绕不开的。
这要能实现「一次登录,全线通用」,才是真正的单点登录。
这种场景下,我们需要独立的认证服务,通常被称为 SSO。
- 用户进入 A 系统,没有登录凭证(ticket),A 系统给他跳到 SSO
- SSO 没登录过,也就没有 sso 系统下没有凭证(注意这个和前面 A ticket 是两回事),输入账号密码登录
- SSO 账号密码验证成功,通过接口返回做两件事:一是种下 sso 系统下凭证(记录用户在 SSO 登录状态);二是下发一个 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 A 接口
- 系统 A 校验 ticket,成功后正常处理业务请求
- 此时用户第一次进入系统 B,没有登录凭证(ticket),B 系统给他跳到 SSO
- SSO 登录过,系统下有凭证,不用再次登录,只需要下发 ticket
- 客户端拿到 ticket,保存起来,带着请求系统 B 接口