Web App Token 鉴权方案的设计与思考

Web App Token 鉴权方案的设计与思考

作为一种新兴的鉴权方案,Token 作为 Session ID 的替代品有许多天然优势,很多主流产品也从 Session ID 鉴权变成了 Token 鉴权。在我近期完成的产品实践中,也使用了 Token 进行鉴权。作为鉴权方案设计的参与者,我今天与负责该部分业务的同学进行了一些讨论,同时咨询了几位业界老司机,结合我在产品设计之初的一些调研,总结出这篇文章分享给大家。

我不是安全专家,只是在能力范围内对系统安全尽可能地做了优化,考虑不周的地方欢迎大家批评指正。

关于用户系统

对于一个简单的用户系统(不考虑复杂的权限控制,只考虑最单一的“合法用户”的鉴定),其功能其实可以被拆的很简单:注册、登录、鉴权。

  • 注册:用户将用户名和密码交给服务器,并由服务器存储的过程。
  • 登录:用户将用户名和密码交给服务器,服务器鉴定是否正确的过程(在 Token鉴权系统中,这一步如果通过,会生成并返回 Token)。
  • 鉴权:用户将 Token 发送给服务器,服务器校验该 Token 是否合法的过程(不考虑复杂鉴权)。

流程清楚了,我们就来分析一下问题。不考虑前端可能出现的网络抓包等问题,仅从服务器角度考虑,我们可能遇到的安全问题有以下几个:

  • 密码泄漏
  • 生成 Token 的 Secret Key(Salt)泄漏
  • Token 泄漏 / 伪造

归纳一下:我们要解决的最重要的安全问题,就是用户最机密的安全信息被泄漏或伪造。

我们的鉴权设计实践

在我最近完成的产品上,为了规避这些问题,我们在关键步骤上进行了一些处理。整个鉴权系统依赖 Apache Shiro 框架;同时,在密码处理,Token 认证上,我们结合了一些自己的解决方案。

整个流程大致是这样的:(流程图软件到期了 TAT)

一、注册


二、登录


三、鉴权

关于密码加密存储与验证

密码是一定要进行加密存储的。用户系统最核心的数据表,就是包含用户名(ID)、加密后的密码、Salt 的表。Salt 的生成,我们使用了 Shiro 提供的随机字符串生成工具,与用户名连接后,再进行 MD5。然后使用 Salt 加密密码,然后同时保存 Salt 和加密后的密码。

当用户登录时,我们使用 Salt 对用户输入的密码进行加密,再尝试与存储的密码进行匹配。

关于 Token 方案( JWT Token )

我们使用 JWT Token 作为我们的 Token 方案。

一、JWT Token

JWT Token 的全称是 JSON Web Token。一个 JWT Token 由三部分构成:Header,Payload,Signature。Header 规定了 Token 使用的加密方式与 Token 的类型,Payload 是 Token 中包含的用户信息(用户名,过期时间等),Signature 是 Header 的 Base64 值 + Payload 的 Base64 值 + Secret Key 生成的字符串,再对该字符串使用 Header 中规定的散列方式(HS256 或 RS256)取散列值后得到的字符串。一个典型的 JWT Token 是这个样子的:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

生成方法大致是这样的:

// HEADER
{
  "alg": "HS256",
  "typ": "JWT"
}
// PAYLOAD
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
// SIGNATURE
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),  
  secret
) 

Header、Payload 和 Signature 用 . 分隔。

验证 Token 的时候,我们只需要将前两段(即 Header 和 Payload 的 Base64)加上 Secret Key,然后按照 Header 规定的加密方式进行加密,将生成的字符串与第三段(Signature)比对即可。当然,Token 验证的实践上,不同的项目存在一些分歧:有些人会将生成的 Token 直接存在数据库(比如 Redis)里,然后通过 Query 的方式验证是否合法。这一点我们随后讨论。

一个 JWT Token 唯一不可见的部分,就是 Secret Key。它是保证这个 Token 合法且安全的唯一字段。拿不到 Secret Key ,就无法生成 Token,也无法验证 Token。这种 Token 机制很常见(HTTPS 的握手过程就类似这样,SSH 连接也是 - 私钥只有一方持有),难点在于,如何生成并管理 Secret Key。

二、Secret Key

首先,所有用户使用相同的 Secret Key 一定是不合理的。所以我们要解决的第一个问题是,如何为每一个用户生成唯一的 Secret Key ?

还记得刚才的 Salt 么?每个用户的 Salt 都是唯一的,我们使用 Salt ,但不直接使用 Salt 作为 Secret Key。我们使用 Salt + 加密后的密码,再取 MD5 值作为该用户的 Secret Key。每次鉴权前,我们通过这个方式生成 Secret Key,再使用 Secret Key 进行鉴权。

安全性分析

整套系统的安全之处在于,我们没有将任何敏感信息本地化。假设一种最坏的情况:攻击者拿到了我们数据库的全部数据,他能做什么?

  • 获取密码:密码被加密了,而且每个用户使用不同的 Salt 进行加密,加密方法是自定义的,不知道加密方法的话难以破解。
  • 获取 Token:我们没有保存任何的 Token。
  • 获取 Secret Key:Secret Key 是算出来的,即便拿到了 Salt,不知道算法也无法直接得到 Secret Key。

我们避免了直接保存任何安全信息。攻击者拿到的数据,都无法被直接利用。即便尝试破解,代价也是巨大的。

关于 Redis

在我看到的一些实践中,有些项目喜欢使用 Redis 存储生成的 Token,从而简化鉴权流程,提升鉴权效率。这样做可以吗?

我咨询了一位业界专家,同时查阅了相关资料,我给出的答案是:可以,但是不合理,不推荐。

一、避免用 Redis 直接存储 Token

还记得我们安全性分析的前提么:如果攻击者拿到了我们数据库的全部数据,他能做什么?

将 Token 保存在 Redis 中,一定是有风险的。如果服务器被攻破,用户 Token 泄漏的话,在规定的过期时间内,这些被泄漏的 Token 将会使用户账户变得非常危险。

当然,如果系统运行在内网环境,或者系统本身对用户安全的要求不高,这种方案从某种程度上讲,确实可以提升鉴权效率,简化鉴权流程。但是鉴于其可能存在的安全问题,我不推荐。

二、可以用 Redis 缓存 Salt

在我们的产品设计中,我们使用 Salt 计算 Secret Key,然后再进行 Token 认证。我们可以在用户登录时把 Salt 缓存到 Redis 中以提升查询效率。

进一步优化

一、使用 Payload 生成 Secret Key

现在,整个系统的安全性基本可靠了。但是,仔细分析系统的设计,还是有一点问题:每次鉴权都需要去查询 Salt,I/O 开销比较大。这恰恰也是有些人使用 Redis 的原因之一 —— 提升查询速度。

仔细分析一下,我们用 Salt 当做了生成 Secret Key 的 Seed ,目的在于保证 Secret Key 唯一,同时不直接存储 Secret Key 。但其实,保持 Secret Key 唯一的方式有很多,不一定要通过 Salt 。实际上只有登录操作必须依赖 Salt,鉴权操作完全可以使用别的机制。

我们可以使用 JWT 的 Payload 中的某些字段,通过特定算法生成 Secret Key。比如:有效期时间戳 + 用户名,再取 SHA256 散列值(当然可以更复杂,不过要注意性能开销)。因为生成 Secret Key 的算法是不透明的,所以 Secret Key 也是相对安全的。

如果对把生成 Token 的信息放在 Payload 中心存顾虑的话,我们可以在服务器上通过静态配置文件的方式设置固定的 Secret Salt ,配合 Payload 生成 Secret Key。

通过这样的方式,我们可以避免在鉴权阶段对数据库进行访问,提升响应效率。我们也可以利用 Secret Salt 进行细粒度的权限角色划分,在此就不赘述了。

二、更标准的密码加密模式

关于密码加密等方式,我的老师给了我一些建议:可以使用 Blowfish 算法进行对称加密。这样的加密更标准,更安全。

JWT Token 与前端

一、JWT Token 应该放在哪

官方建议使用 Bearer 的模式,即:

Authorization: Bearer <token>

二、合理使用 Payload,避免 Token 过长

JWT Token 是有 Payload 的,这从一定程度上会造成 Payload 滥用。我在 Chrome 上遇到一个奇怪的 Bug:如果 Authorization 过长,Chrome 传递这个字段的时候会发生截断。我们的产品刚开始研发的时候,过度依赖 JWT 的 Payload 传递用户基本信息(用户名、所属用户组、邮箱等),造成 Token 长度非常长。后来对 Token 进行了几次瘦身,才避免了 Chrome 上的 Bug。

三、前端真的需要依赖 Payload 吗

答案是否定的。前端并不关心,也不应该关心 Token 的 Payload 是什么,真正使用 Payload 的应该是后端。前端获取用户信息的方式,应当是在用户登录的时候,由服务器作为 HTTP Response 回传,并使用 Cookie / Local Storage / Session Storage 进行持久化存储,而不是通过解析 Token 的 Payload 获得。


以上就是我对 Web App Token 鉴权方案的一些思考。以下是有关 Web App Token 认证的文档:

RFC 6749: The OAuth 2.0 Authorization Framework

RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage

RFC 7519 JSON Web Token (JWT)

编辑于 2017-08-04

文章被以下专栏收录