登录Token如何设计 Posted by xmpace on 2019-08-18

Token 的由来

客户端向服务端发送请求,服务端如何知道该请求是哪个用户发起的呢?

服务端不知道用户身份

为了标示请求的所属用户,客户端发送请求时必须带上用户的账号信息,像这样 /api/my_user_info?username=badboy123&password=goodgirl321。但这么一来,每个请求都得带上账号密码,这不安全,密码容易被泄露。

请求都带上账号信息

于是诞生了以 token 为基础的认证方案。客户端向服务端发送账号密码请求登录,服务端收到账号密码,校验后生成一个加密的 token 返回给客户端,后续客户端的所有请求都带上这个 token,服务端通过解密就能拿到客户端的登录账号信息了。

Token 认证

现在密码被换成了 token,不用担心密码被泄露的问题了,但是 token 也会被泄露啊?是的,token 现在面临跟密码同样的问题,但 token 其实只相当于一个临时密码,用户退出登录后,这个 token 就失效了,所以 token 被泄露的危害相较于密码被泄露的危害要小。

有人可能会觉得,登录的时候还是要传密码啊,怎么就减小密码被泄露的风险了呢?其实在 https 的保护下,数据在通信过程中被泄露的危险已经几乎没有了,更多的是网站各个页面的漏洞(如 XSS)造成的数据泄露,所以采用 token 认证方案,我们保护好登录这一个页面就能够很好的保护密码了。如果采用每个请求都带账号密码的方案,那任何一个页面的漏洞都可能造成密码泄露。

Token 的设计

上面介绍了 token 诞生的初衷,因此我们也知道了 token 需要满足的条件:

  1. 服务端能通过 token 拿到用户账号信息(废话)
  2. token 不能被伪造

我们看第 2 条就行了,防伪造,那简单啊,搞个随机数不就完了么。客户端发来账号和密码,服务端一校验,生成一个随机字符串,用随机字符串做 key,账号信息做 value,往 redis 一存就完了?这么干其实也可以,但是,用一个较长的字符串做 key 来查询,性能上不及用整型做查询。有没有方案能用整型做 key 呢?

账号一般都有账号 id,不妨用账号 id 来做 key,我们可以把账号 id 放进 token 里。账号 id 必须能通过直接解析 token 得到,这样一来,随机数防伪的方案就得抛弃了,我们得另寻防伪的方法。

说到防伪,在计算机领域首先想到的就是摘要算法了。

摘要算法

摘要算法,顾名思义就是像文章的摘要一样,摘要算法可以将任意长的数据映射成一段定长的摘要数据,这段摘要数据基本就可以用来唯一标识原数据,所以常常也将摘要称为指纹。为什么要说基本呢?因为毕竟摘要数据是定长的(比如 SHA1 长度为160位),也就是有限的,而原数据是无限的,就像人的指纹一样,人是子子孙孙无穷尽也,难保人类史上不会出现两枚同样的指纹。所以两段不同的数据也有可能有相同的摘要,只是我们很难人为制造出这种结果罢了。

摘要算法,从摘要是无法推出原数据的,就像你从文章摘要无法推出文章原文一样。

如果再为摘要算法加上一个限制条件,那么就得到了我们加密学里的加密摘要算法(一般叫加密哈希算法)。这个条件是:给定一段摘要,你无法(或很难)找到一段数据,使它的摘要等于给定的摘要。

常见的加密摘要算法有 MD5、SHA1、SHA256,后面的内容以 SHA1 举例。

加密摘要算法

防伪设计

现在我们用加密摘要算法来做防伪,比如我有一段 data,我在服务端上存了个密钥 “qwert”,我把密钥 “qwert” 拼在 data 后面做一个 SHA1 摘要算法,得到一个摘要,这个摘要是用原数据混合了我的密钥得来的,所以可以说是我的一个签名,这里把它叫 sig。然后把 data 和 sig 拼接起来发放给客户端。客户端每次请求将 data 和 sig 带上,服务端收到后,如同之前的签名操作一样,将密钥 “qwert” 拼在 data 后面做 SHA1 摘要得到一个签名 sig1,比较这个 sig1 和 客户端带来的 sig 是否相等就知道这个 data 是不是被伪造的了。

签名

每次登录生成的 token 都应该不同,否则,token 一旦被泄露,相当于账号被永久泄露了。为了生成不同的 token,密钥就只能是随机的了。每次登录,我们就为该账号生成一串随机数作为密钥,并将其保存起来。

Token 生成过程

登录成功后,后续请求都将 token 带上,服务端拿到 token 后做相应的解析和校验就可以验证用户的身份了。

Token 验证过程

以上就是 token 认证的一个基本方案,在这个基础上可以根据需要定制,比如在服务端我们只存了临时随机密码,其他信息也是可以存的,比如存登录时间以实现过期等。

如果需要实现多端登录管理,那么我们可以为每个登录的 token 生成一个id,用 token 的 id 来替代上文 token 中的账号 id,当然服务端也需要以 token 的 id 来作为 key 存储,原理是一样的。

最后,最好能放个版本号在 token 里,这样以后需要升级的话可以根据版本号执行不同的方案,无缝升级,完美~