API认证之AK-SK机制详解

1. AK-SK简介

AK/SK(Access Key / Secret Key)是一对用于API认证的密钥,类似用户名和密码的组合。

1.1 AK(Access Key)

访问密钥,用于标识用户身份。

属性 说明
作用 告诉服务器"我是谁"
格式 通常为字母数字组合,如 AKIAIOSFODNN7EXAMPLE
保密性 可以公开(会随请求发送)
泄露后果 别人可以冒充你发起请求,但无法伪造签名

类比: 银行卡号 - 告诉别人用来给你打钱

1.2 SK(Secret Key)

秘密密钥,用于验证请求真实性。

属性 说明
作用 对请求内容进行签名,证明"请求确实是我发出的"
格式 通常为更长的随机字符串
保密性 绝对不能泄露,不传输、不存储在客户端
泄露后果 别人可以伪造任意请求,后果严重

类比: 银行卡密码 - 丢了钱就没了

1.3 安全对比

AK泄露:风险可控
├── 别人只能知道你是谁
├── 无法伪造合法请求
└── 可以轮换SK修复

SK泄露:灾难性后果
├── 攻击者可以伪造任意请求
├── 可以执行任何操作
└── 必须立即禁用并重新生成

1.4 应用场景

AK/SK认证广泛应用于以下场景:

场景 说明 典型厂商
云服务API 调用云厂商的API管理资源 AWS、阿里云、腾讯云、华为云
开放平台 第三方应用调用平台能力 微信开放平台、支付宝开放平台
微服务间认证 内部服务间的API调用鉴权 服务网格、API网关
IoT设备认证 设备与云端的安全通信 智能家居、工业物联网
数据开放平台 对外提供数据接口 政务数据、企业数据共享
CI/CD集成 自动化工具调用API GitHub Actions、Jenkins

为什么选择AK/SK而不是账号密码?

  • 无状态:服务器不需要存储会话状态
  • 可审计:每个请求都有独立签名,便于追踪
  • 可控制:可以限制AK的权限范围、IP、有效期
  • 可轮换:定期更换SK,降低泄露风险

2. 工作原理

服务器 客户端应用 服务器 客户端应用 1. 构造原始请求 方法、URL、参数、请求头 签名计算(客户端内部) 签名验证(服务器内部) alt [签名验证通过] [签名验证失败] 2. 构造规范请求(Canonical Request) 3. 构造签名字符串(StringToSign) 4. 用SK计算HMAC-SHA256签名 5. 将签名Base64编码 6. 发送请求 携带: AK + 签名 + 时间戳 + Nonce 7. 验证时间戳是否在有效范围内 8. 验证Nonce是否重复 9. 根据AK查找对应的SK 10. 用相同SK重新计算签名 11. 对比签名是否一致 12a. 返回请求结果 12b. 返回401/403错误

签名计算详解(客户端)

输入:
├── HTTP方法:GET
├── URI:/api/users
├── 查询参数:limit=10&page=1(已排序)
├── 请求头:host:api.example.com, date:2026-06-04T10:00:00Z
└── 请求体哈希:空(GET请求)

处理:
├── Step 1: 构造规范请求(Canonical Request)
├── Step 2: 对规范请求做SHA256哈希
├── Step 3: 构造签名字符串(Algorithm + 时间戳 + 哈希值)
└── Step 4: 用SK对签名字符串做HMAC-SHA256

输出:
└── Base64编码后的签名字符串

签名验证详解(服务器)

输入:
├── 客户端传来的AK
├── 客户端传来的签名
├── 完整的HTTP请求(用于重新计算签名)

处理:
├── Step 1: 根据AK查找对应的SK
├── Step 2: 检查时间戳是否在±5分钟内
├── Step 3: 检查Nonce是否已使用过
├── Step 4: 用相同算法重新计算签名
└── Step 5: 对比两个签名是否一致

输出:
├── 一致 → 处理请求
└── 不一致 → 返回401/403

3. 签名算法

重要说明:
签名只用SK,不用AK。AK只是告诉服务器"我是谁",服务器根据AK找到对应的SK来验证签名。

3.1 规范请求构造

规范请求(Canonical Request)是将HTTP请求的关键信息按固定格式拼接:

CanonicalRequest = HTTPMethod + "\n" +
                   URI + "\n" +
                   QueryString + "\n" +
                   CanonicalHeaders + "\n" +
                   SignedHeaders + "\n" +
                   HexEncode(Hash(RequestPayload))

示例:

GET                           ← HTTP方法
/api/users                    ← URI
limit=10&page=1               ← 查询参数(已排序)
host:api.example.com          ← 请求头1的值
date:2026-06-04T10:00:00Z     ← 请求头2的值
                              ← 空行分隔
host:date                     ← SignedHeaders:声明签了哪些请求头(分号分隔)
e3b0c44298fc1c149afbf4c8...   ← 请求体的SHA256哈希(空body)

3.2 完整请求示例

GET /api/users?page=1&limit=10 HTTP/1.1
Host: api.example.com
Date: 2026-06-04T10:00:00Z
X-AK: AKIAIOSFODNN7EXAMPLE
X-Signature: 02987e4b68a7e3d4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8

3.3 阿里云签名

阿里云使用HMAC-SHA1算法,签名通过URL参数传递(不是请求头)。

签名计算公式:

Signature = Base64( HMAC-SHA1( AccessSecret, UTF-8-Encoding-Of(StringToSign) ) )

完整请求示例:

https://ecs.aliyuncs.com/
?Action=DescribeInstances
&Format=JSON
&Version=2014-05-26
&AccessKeyId=LTAI****************
&SignatureMethod=HMAC-SHA1
&Timestamp=2026-06-04T10:00:00Z
&SignatureVersion=1.0
&SignatureNonce=ed8fb51f-0c38-4da4-a21a-f189b3a7aecb
&Signature=PPwf***********************

参数说明:

参数 说明
Action API操作名称
Format 返回格式(JSON/XML)
Version API版本
AccessKeyId AK
SignatureMethod 签名算法(HMAC-SHA1)
Timestamp 时间戳
SignatureVersion 签名算法版本(1.0)
SignatureNonce 随机数,防重放
Signature 签名值

注意:
阿里云签名通过URL参数传递,不是请求头。这与AWS不同(AWS用请求头)。

官方文档: 阿里云官方文档

4. Java代码示例

4.1 签名方法

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

/**
 * 使用HMAC-SHA256算法对请求进行签名
 * 
 * @param secret        密钥(SK)
 * @param stringToSign  待签名的字符串(规范请求)
 * @return Base64编码的签名结果
 */
public static String sign(String secret, String stringToSign) throws Exception {
    // 获取HMAC-SHA256算法实例
    Mac mac = Mac.getInstance("HmacSHA256");
    
    // 用SK初始化Mac对象
    mac.init(new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"));
    
    // 计算HMAC签名
    byte[] signBytes = mac.doFinal(stringToSign.getBytes("UTF-8"));
    
    // 将字节数组转换为Base64字符串
    return Base64.getEncoder().encodeToString(signBytes);
}

4.2 签名校验方法

import java.security.MessageDigest;

/**
 * 校验签名是否有效
 * 
 * @param secret            密钥(SK)
 * @param stringToSign      待签名的字符串(与签名时相同)
 * @param expectedSignature 客户端传来的签名
 * @return true表示签名有效,false表示签名无效
 */
public static boolean verify(String secret, String stringToSign, String expectedSignature) throws Exception {
    // 用相同的SK重新计算签名
    String calculatedSignature = sign(secret, stringToSign);
    
    // 使用时间安全的比较方法,防止时序攻击
    // 普通的equals()会在发现第一个不同字符时立即返回,攻击者可以通过计时推断出签名
    return MessageDigest.isEqual(
        calculatedSignature.getBytes("UTF-8"),
        expectedSignature.getBytes("UTF-8")
    );
}

4.3 使用示例

// ===== 客户端签名 =====

// 1. 定义请求参数
String url = "/api/users?limit=10&page=1";  // 完整URL路径+参数
String host = "api.example.com";
String date = "2026-06-04T10:00:00Z";
String bodyHash = "abc123";  // 空body时为固定值

// 2. 构造需要签名的请求头
String signedHeaders = "host:date";
String header1 = "host:" + host;
String header2 = "date:" + date;

// 3. 构造规范请求字符串(StringToSign)
String stringToSign = String.join("\n",
    "GET",              // HTTP方法
    url,                // 完整URL(路径+参数)
    header1,            // Host请求头
    header2,            // Date请求头
    "",                 // 空行分隔
    signedHeaders,      // 签名的请求头列表
    bodyHash            // 请求体哈希
);

// 4. 使用SK进行签名
String signature = sign(sk, stringToSign);

// 5. 将签名添加到请求头
// 请求头:X-AK: <access_key>, X-Signature: <signature>


// ===== 服务器校验 =====

// 1. 从请求头获取AK和签名
// 2. 根据AK查找对应的SK
// 3. 用相同的SK和请求内容重新计算签名
boolean isValid = verify(sk, stringToSign, clientSignature);

// 4. 根据校验结果处理请求
if (!isValid) {
    // 签名无效,拒绝请求
    throw new SecurityException("签名验证失败");
}
// 签名有效,继续处理业务逻辑

说明:

  1. 使用JDK标准库(javax.crypto.Macjava.util.Base64
  2. 使用MessageDigest.isEqual()进行时间安全比较,防止时序攻击
  3. 生产环境建议使用HMAC-SHA256(如阿里云用SHA1则改为HmacSHA1

5. 安全机制

5.1 时间戳防重放

请求携带时间戳
├── 服务器检查时间戳是否在允许范围内(如±5分钟)
├── 超出范围的请求直接拒绝
└── 防止请求被录制后重复发送

形象类比:
时间戳 = 验证码有效期
验证码5分钟内有效,过期就不能用了

5.2 Nonce防重放

每次请求携带唯一随机数(Nonce)
├── 服务器记录已使用的Nonce
├── 相同Nonce的第二次请求直接拒绝
└── 彻底防止重放攻击

形象类比:
Nonce = 一次性优惠券
用过就作废,别人捡到也没用

Nonce存储方案:

方案 特点 适用场景
Redis SETEX key ttl value,自动过期 分布式系统(推荐
数据库 expire_time字段,定时清理 数据量小、需要审计
内存缓存 Caffeine/Guava Cache,设maxSize和过期时间 单机部署
// Redis存储示例
// 存储Nonce(5分钟过期)
redis.setex("nonce:" + nonce, 300, "1");

// 检查Nonce是否已存在
boolean exists = redis.exists("nonce:" + nonce);

关键点:

  • 必须设过期时间,否则内存会爆
  • 过期时间 > 请求最大有效期(如5分钟)
  • 分布式环境用Redis,单机用内存缓存即可

5.3 范围限制

限制AK的使用范围:
├── IP白名单:只允许特定IP调用
├── 服务范围:只能访问特定服务
├── 时间范围:AK有有效期
└── 操作范围:只能执行特定操作

6. 安全最佳实践

6.1 密钥管理

实践 说明
不要硬编码 密钥存在环境变量或密钥管理服务
定期轮换 每3-6个月更换一次SK
最小权限 AK只授予必要的权限
分离环境 开发、测试、生产用不同的AK

6.2 传输安全

实践 说明
必须用TLS AK/SK + 明文HTTP = 灾难
不要在URL中传递 参数会被记录在日志中
使用HTTPS 确保传输过程加密

6.3 代码实现

实践 说明
使用官方SDK 不要自己实现签名逻辑
时间同步 确保服务器时间准确
错误处理 不要泄露签名细节

7. 常见问题

7.1 Q: AK泄露了怎么办?

1. 立即禁用该AK
2. 生成新的AK/SK对
3. 更新所有使用该AK的客户端
4. 检查是否有异常调用

7.2 Q: SK泄露了怎么办?

1. 立即禁用该AK(SK无法单独禁用)
2. 生成新的AK/SK对
3. 检查是否有数据泄露
4. 考虑是否需要通知用户

7.3 Q: 如何安全存储SK?

├── 环境变量(不写在代码里)
├── 密钥管理服务(如AWS KMS、阿里云KMS)
├── 配置中心(加密存储)
└── 绝对不要提交到Git

8. 总结

概念 一句话解释
AK 标识身份,可以公开
SK 签名验证,必须保密
签名 用SK对请求内容计算摘要
验证 服务器用同样的SK重新计算并对比
时间戳 防止请求被重放
Nonce 一次性随机数,彻底防重放
TLS 必须配合使用,保护传输安全
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐