微服务中使用JWT 认证体系详解:HMAC vs RSA 签名、OAuth2 Token 获取机制
微服务中使用JWT 认证体系详解:HMAC vs RSA 签名、OAuth2 Token 获取机制
一、JWT 基础概念
1.1 什么是 JWT
JWT(JSON Web Token)是一种紧凑的、自包含的令牌格式,用于在各方之间安全传递信息。它由三部分组成,用点号.分隔:
xxxxx.yyyyy.zzzzz ↓ ↓ ↓ Header Payload Signature- Header:声明令牌类型和签名算法
- Payload:存放实际数据(用户ID、权限、过期时间等)
- Signature:对前两部分的签名,防止数据被篡改
1.2 一个 JWT 长什么样
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ0ZXN0IiwiZXhwIjoxNzg0Njc4NDA1fQ.签名部分对 Header 进行 Base64 解码:
{"alg":"RS256",// 签名算法"typ":"JWT"// 令牌类型}对 Payload 进行 Base64 解码:
{"user_name":"test","member_id":100000,"exp":1784678405,// 过期时间(Unix 时间戳)"client_id":"my_app","scope":["read","write"]}注意:Header 和 Payload 只是 Base64 编码(不是加密),任何人都能解码看到内容。Signature 才是保证数据不被篡改的关键。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、两种签名算法对比:HMAC vs RSA
2.1 HMAC(对称加密签名)
代表算法:HS256、HS384、HS512
原理:用一个共享密钥(Secret Key)对数据进行签名和验签,签名方和验签方使用同一个密钥。
签名过程:HMAC-SHA256(Header + "." + Payload, 密钥) → Signature 验签过程:用同一个密钥重新计算签名,对比是否一致特点:
- 签名和验证都用同一把钥匙
- 速度快、实现简单
- 适合单体系统或前端-后端之间的简单场景
- 密钥泄露 = 既能伪造又能验证
典型场景:前端登录后,后端用一个 Secret 签发 Token,前端携带该 Token 访问同一后端。
┌──────────┐ 同一把密钥 ┌──────────┐ │ 登录服务 │ ←────────────────────→ │ 业务服务 │ │ (签发方) │ 签发 + 验证都用它 │ (验证方) │ └──────────┘ └──────────┘2.2 RSA(非对称加密签名)
代表算法:RS256、RS384、RS512
原理:使用一对公钥-私钥。私钥签名,公钥验签。签名方和验签方使用不同的密钥。
签名过程:RSA-SHA256(Header + "." + Payload, 私钥) → Signature 验签过程:用公钥验证签名是否与数据匹配特点:
- 私钥签名(只有认证服务有)
- 公钥验签(所有业务服务都可以有)
- 公钥泄露无所谓,只能验证不能伪造
- 适合分布式微服务架构
- 计算稍慢
典型场景:独立的认证服务用私钥签发 Token,各个微服务用公钥验证 Token 的合法性。
┌──────────────┐ │ 认证服务 │ 持有私钥,负责签发 Token │ (Auth Server)│ └──────┬───────┘ │ 签发的 Token ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 服务 A │ │ 服务 B │ │ 服务 C │ │ 持有公钥 │ │ 持有公钥 │ │ 持有公钥 │ │ 只能验证 │ │ 只能验证 │ │ 只能验证 │ └──────────┘ └──────────┘ └──────────┘2.3 核心区别一览
| 维度 | HMAC (HS256) | RSA (RS256) |
|---|---|---|
| 密钥 | 一个共享密钥 | 一对公私钥 |
| 签发 | 持有密钥的任何人 | 只有持有私钥的一方 |
| 验证 | 持有密钥的任何人 | 持有公钥的任何人 |
| 安全性 | 密钥泄露 = 全部完蛋 | 公钥公开也安全 |
| 性能 | 快 | 稍慢 |
| 适用场景 | 单体/简单前后端 | 微服务/多系统 |
| 密钥管理 | 所有需验证的服务都要密钥 | 只有认证服务有私钥 |
三、为什么微服务架构用 RSA 而不是 HMAC
假设你有 20 个微服务都需要验证 Token:
如果用 HMAC:
- 每个服务都要存储同一个密钥
- 任何一个服务泄露密钥,攻击者就能伪造 Token
- 密钥轮换时要同时更新 20 个服务
如果用 RSA:
- 只有认证服务有私钥
- 其他 20 个服务只有公钥,泄露了也只能验证不能伪造
- 私钥轮换只改认证服务一处
这就是为什么项目使用独立的认证服务+ RSA 公钥的架构。
四、OAuth2 协议基础
4.1 OAuth2 是什么
OAuth2 是一套授权框架,定义了客户端如何获取访问令牌(Access Token)。它不关心 Token 的具体格式,但通常与 JWT 配合使用。
4.2 四种授权模式
| 模式 | 适用场景 | 简述 |
|---|---|---|
| Authorization Code | Web应用、第三方登录 | 最安全,需要浏览器跳转 |
| Password Grant | 内部系统、受信任客户端 | 直接传账号密码换 Token |
| Client Credentials | 服务间调用 | 无用户参与,纯机器对机器 |
| Implicit | 已废弃 | 不推荐使用 |
4.3 Password Grant 详解
客户端(ApiPost/curl) 认证服务(Auth Server) │ │ │ POST /oauth/token │ │ Authorization: Basic base64(id:pw)│ │ Body: grant_type=password │ │ &username=xxx │ │ &password=xxx │ │──────────────────────────────────→ │ │ │ 验证账号密码 │ │ 用私钥签发 JWT │ { │ │ "access_token": "eyJ...", │ │ "token_type": "bearer", │ │ "expires_in": 53640, │ │ "refresh_token": "eyJ..." │ │ } │ │←────────────────────────────────── │ │ │请求中的两层认证:
- Basic Auth(
Authorization: Basic xxx):证明"你是哪个客户端应用"- 将
client_id:client_password进行 Base64 编码 - 例如:
demo_app:123456→ZGVtb19hcHA6MTIzNDU2 - 表示"我是 demo_app 这个应用"
- 将
- Body 中的账号密码:证明"你代表哪个用户"
username+password- 表示"我代表 zhangsan 这个用户请求授权"
4.4 返回结果字段说明
{"access_token":"eyJhbGciOiJSUzI1NiIs...",// 访问令牌,调接口时用"token_type":"bearer",// 令牌类型,固定为 bearer"refresh_token":"eyJhbGciOiJSUzI1NiIs...",// 刷新令牌,用于续期"expires_in":53640,// access_token 有效期(秒)"scope":"read write",// 授权范围"user_name":"zhangsan",// 用户名"member_id":100000// 业务字段}五、两种 Token 的本质区别
以实际场景说明为什么"前端登录拿到的 Token"不能用于调用微服务接口:
5.1 前端 Web Token(HMAC 签名)
浏览器 → 前端登录接口(/open/login) → 返回 coc_jwt(放在 Set-Cookie 中)- 签名算法:HS512(HMAC)
- 签发方:前端网关/BFF 层
- 验证方:同一个前端网关
- 用途:标识浏览器会话,前端页面鉴权
- 特征:密钥只在网关内部,微服务拿不到这个密钥
5.2 微服务 Token(RSA 签名)
客户端 → 认证服务(/oauth/token) → 返回 access_token(JWT,RS256 签名)- 签名算法:RS256(RSA)
- 签发方:专用认证服务(用私钥签名)
- 验证方:所有微服务(用公钥验签)
- 用途:服务间调用鉴权
- 特征:每个微服务配置公钥即可独立验证
5.3 为什么不能混用
前端 Token (HS512) → 微服务 (配置的是 RSA 公钥) ↓ 尝试用 RSA 公钥验证 HMAC 签名 ↓ ❌ "Cannot convert access token to JSON"微服务只认识 RSA 签名的 Token。你拿 HMAC 签名的 Token 给它,它用 RSA 公钥去验签,格式完全对不上,所以报错。
六、完整认证架构图
┌─────────────────────────────┐ │ 认证服务 (Auth Server) │ │ 持有 RSA 私钥 │ │ /oauth/token │ │ 签发 RS256 JWT │ └──────────┬──────────────────┘ │ ┌────────────────┼────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ A 服务 │ │ B 服务 │ │ C 服务 │ │ 端口 3012 │ │ 端口 3013 │ │ 端口 3014 │ │ 配置 RSA 公钥│ │ 配置 RSA 公钥│ │ 配置 RSA 公钥│ │ 独立验签 │ │ 独立验签 │ │ 独立验签 │ └──────────────┘ └──────────────┘ └──────────────┘ ┌─────────────────────────────────────────────────┐ │ 前端网关 (BFF / Gateway) │ │ /open/login → 签发 coc_jwt (HS512) │ │ 用于浏览器 Cookie 会话管理 │ │ 与微服务 Token 体系完全独立 │ └─────────────────────────────────────────────────┘七、通用代码示例
7.1 Java Spring Boot 配置 RSA 公钥验签
# application.ymlsecurity:jwt:signing-key:|-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----resource-ids:my-servicematchers:-path:/api/public/**attribute:permitAll-path:/api/**attribute:authenticated7.2 curl 获取 OAuth2 Token
# 将 client_id:client_password 进行 Base64 编码后放入 Basic Authcurl-s-XPOST"https://auth-server.com/oauth/token"\-H"Content-Type: application/x-www-form-urlencoded"\-u"my_client_id:my_client_password"\-d"grant_type=password&username=user&password=pass"-u参数等价于:
-H"Authorization: Basic$(echo-n'my_client_id:my_client_password'|base64)"7.3 PowerShell 获取 Token
# 构造 Basic Auth$pair="my_client_id:my_client_password"$base64=[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($pair))# 请求 Token$headers= @{"Authorization"="Basic$base64""Content-Type"="application/x-www-form-urlencoded"}$body="grant_type=password&username=user&password=pass"$response=Invoke-WebRequest-Uri"https://auth-server.com/oauth/token"`-Method POST-Headers$headers-Body$body-UseBasicParsing$token=($response.Content|ConvertFrom-Json).access_token7.4 Java 代码获取 Token(RestTemplate)
/** * 通过 OAuth2 Password Grant 获取访问令牌 */publicStringgetAccessToken(){StringclientId="my_client_id";StringclientPassword="my_client_password";Stringcredentials=Base64.getEncoder().encodeToString((clientId+":"+clientPassword).getBytes());HttpHeadersheaders=newHttpHeaders();headers.set("Authorization","Basic "+credentials);headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);MultiValueMap<String,String>body=newLinkedMultiValueMap<>();body.add("grant_type","password");body.add("username","user");body.add("password","pass");HttpEntity<MultiValueMap<String,String>>request=newHttpEntity<>(body,headers);ResponseEntity<Map>response=restTemplate.postForEntity("https://auth-server.com/oauth/token",request,Map.class);return(String)response.getBody().get("access_token");}7.5 使用 Token 调用业务接口
# 获取到 token 后curl-s-XPOST"http://127.0.0.1:8080/api/your-endpoint"\-H"Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."\-H"Content-Type: application/json"\-d'{"pageNum":1,"pageSize":10}'八、验签过程详解
当业务服务收到请求时:
1. 从 Header 中提取: Authorization: Bearer eyJxxx.eyJyyy.zzz 2. 拆分 JWT 三段: - Header: eyJxxx → 解码得 {"alg":"RS256","typ":"JWT"} - Payload: eyJyyy → 解码得 {"user_name":"test","exp":1784678405,...} - Signature: zzz 3. 用配置的 RSA 公钥验证签名: RSA_Verify(公钥, "eyJxxx.eyJyyy", zzz) → true/false 4. 如果签名验证通过: - 检查 exp 是否过期 - 检查 resource-ids 是否匹配 - 提取用户信息注入到 SecurityContext 5. 如果验证失败: - 返回 401 Unauthorized - 错误信息如 "Cannot convert access token to JSON"(格式不对) - 或 "Access token expired"(过期)九、总结
| 概念 | 一句话解释 |
|---|---|
| JWT | 一种自包含的令牌格式,三段式结构 |
| HMAC (HS256/512) | 对称签名,一把钥匙既签又验 |
| RSA (RS256) | 非对称签名,私钥签、公钥验 |
| OAuth2 | 一套获取令牌的标准流程/协议 |
| Password Grant | OAuth2 的一种模式,直接用账号密码换 Token |
| Basic Auth | 用 Base64(id:password) 标识客户端身份 |
| Bearer Token | Token 的传递方式,放在 Authorization Header 中 |
| 公钥 | 公开的,只能验证签名,不能伪造 |
| 私钥 | 保密的,用来签名,只有认证服务持有 |
核心理解:在微服务架构中,认证服务是唯一持有私钥的"发证机关",各个业务服务通过公钥验证"证件"的真伪,而不需要每个服务都能"发证"。前端页面用的是另一套简单的会话机制(HMAC Cookie),两者是独立的认证体系。