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

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

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

关于用户系统

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

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

安全问题

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

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

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

我们的鉴权设计实践

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

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

注册

Register

登录

Login

鉴权

Authentication

关于密码加密存储与验证

密码是一定要进行加密存储的。用户系统最核心的数据表,就是包含用户名(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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容