在搭建中小型业务系统时,访问Token的鉴权认证往往与后台服务紧密关联。考虑采用Openresty设计一套业务后台无感知鉴权的流程方案,让后台开发人员能够专注于业务逻辑开发,无需担忧鉴权逻辑的设计。
主要借助Openresty自带的lua脚本模块来实现拦截鉴权逻辑。通过这种途径,可以将鉴权逻辑从后台服务中分离出来,实现鉴权的独立处理,进而简化后台服务的复杂度,提升系统的整体性能与可维护性。
业务系统的架构一般情况下如下图所示
(一)用户登录的认证流程
首先梳理一下登录流程,逻辑流程如下:
1. 请求登录接口
当进行前端页面登录接口的请求时,网关不会直接给予免认证放行,而是运用接口签名校验方案。在请求登录接口时,需要携带签名(例如 Authorization: Signature <签名>),签名方式可选用 HMAC-SHA256。 采用 HMAC 签名方式时,一个简便的实现办法是在接口请求的 Header 里携带请求时间戳和 Nonce,同时在 Authorization 中携带 HMAC-SHA256(Host + URI + 时间戳 + Nonce) 的哈希消息码。
2. 网关拦截校验
Openresty 的 Lua 脚本会对所有请求进行拦截。若请求的是登录接口,会对时间的有效性、是否为重放请求以及 Authorization 签名的有效性进行校验。
3. 生成设备指纹
简易的设备指纹能够直接借助 ngx.md5(user-agent | accept - encoding | accept - language) 来生成。要是仅面向内部网络进行访问,不向外部互联网开放,可在 ngx.md5 里增添 remoteAddr。 把 X-Fingerprint 的值设定为 ngx.md5(user-agent | accept - encoding | accept - language),并且将其置于 Header 之中,传递给后端服务。 不过,当不同设备的网络特征极为相似(例如浏览器版本相同、网络环境一致)时,模块或许会生成相同的指纹。此实现方案的准确率大约为 60%,不适用于需要精准追踪用户的业务,不过对基础爬虫识别以及 CC 攻击防御能起到一定的功效。
4. 账号密码校验
当授权认证服务收到用户使用账号密码发起的登录请求后,会进行账号密码的校验、登录防爆破策略的校验(像账号密码输入错误的次数、验证码的发送间隔、验证码错误的次数、同一 IP 下多账号登录等情形),同时记录登录风险(比如同一账号频繁登录、常用 IP 发生变更、常用地理位置改变、登录时间出现变动等状况)。
5. sessionId及accessToken
在账号密码验证通过之后,授权认证服务会采用 JWT 方案来生成 sessionId 与 accessToken。可将用户 ID、设备指纹、生成时间以及过期时间录入 JWT 中,方便网关进行解析和校验。 sessionId:体现用户的登录状态,具有长期有效性(一般为 7 天或者更长时间),存放在安全的地方(例如 HttpOnly Cookie 或者本地存储),不参与常规请求,能减少被截获的风险。 accessToken:具备短期有效性(通常为 15 - 30 分钟),用于访问受保护的资源(如 API、用户数据)。直接嵌入请求头(例如 Authorization: Bearer ),尽管传输较为频繁,但泄露风险的时间窗口较小。 注意:若用户主动登出或者修改密码,授权认证服务端需主动注销缓存里的 sessionId 和 accessToken。
6. 登录成功
登录成功后,接口会返回 accessToken 供前端页面使用,并且会把 sessionId 写入 Cookie 中。前端页面拿到 accessToken 之后,就能够访问业务接口。
(二)用户访问业务系统的鉴权逻辑流程
前端页面请求业务接口认证流程步骤
1. 前端发起请求
当前端页面发起业务请求时,拦截器会统一携带 accessToken、Timestamp、Nonce 等信息。这些信息的携带形式如下。
js
Authorization: Bearer <accessToken>
X-Nonce: <随机字符串> # 防重放攻击
X-Timestamp: <请求时间戳> # 客户端时间戳
2. 网关拦截验证
网关会对所有业务服务请求进行拦截,并按以下步骤开展校验工作:
(1)时间戳校验:具体规则是将 X-Timestamp 与服务器时间进行比较,若两者误差超过 ±5 分钟,该请求将被拒绝。
(2)Nonce 防重放:检查缓存中是否存在该 Nonce,若存在,则认定此为重复请求。
(3)JWT 解析与验证:对 accessToken 进行解析,获取用户 ID、设备指纹以及过期时间。查看 JWT 是否已过期(依据 exp 字段判断);查询 Redis 缓存,确认 accessToken 是否存在且有效;核实 JWT 中的设备指纹与请求来源的指纹是否一致。
(4)认证通过:若 AccessToken 认证成功,会把 JWT 解析得到的用户 ID 设置到新的 Header(例如 X-User-ID)中,便于业务服务获取使用,随后将请求转发至业务服务。
3. 业务服务处理
业务服务会对请求进行处理,并返回相关的业务结果数据。
(三)AccessToken过期请求刷新逻辑流程
前端页面拦截器,在401响应后自动发起刷新accessToken请求,避免用户感知中断。请求刷新accessToken逻辑如下所示
前端页面请求刷新AccessToken流程步骤
1. 前端发起刷新Token请求
当页面前端发起刷新Token请求时,拦截器会统一携带 Cookie、Timestamp、Nonce 信息进行处理。
js
Cookie: sessionId=<value> # 会话标识
Headers:
X-Nonce: <随机字符串> # 防重放攻击
X-Timestamp: <请求时间> # 客户端时间戳
2. 网关拦截验证
(1)时间戳校验:具体规则是将 X - Timestamp 与服务器时间作比较,若两者误差超出 ±5 分钟,请求将被拒绝。
(2)Nonce 防重放:对缓存进行检查,查看其中是否存在该 Nonce,若存在,则认定为重复请求。
(3)Cookie 中 SessionId 检查:查看 Cookie 里是否存在 sessionId;验证 JWT 是否已过期(依据 exp 字段);查询 Redis 中是否有该 sessionId 的缓存记录;对比缓存中的设备指纹与当前请求的设备指纹是否相符。
(4)若认证通过,网关会解析 JWT 以获取用户 ID,并把请求转发至授权认证服务。
3. 重新生成AccessToken
授权认证服务会生成新的 accessToken(JWT,包含用户 ID、设备指纹,有效期为 30 分钟),将新的 Token 存入 Redis 缓存,再把新的 AccessToken 返回给前端。