FIDO
FIDO(Fast Identity Online,快速在线身份验证)是一套开源的身份验证标准,旨在替代传统的用户名 / 密码登录方式,解决密码易泄露、管理复杂等问题。它通过公私钥加密技术和双因素认证(如生物识别、安全密钥等)实现更安全、便捷的身份验证。
1 FIOD 的核心标准
FIDO 联盟(FIDO Alliance)制定了两大核心标准:
- FIDO UAF(Universal Authentication Framework,通用认证框架)
-
目标:完全替代用户名 / 密码,支持生物识别(指纹、面部识别)、硬件密钥(如 USB Key)等认证方式。
-
特点:
- 客户端直接与服务端通信,无需依赖中间认证服务器。
- 支持跨设备、跨平台认证(如手机、电脑)。
- 典型应用:Windows Hello、Touch ID/Face ID 在部分网站的登录。
- FIDO U2F(Universal 2nd Factor,通用第二因子)
-
目标:作为传统密码的二次认证因子,增强现有登录流程的安全性(如 “密码 + 安全密钥” 组合)。
-
特点:
- 需配合用户名 / 密码使用,提供双因素认证(2FA)。
- 通过 USB、NFC 或蓝牙连接硬件密钥(如 YubiKey、Google Titan Key)。
- 典型应用:GitHub、Google、Microsoft 等平台的二次认证。
2 FIDO 的工作原理
2.1 FIDO工作原理概述
FIDO 的工作原理主要通过以下步骤实现:
- 注册阶段:用户将支持 FIDO 的设备(如生物识别设备、智能手机、安全密钥等)与在线服务进行注册。此时,设备会生成一对唯一的公私钥对,公钥会被发送并存储到服务端,与用户账户相关联,而私钥则安全地保存在用户设备中。
- 认证请求阶段:当用户尝试登录在线服务时,服务端会向用户设备发送一个唯一的挑战(Challenge)消息,该消息用于验证用户的身份。
- 认证响应阶段:用户设备收到挑战消息后,会使用存储在本地的私钥对挑战进行签名(用户通过物理按键(或生物识别)确认操作),然后将签名后的响应消息发送回服务端。服务端收到响应后,会使用之前注册的公钥来验证签名的有效性。如果签名验证成功,服务端就会确认用户的身份合法,允许用户登录。
以某银行 APP 使用 FIDO 认证为例:
- 注册:用户在银行 APP 中开启 FIDO 认证功能,将自己的手机(作为认证设备)与银行账户进行绑定。手机生成公私钥对,公钥上传至银行服务器,与用户账户信息关联存储,私钥则保存在手机的安全区域,如可信执行环境(TEE)或安全芯片中。
- 登录认证:当用户下次登录银行 APP 时,APP 向服务器发送登录请求,服务器返回一个挑战消息给 APP。用户通过手机的指纹识别或其他生物识别方式(如面部识别)解锁手机中的私钥,然后手机使用私钥对挑战消息进行签名,并将签名结果发送给银行服务器。服务器收到签名后,用之前注册的公钥进行验证,若验证通过,则确认是合法用户,允许用户登录并访问账户信息和进行相关操作。
在这个案例中,FIDO 认证通过公私钥加密技术和用户的生物特征识别,实现了安全、便捷的登录认证过程,无需用户输入传统的用户名和密码,降低了密码泄露的风险,同时也提升了用户体验。
2.2 FIDO 认证流程中服务端与客户端交互细节
以下是关于 FIDO 认证流程中服务端与客户端交互细节 的详细解析,涵盖挑战消息内容、公私钥使用、签名生成及服务端验证逻辑:
(1)服务端发送挑战消息(Challenge)的内容
在 FIDO 认证(如 FIDO2/WebAuthn)中,服务端发起认证请求时会生成并发送挑战消息,用于验证用户的私钥持有性。以下是挑战消息的核心内容及作用:
1.必须包含的基础信息
| 字段 | 说明 |
|---|---|
| challenge | - 随机生成的字节串(通常为 16-32 字节),用于防止重放攻击。 - 客户端需用私钥对该值签名,服务端通过验证签名确认客户端确实持有私钥。 |
| rpId | - 依赖方(Relying Party,即应用服务端)的域名(如example.com)。 - 用于确保客户端不会将认证信息误发给其他服务端。 |
| userHandle | - 用户在服务端的唯一标识(如 UUID),关联用户的公钥信息。 - 注意:在 FIDO2 中,userHandle是可选的,但通常用于多用户场景。 |
| allowCredentials | - 允许使用的认证器列表(包含公钥 credential ID)。 - 服务端通过此列表指定用户可使用的安全密钥(如 U2F 设备、手机指纹等)。 |
| timeout | - 认证操作的超时时间(毫秒),防止请求长时间挂起。 |
2.可选扩展字段
- extensions:用于传递自定义参数(如应用特定的权限校验信息)。
- pubKeyCredParams:指定支持的公钥算法(如 ES256、RS256 等),通常在注册阶段而非认证阶段使用。
3.是否携带公钥?
-
认证阶段不携带公钥
公钥在注册阶段已由客户端生成并存储在服务端(关联到用户的
userHandle或credential ID)。认证时,服务端通过allowCredentials字段指定对应的credential ID,客户端根据该 ID 找到本地存储的私钥进行签名。- 优势:避免公钥在认证阶段传输,减少数据泄露风险。
(2)客户端生成签名的过程
客户端(如浏览器或安全密钥设备)收到挑战消息后,需用私钥对挑战内容进行签名,具体步骤如下:
1.准备签名数据
-
客户端将挑战消息中的关键字段组合成 待签名的数据结构,通常包括:
{ "challenge": "随机字节串", "origin": "服务端域名(如https://example.com)", "rpId": "example.com", "userHandle": "用户唯一标识", "type": "webauthn.get" // 表示认证操作 } -
该数据会被序列化为 CBOR 格式(FIDO 协议推荐的二进制格式)或 JSON,并进行哈希(如 SHA-256)生成摘要。
2.使用私钥生成签名
- 私钥存储位置: 私钥通常存储在客户端的安全硬件(如 TPM 芯片、手机 SE 安全元件)或操作系统的安全密钥库(如 Windows Hello、macOS Keychain)中,无法被应用直接读取,只能通过系统 API 调用私钥进行签名。
- 签名算法: 根据注册阶段协商的算法(如 ES256、RS256)使用私钥对摘要进行签名,生成 签名结果(通常为字节数组,如 ECDSA 的 r+s 值或 RSA 的签名值)。
3.返回给服务端的认证响应
客户端将签名结果与其他信息打包为 AuthenticatorAssertionResponse,包含:
credentialId:认证器的唯一标识(关联服务端存储的公钥)。signature:对挑战消息的签名值。userHandle:用户标识(可选)。clientDataJSON:客户端生成的附加数据(包含challenge、origin等,用于服务端校验一致性)。
(3)服务端验证签名的流程
服务端收到客户端的认证响应后,需通过以下步骤验证签名合法性及用户身份:
1.解析客户端数据(Client Data)
-
解码
clientDataJSON,校验以下内容:- challenge 一致性:确保客户端返回的
challenge与服务端发送的完全一致,防止中间人篡改。 - origin 合法性:验证
origin是否为服务端域名,防止跨站攻击。 - type 正确性:确保操作类型为
webauthn.get(认证),而非注册等其他操作。
- challenge 一致性:确保客户端返回的
2.查找用户公钥
- 根据
credentialId从服务端存储中获取对应的 公钥 和 用户信息(如userHandle)。 - 若
credentialId无效或未找到公钥,认证直接失败。
3.验证签名
-
组装待验证数据: 将
clientDataJSON的哈希值与credentialId等信息组合,作为公钥验证的输入(具体格式需符合 FIDO 协议标准)。 -
使用公钥验证签名:
通过公钥算法(如 ES256、RS256)解签客户端返回的
signature,验证其是否与待验证数据匹配。-
若匹配成功,证明客户端确实持有对应的私钥(即用户通过生物识别或 PIN 码解锁了私钥存储设备)。
服务端的解签过程仅用于验证签名是否由对应的私钥生成,而无法获取私钥本身。这是公钥密码学的核心特性之一
为什么解签无法反推私钥?
(1)现代签名算法(如 ECDSA、EdDSA)均基于单向陷门函数,即:
- 已知私钥时,生成签名是高效的(多项式时间);
- 仅已知公钥和签名时,反推私钥是计算上不可行的(指数级时间复杂度)。
- 例如:破解 256 位的 ECDSA(P-256 曲线)需要的计算量约为2128次操作,远超当前超级计算机的能力。
(2)签名验证的输入限制: 服务端解签时的输入是:
- 公钥(Q)、消息哈希(H(m))、签名(r,s)。 这些数据中不包含任何私钥的直接或间接信息(你可能会有疑问,签名不就是由私钥参与生成的吗?虽然如此,但是这不影响签名中不包含任何私钥的直接或间接信息,这是由签名算法的数学原理和单向函数特性实现的。),验证过程仅是对 “私钥是否参与签名生成” 的逻辑判断,而非数学上的逆运算。
实际应用中的安全性保障
-
私钥的物理隔离: 在 FIDO 等安全协议中,私钥通常存储在 安全元件(SE) 或可信执行环境(TEE) 中,无法被操作系统或应用层读取,从源头杜绝私钥泄露风险。
- 例如:手机的指纹模块、U 盾的硬件芯片均通过物理安全机制保护私钥。
-
对抗 “已知签名攻击” : 即使攻击者获取了多个(消息,签名)对,也无法通过数学方法反推私钥(因签名算法的抗碰撞性和单向性)。
- 这也是为什么 FIDO 允许同一私钥对不同挑战值生成签名,而不必担心私钥泄露。
-
4.附加验证(可选)
- 用户验证状态:检查客户端是否通过生物识别或 PIN 码完成用户验证(FIDO 协议中的
userVerified字段)。 - 认证器安全策略:如要求认证器必须为平台认证器(内置在设备中)或跨平台认证器(如 USB 密钥)。
(4)与 JWT 的关联(扩展说明)
-
FIDO 认证与 JWT 的关系:
FIDO 认证完成后,服务端通常会生成 JWT 令牌(包含用户身份、权限等声明),用于后续接口请求的身份验证。
- JWT 的签名验证:与 FIDO 的签名逻辑类似,服务端通过 JWT 的公钥(或对称密钥)验证令牌签名,解析声明(如
sub用户标识、exp过期时间)以确认用户合法性。 - 区别:FIDO 签名用于证明私钥持有性(硬件级认证),JWT 用于传输阶段的身份断言(令牌级验证)。
- JWT 的签名验证:与 FIDO 的签名逻辑类似,服务端通过 JWT 的公钥(或对称密钥)验证令牌签名,解析声明(如
通过上述流程,FIDO 协议确保了用户私钥的安全存储与签名验证的不可抵赖性,而服务端通过挑战 - 响应机制和公钥密码学,在无需存储用户密码的前提下完成强身份认证。
3 FIDO 的优势
-
安全性更高:
- 无需存储密码,避免拖库泄露风险。
- 私钥永不离开设备,黑客无法通过网络窃取。
- 依赖物理验证(如按键确认),防止钓鱼攻击。
-
便捷性更强:
- 一键认证(生物识别或硬件密钥),无需记忆密码。
- 支持多设备同步,跨平台使用(如手机扫码登录电脑)。
-
标准化与兼容性:
- 开源协议,支持主流浏览器(Chrome、Firefox、Edge)和操作系统(Windows、macOS、Linux)。
- 硬件密钥可通用(如 YubiKey 支持数千个应用)。
4 FIDO 的应用场景
-
企业办公:
- 替代传统 VPN 密码,通过安全密钥实现远程办公认证。
- 案例:微软 Azure AD、Okta 等身份管理平台支持 FIDO。
-
互联网服务:
- 社交媒体、云服务(如 Facebook、Dropbox)使用 FIDO 作为二次认证选项。
-
移动设备:
- 手机指纹 / 面部识别直接用于 APP 登录(如银行 APP、电商平台)。
-
物联网(IoT) :
- 设备通过 FIDO 认证接入网络,避免弱密码导致的安全漏洞。
5 FIDO 与传统认证的对比
| 维度 | 传统密码 | FIDO 认证 |
|---|---|---|
| 安全性 | 依赖密码强度,易泄露 | 基于公私钥加密,无密码存储 |
| 便捷性 | 需记忆多套密码 | 一键生物识别或硬件密钥 |
| 抗钓鱼能力 | 易受钓鱼攻击 | 通过挑战 - 响应机制抵御钓鱼 |
| 部署成本 | 低(无需额外硬件) | 需采购安全密钥或生物设备 |
6 FIDO认证服务端实现
在服务端实现 FIDO 认证时,存储和架构设计需要考虑安全性、扩展性和性能。
6.1 FIDO服务端的核心存储内容
FIDO 认证的服务端需要存储以下关键信息(以安全且防篡改的方式):
(1)公钥凭证(Public Key Credentials)
-
内容:用户注册时生成的公钥、凭证ID(Credential ID)、凭证类型等。
-
存储方式:
- 数据库(如MySQL、PostgreSQL):存储结构化数据,支持索引查询。
- 加密存储:公钥通常无需加密,但 凭证ID 可能需通过 HMAC 等方式防篡改。
(2)用户与凭证的绑定关系
-
内容:将 用户ID 与 公钥凭证 关联,可能包含多设备支持(如手机、安全密钥)。
-
示例结构:
{ "userId": "user123", // 用户ID "credentials": [ // 公钥凭证 { "credentialId": "BASE64_ENCODED_ID", // 凭证ID "publicKey": "BASE64_ENCODED_PUBLIC_KEY", //公钥 "type": "public-key", // 凭证类型 "counter": 0, // 防重放攻击的签名计数器 "attestationFormat": "packed", "createdAt": "2023-01-01T00:00:00Z" } ] }
(3)签名计数器(Signature Counter)
- 作用:记录每次认证的签名次数,用于检测重放攻击(如设备丢失后被滥用)。
- 存储要求:原子性递增(需数据库或分布式锁支持)。
6.2 微服务场景下的架构设计
在微服务架构中,FIDO 认证通常有两种实现方式:
6.2.1 独立的认证服务(推荐)
架构:
[客户端] → [API网关] → [认证服务(FIDO)] ←→ [用户服务]
↓
[数据库/缓存]
职责:
- 专门处理所有认证相关逻辑(包括 FIDO 注册、验证)。
- 提供标准化的认证接口给其他微服务调用。
优势:
- 解耦:核心认证逻辑独立,便于维护和扩展。
- 安全隔离:敏感数据(如公钥)集中管理,降低泄露风险。
- 复用性:支持多业务线共享认证能力(如 APP、Web、IoT 设备)。
6.2.2 集成到用户服务中
架构:
[客户端] → [API网关] → [用户服务(含FIDO模块)]
↓
[用户数据库]
适用场景:
- 认证逻辑简单,与用户管理强耦合。
- 初期快速迭代,后续可能拆分为独立服务。
风险:
- 服务职责过重,可能影响性能和可维护性。
- 安全漏洞可能影响整个用户服务。
6.2.3 关键技术实现细节
(1)数据库设计
CREATE TABLE fido_credentials (
user_id VARCHAR(255) NOT NULL,
credential_id BYTEA NOT NULL, -- 二进制存储凭证ID
public_key BYTEA NOT NULL, -- 二进制存储公钥
counter BIGINT NOT NULL DEFAULT 0,
transports TEXT[], -- 认证传输方式(USB、NFC等)
created_at TIMESTAMP NOT NULL,
PRIMARY KEY (user_id, credential_id)
);
(2)加密建议
- 凭证ID 和 公钥 使用数据库字段级加密(如 AWS KMS、Vault)。
- 敏感字段避免明文日志记录。
(3)分布式环境下的计数器管理
-
问题:多实例环境下,签名计数器需保证原子性递增。
-
解决方案:
- 数据库事务:使用
UPDATE ... RETURNING语句(如 PostgreSQL)。 - 分布式锁:通过 Redis 或 ZooKeeper 实现锁机制。
- 缓存优化:短期缓存计数器值,定期持久化到数据库。
- 数据库事务:使用
(4)与其他微服务的集成
认证令牌生成:
-
FIDO 验证通过后,生成 JWT 或 OAuth 令牌,传递给其他服务。
-
示例流程:
1. 客户端完成FIDO认证 → 2. 认证服务生成JWT → 3. 客户端携带JWT访问其他微服务 → 4. 资源服务通过认证服务验证JWT有效性
服务间通信:
- 通过 gRPC 或 REST API 暴露认证接口。
- 使用服务发现(如 Consul、Nacos)注册认证服务。
7 关于FIDO认证的问答
(1)通常用户在登录app的时候会进行FIDO认证,那么在用户后续使用app的过程中是不是不会触发FIDO认证了?
用户在登录 App 时完成 FIDO 认证后,后续是否会再次触发 FIDO 认证取决于应用的安全策略和使用场景。以下是详细分析:
当次认证不再触发的场景
(1)基于会话令牌的短期信任
机制:
- 用户通过 FIDO 认证后,服务端生成短期会话令牌(如 JWT),并返回给客户端。
- 客户端在令牌有效期内(如 30 分钟)访问应用时,只需携带令牌,无需重复进行 FIDO 认证。
示例流程:
登录阶段:
客户端 → FIDO认证(指纹/安全密钥)→ 服务端验证通过 → 颁发JWT(有效期30分钟)
后续访问:
客户端携带JWT访问接口 → 服务端验证JWT有效 → 直接返回数据
适用场景:
- 普通功能访问(如浏览页面、查看非敏感数据)。
- 对安全性要求适中,希望提升用户体验的场景。
(2)设备绑定与可信设备标记
-
机制:
- 首次认证时,服务端将用户设备标记为 “可信设备”(通过设备指纹、硬件 ID 等)。
- 后续在同一设备上访问时,默认信任设备,仅需验证会话令牌,无需重复 FIDO 认证。
-
风险:
- 设备一旦被标记为可信,若设备丢失或被入侵,可能导致未授权访问。
- 需配合设备端安全措施(如设备加密、生物识别锁)降低风险。
需要重新触发 FIDO 认证的场景
1. 敏感操作强制二次认证
触发条件:
- 用户进行高风险操作时(如修改密码、转账、访问隐私数据),即使处于有效会话中,仍需重新进行 FIDO 认证。
示例:
- 用户在 App 中发起 “修改登录手机号” 操作 → 系统提示 “请验证身份” → 触发 FIDO 认证(指纹 / 安全密钥)。
实现逻辑:
- 敏感接口在服务端校验时,除验证会话令牌外,额外检查是否满足 “最近一次 FIDO 认证时间在 X 分钟内”。
- 若超时或未进行过认证,则拒绝请求并要求重新认证。
2. 会话超时或令牌过期
触发条件:
- 会话令牌过期(如超过 24 小时未操作),用户继续使用 App 时需重新登录并完成 FIDO 认证。
示例:
- 用户上午登录 App 后未操作,下午再次打开时 → 会话已过期 → 提示 “请重新登录” → 触发 FIDO 认证。
3. 多因素认证(MFA)策
机制:
- 应用要求每次登录或关键操作必须通过多因素认证(如 FIDO + 短信验证码)。
- 即使会话未过期,用户切换设备或退出后重新登录时,仍需再次完成 FIDO 认证。
适用场景:
- 金融类 App、企业内部系统等高安全等级场景。
4. 设备环境变更
触发条件:
- 用户从新设备、新 IP 地址或异常地理位置访问时,系统检测到环境变更,强制要求 FIDO 认证。
示例:
- 用户在海外首次登录 App → 系统识别为异常环境 → 触发 FIDO 认证 + 短信验证。
核心设计原则
-
安全与平衡体验:
- 普通操作通过会话令牌减少认证频率,提升体验;
- 敏感操作或环境风险升高时,强制 FIDO 认证以保障安全。
-
动态风险评估:
- 结合设备指纹、行为分析(如登录频率、操作类型)动态决定是否需要重新认证。
-
合规性要求:
- 遵循行业标准(如 GDPR、金融合规),对特定操作强制要求强认证。
场景描述
用户 A 使用银行 App 进行日常操作,流程如下:
-
首次登录:
- 输入用户名 → 选择 “指纹登录”(FIDO 认证)→ 验证通过 → 获得 JWT(有效期 2 小时)。
-
常规操作(无需重新认证) :
- 查看账户余额、交易记录 → 携带 JWT 访问接口 → 服务端验证令牌有效 → 返回数据。
-
敏感操作(强制认证) :
- 发起 10 万元转账 → 服务端检测到操作风险 → 提示 “请验证身份” → 触发指纹认证(FIDO)→ 验证通过后执行转账。
-
会话过期:
- 2 小时未操作 → JWT 过期 → 再次打开 App 时 → 提示 “请重新登录” → 需重新完成 FIDO 认证。
(2)客户端拿到FIDO认证后的JWT,是怎么进行存储的?
客户端拿到 FIDO 认证后的 JWT(JSON Web Token)后,存储位置的选择需平衡安全性与可用性。以下是常见的存储方案及其适用场景:
前端存储方案(Web/App)
1. Http Cookie(安全级别中等)
存储方式:
将 JWT 存储在 Cookie 中,并设置以下属性增强安全性:
HttpOnly: 防止 JavaScript 脚本访问,避免 XSS 攻击。Secure: 仅通过 HTTPS 传输,防止中间人攻击。SameSite: 限制跨域请求时 Cookie 的发送,防御 CSRF 攻击。
适用场景:
- 传统 Web 应用(如网站),依赖 Cookie 进行会话管理。
风险:
- 若未正确设置
HttpOnly,可能被 XSS 攻击窃取。 - 存在 CSRF 风险(需配合 SameSite 或 CSRF 令牌)。
2. LocalStorage/SessionStorage(安全级别较低)
存储方式:
// 存储JWT
localStorage.setItem('jwt_token', token);
// 读取JWT
const token = localStorage.getItem('jwt_token');
特点:
- LocalStorage:长期存储(除非手动删除)。
- SessionStorage:会话结束(浏览器关闭)后清除。
风险:
- XSS 攻击:JavaScript 可直接访问存储内容,易被窃取。
- 持久性风险:LocalStorage 中的数据可能被恶意脚本长期利用。
适用建议:
- 仅在无敏感信息的场景使用,或对 JWT 进行加密存储(如结合
crypto.subtleAPI)。
3. IndexedDB(安全级别较高)
存储方式: 浏览器提供的异步数据库,可存储结构化数据(包括加密后的 JWT)。
优势:
- 相比 LocalStorage,IndexedDB 有更严格的访问控制和安全模型。
- 支持数据加密存储(需配合加密库如
crypto-js)。
风险:
- 仍需防范 XSS 攻击(若加密密钥泄露,数据可能被解密)。
移动端存储方案(App)
1. 安全存储(iOS/Android)
iOS:
使用Keychain Services存储 JWT,系统级加密保护,防逆向工程。
// Swift示例:存储到Keychain
let query = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: "jwt_token",
kSecValueData: token.data(using: .utf8)!
] as CFDictionary
SecItemAdd(query, nil)
Android:
使用Android Keystore结合SharedPreferences,加密存储敏感数据。
// Java示例:使用Android Keystore加密
SecretKey key = generateKey();
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedToken = cipher.doFinal(token.getBytes());
优势:
- 设备级加密,数据存储更安全。
- 支持生物识别(如指纹)解锁访问
2. 偏好存储(需加密)
iOS: UserDefaults(未加密,需自行加密 JWT 后存储)。
Android: SharedPreferences(未加密,需配合加密库如Tink)。
风险:
- 明文存储存在风险,建议结合设备加密(如 Android 的
EncryptedSharedPreferences)。
混合方案(前端 + 后端)
(1)双重存储(Cookie + LocalStorage)
-
机制:
- 将 JWT 的签名部分存储在 HttpOnly Cookie 中,用户 ID 等非敏感信息存储在 LocalStorage。
- 请求时,客户端合并两部分数据发送给服务端验证。
-
优势:
- 降低 XSS 攻击风险(签名部分无法被 JavaScript 访问)。
- 提升可用性(非敏感信息可快速读取)。
(2)短期缓存 + 服务端会话
-
机制:
- 客户端仅在内存中临时存储 JWT(如 Vuex/Redux 状态管理),不持久化。
- 关键操作时,重新从服务端获取认证状态或刷新令牌。
-
优势:
- 避免本地存储风险,安全性最高。
-
劣势:
- 网络依赖强,频繁请求服务端影响性能。
存储方案对比
| 存储位置 | 安全性 | 持久性 | 防 XSS | 防 CSRF | 适用场景 |
|---|---|---|---|---|---|
| HttpOnly Cookie | 中高 | 会话 / 长期 | ✅ | ❌ | Web 应用 |
| LocalStorage | 低 | 长期 | ❌ | ❌ | 非敏感数据缓存 |
| SessionStorage | 低 | 会话 | ❌ | ❌ | 非敏感数据缓存 |
| IndexedDB | 中高 | 长期 | ✅ | ❌ | 需加密存储的场景 |
| Keychain | 高 | 长期 | ✅ | ❌ | iOS App |
| Android Keystore | 高 | 长期 | ✅ | ❌ | Android App |
| 内存存储 | 最高 | 会话 | ✅ | ❌ | 高安全需求的临时操作 |
最佳实践
选择 JWT 存储位置时,需根据应用安全等级和使用场景权衡:
- 高安全性场景(如金融 App):优先使用 iOS Keychain/Android Keystore + 加密存储。
- Web 应用:推荐 HttpOnly Cookie + SameSite 属性,结合 CSRF 防护。
- 临时会话:使用内存存储或 SessionStorage,减少持久化风险。
无论采用哪种方案,均需配合服务端的令牌验证机制和安全审计,形成完整的安全闭环
(3)在客户端已经登录(即拿到了FIDO认证的JWT)场景下,客户端访问服务端时,服务端是怎么验证这个用户是合法的,需要调用FIDO服务吗?
在客户端持有 FIDO 认证后的 JWT 访问服务端时,服务端的验证流程通常不需要再次调用 FIDO 服务,而是基于 JWT 本身的签名和声明信息进行验证。以下是详细的验证过程和技术细节:
JWT 验证的核心流程
服务端验证 JWT 的合法性主要基于以下步骤,无需依赖 FIDO 服务:
1.签名验证
-
目的:确保 JWT 未被篡改或伪造。
-
流程:
- 服务端使用签名密钥(通常是私钥的公钥部分)对 JWT 的签名部分进行验证。
- 若签名验证失败,直接拒绝请求。
2.声明(Claims)验证
-
常见验证项:
exp(Expiration Time) :检查令牌是否过期。nbf(Not Before) :检查令牌是否已生效。iss(Issuer) :验证令牌颁发者是否合法。aud(Audience) :验证令牌接收方是否为当前服务。- 自定义声明:如用户角色、权限范围等。
3.会话状态验证
-
可选步骤:
- 服务端可维护一个令牌黑名单(如用户主动登出时),验证 JWT 是否在黑名单中。
- 对于高安全场景,可结合Refresh Token机制定期刷新访问令牌。
Java实现JWT验证示例
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
public boolean validateToken(String jwt) {
try {
// 使用签名密钥解析JWT
Jwts.parser()
.setSigningKey("YOUR_SECRET_KEY") // 生产环境应使用安全存储的密钥
.parseClaimsJws(jwt);
return true;
} catch (ExpiredJwtException e) {
// 令牌过期
return false;
} catch (SignatureException e) {
// 签名无效
return false;
} catch (Exception e) {
// 其他异常
return false;
}
}
是否需要调用FIDO服务
通常不需要。服务端验证 JWT 的合法性是独立于 FIDO 认证流程的,原因如下:
- JWT 自包含验证信息: JWT 的签名部分已经包含了用户身份的证明(通过 FIDO 认证时生成),服务端只需验证签名即可确认用户身份。
- 性能考虑: 每次请求都调用 FIDO 服务会显著增加延迟,降低系统吞吐量。
- 安全边界: FIDO 认证仅用于初始登录,后续会话安全由 JWT 和服务端的会话管理机制负责。
例外情况: 在以下场景中,服务端可能需要额外调用 FIDO 服务:
- 多因素认证(MFA) : 对于高风险操作(如转账),服务端要求用户再次通过 FIDO 验证(此时需调用 FIDO 服务)。
- 令牌绑定验证: 服务端在 JWT 中嵌入了设备指纹或公钥 ID,需验证其与 FIDO 注册信息是否匹配。
JWT验证完整流程
1. 客户端请求:
客户端 → [携带JWT] → 服务端
2. 服务端验证:
a. 提取HTTP Header中的JWT(如Authorization: Bearer token)
b. 验证JWT签名是否有效
c. 检查JWT是否过期
d. 解析JWT中的用户ID等声明信息
e. (可选)验证用户权限是否满足请求要求
3. 响应结果:
- 验证通过 → 返回请求的资源
- 验证失败 → 返回401 Unauthorized错误
安全增强措施
- 使用 Refresh Token: JWT 设置较短的有效期(如 15 分钟),通过 Refresh Token 定期刷新,降低令牌被盗用的风险。
- 令牌绑定(Token Binding) : 将 JWT 与客户端设备的公钥或指纹绑定,验证请求来源的一致性。
- 分布式缓存: 使用 Redis 等缓存存储已验证的 JWT 状态,减少重复验证开销。
- 审计日志: 记录异常的验证失败请求,及时发现潜在的攻击行为。
总结
服务端验证 JWT 的合法性主要依赖于签名验证和声明解析,通常无需再次调用 FIDO 服务。FIDO 认证的作用是在用户登录时生成可信的身份凭证(JWT),而后续的会话管理则由服务端基于 JWT 独立完成。只有在特殊的安全场景(如多因素认证)下,才需要再次触发 FIDO 验证流程。
(4)服务端拿到一个JWT后,它是怎么知道这个JWT是合法的?
在服务端验证 JWT(JSON Web Token)的合法性时,签名验证和声明解析是两个核心环节。以下从技术原理、具体流程和安全逻辑三个维度详细解析,帮助理解服务端如何判断 JWT 是否合法。
签名验证:确保 JWT 未被篡改
(1)签名验证的技术原理
JWT 的签名机制基于密码学哈希算法和非对称加密(或对称加密),其核心逻辑是:
-
生成签名时: 客户端完成 FIDO 认证后,认证服务器(如 FIDO 服务)使用私钥(非对称加密场景)或密钥(对称加密场景),对 JWT 的 头部(Header) 和载荷(Payload) 进行哈希计算,生成签名(Signature),并将其附在 JWT 末尾。 JWT 结构:
[Header].[Payload].[Signature] -
验证签名时:
服务端使用与签名生成时匹配的公钥(非对称加密)或相同密钥(对称加密),对收到的 JWT 的头部和载荷进行相同的哈希计算,并将结果与 JWT 自带的签名进行比对。若一致,则证明 JWT 未被篡改;若不一致,则 JWT 非法。
(2)签名验证的具体流程
以非对称加密(RSA 算法) 为例:
-
提取 JWT 的三部分: 将 JWT 按
.拆分为headerBase64、payloadBase64、signatureBase64。 -
解码头部: 对
headerBase64进行 Base64 解码,获取签名算法(如RS256表示使用 RSA-SHA256 算法)和密钥相关信息(如公钥 ID)。 -
获取验证密钥: 根据头部中的信息(如公钥 ID),从密钥管理服务(如 AWS KMS、HashiCorp Vault)中获取对应的公钥。
-
重构签名: 使用相同的哈希算法(如 SHA256),对原始的
headerBase64和payloadBase64进行哈希计算,得到原始哈希值。 -
验证签名:
使用公钥对 JWT 自带的
signatureBase64进行解密,得到签名哈希值。将其与步骤 4 的原始哈希值对比:- 一致 → 签名有效,JWT 未被篡改。
- 不一致 → 签名无效,JWT 非法(可能被篡改或伪造)。
(3)关键安全逻辑
- 非对称加密的安全性: 私钥仅由认证服务器(如 FIDO 服务)持有,公钥可公开分发。攻击者无法通过公钥反推私钥,因此无法伪造合法签名。
- 对称加密的风险: 若使用对称加密(如 HMAC 算法),密钥需在服务端和认证服务器之间安全共享,一旦泄露会导致签名验证失效。因此生产环境更推荐非对称加密。
声明解析:验证 JWT 的有效性和权限
(1)声明(Claims)的定义与分类
JWT 的载荷(Payload)包含一组键值对声明,用于描述用户身份、权限、令牌时效等信息。声明分为三类:
-
注册声明(Registered Claims) :
预定义的标准声明,如:
exp(Expiration Time):令牌过期时间(Unix 时间戳)。nbf(Not Before):令牌生效时间。iss(Issuer):令牌颁发者(如fido-auth-server.com)。aud(Audience):令牌接收方(如目标服务api.example.com)。
-
公共声明(Public Claims) : 自定义声明,需在注册中心注册(非必需),如
user_id、role。 -
私有声明(Private Claims) : 自定义的非公开声明,仅在发行方和消费方之间约定,如
department。
(2)声明解析的验证规则
服务端解析声明后,按以下规则验证 JWT 的有效性:
-
过期时间验证(
exp) : 检查当前时间是否超过exp,若超过则 JWT 过期,拒绝请求。 -
生效时间验证(
nbf) : 检查当前时间是否早于nbf,若早于则 JWT 尚未生效,拒绝请求。 -
颁发者验证(
iss) : 确认iss是否为信任的认证服务器(如 FIDO 服务的域名),防止接收非法颁发者的令牌。 -
接收方验证(
aud) : 检查aud是否包含当前服务的标识(如服务端配置的audience列表),防止令牌被跨服务滥用。 -
权限验证(自定义声明) :
根据业务需求验证用户权限,如:
role声明是否包含admin或user。scopes声明是否包含请求所需的权限(如read:profile)。
签名验证与声明解析的协同作用
(1)缺一不可的安全机制
-
签名验证是基础: 即使声明内容看似合法,若签名验证失败(如 JWT 被篡改),服务端会直接拒绝请求。
-
声明解析是补充:
签名有效仅说明 JWT 未被篡改,但无法保证其时效性和权限合法性。例如:
- 攻击者可能窃取未过期的合法 JWT 用于越权访问,此时需通过声明中的
role或scopes拦截请求。 - 合法 JWT 过期后,即使签名有效,也需通过
exp声明拒绝请求。
- 攻击者可能窃取未过期的合法 JWT 用于越权访问,此时需通过声明中的
(2)防御场景举例
| 攻击场景 | 签名验证作用 | 声明解析作用 |
|---|---|---|
攻击者篡改 JWT 的role为admin | 签名验证失败(篡改导致哈希值不一致) | 即使签名伪造成功,role声明非法仍被拦截 |
| 攻击者使用已过期的 JWT | 签名可能有效(未被篡改) | exp声明过期,拒绝请求 |
| 非法服务器颁发伪造 JWT | 签名验证失败(使用非法私钥) | iss声明非信任颁发者,拒绝请求 |
JWT验证完整流程
服务端收到JWT →
↓ 提取Header、Payload、Signature
↓
├─ 签名验证 ──→ 失败 → 拒绝请求(401)
└─ 成功 →
↓
├─ 声明解析 ──→ 失败(如过期、权限不足)→ 拒绝请求(403)
└─ 成功 → 允许访问