签名
背景
在 OAuth 授权平台中,获取 access_token接口为三方服务端调用授权服务器的敏感接口。需要校验 OAuth 业务参数合法性,还需要校验请求是否确实由已注册的三方客户端发起,并防止请求在传输过程中被篡改或被重复提交。 因此,在 OAuth 协议参数校验之外,引入基于 HMAC-SHA256 的客户端请求签名机制,用于实现:
- 客户端身份校验
- 请求完整性保护
- 请求防重放
请求头定义
| Header 名称 | 是否必填 | 说明 |
|---|---|---|
| Client-Id | 是 | 三方客户端标识,对应平台注册的 client_id |
| Sign | 是 | 请求签名值 |
| Timestamp | 是 | 请求发起时间戳,单位毫秒 |
| Nonce | 是 | 一次性随机串,用于防重放 |
签名规则
签名各字段说明
- timestamp:请求发起时间戳,单位毫秒,与请求头
Timestamp相同。 - nonce:一次性随机串,与请求头
Nonce相同,用于防重放。 - method:请求方法(POST/GET 等),字母全部大写。
- requestPath:请求接口路径,例如
/v1/oauth/token。 - queryString:请求 URL 中
?后的查询字符串,不存在时为空。 - body:请求主体对应的字符串,如果请求没有主体(通常为 GET 请求)则 body 按空字符串处理。
queryString 为空时,签名格式
timestamp + nonce + method.toUpperCase() + requestPath + body
queryString 不为空时,签名格式
timestamp + nonce + method.toUpperCase() + requestPath + "?" + queryString + body
举例说明
例1:无 QueryString(获取令牌)
- timestamp =
1710000000000 - nonce =
abc123xyz - method =
"POST" - requestPath =
"/v1/oauth/token" - body =
{"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}
生成待签名的字符串:
1710000000000abc123xyzPOST/v1/oauth/token{"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}
生成最终签名的步骤
- 将待签名字符串
baseString使用客户端密钥clientSecret进行 HMAC-SHA256 加密,并对加密结果进行 Base64 编码Sign = Base64(HMAC-SHA256(clientSecret, baseString))
签名示例
- Java
package com.weex.utils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;
public class OAuthApiClient {
/**
* 替换为实际的 clientId / clientSecret
*/
private static final String CLIENT_ID = "your-client-id";
private static final String CLIENT_SECRET = "your-client-secret";
/**
* 替换为实际的 OAuth 授权服务器地址
*/
private static final String BASE_URL = "https://auth.example.com";
/**
* HMAC-SHA256 算法名称
*/
private static final String HMAC_SHA256 = "HmacSHA256";
/**
* 生成签名
*
* 签名原文规则:
* 1. 无 queryString:
* timestamp + nonce + method.toUpperCase() + requestPath + body
*
* 2. 有 queryString:
* timestamp + nonce + method.toUpperCase() + requestPath + queryString + body
*
* 说明:
* - queryString 为空时传空字符串 ""
* - queryString 非空时建议格式为 "?a=1&b=2"
* - body 为空时传空字符串 ""
*/
public static String generateSignature(String clientSecret,
String timestamp,
String nonce,
String method,
String requestPath,
String queryString,
String body) throws Exception {
String safeQueryString = queryString == null ? "" : queryString;
String safeBody = body == null ? "" : body;
String message = timestamp
+ nonce
+ method.toUpperCase()
+ requestPath
+ safeQueryString
+ safeBody;
return hmacSha256Base64(clientSecret, message);
}
/**
* HMAC-SHA256 后进行 Base64 编码
*/
private static String hmacSha256Base64(String secretKey, String message) throws Exception {
SecretKeySpec secretKeySpec = new SecretKeySpec(
secretKey.getBytes(StandardCharsets.UTF_8),
HMAC_SHA256
);
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(secretKeySpec);
byte[] signatureBytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signatureBytes);
}
/**
* 生成毫秒时间戳
*/
private static String generateTimestamp() {
return String.valueOf(System.currentTimeMillis());
}
/**
* 生成随机 nonce
*/
private static String generateNonce() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 发送 POST 请求
*
* 说明:
* - body 必须是客户端最终实际发送的 JSON 字符串
* - 签名时使用的 body,必须与请求发送的 body 完全一致
*/
public static String sendPost(String clientId,
String clientSecret,
String requestPath,
String queryString,
String body) throws Exception {
String timestamp = generateTimestamp();
String nonce = generateNonce();
String signature = generateSignature(
clientSecret,
timestamp,
nonce,
"POST",
requestPath,
queryString,
body
);
String url = BASE_URL + requestPath + (queryString == null ? "" : queryString);
HttpPost postRequest = new HttpPost(url);
postRequest.setHeader("Client-Id", clientId);
postRequest.setHeader("Sign", signature);
postRequest.setHeader("Timestamp", timestamp);
postRequest.setHeader("Nonce", nonce);
postRequest.setHeader("Content-Type", "application/json");
StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8);
postRequest.setEntity(entity);
try (CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = httpClient.execute(postRequest)) {
int statusCode = response.getStatusLine().getStatusCode();
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
System.out.println("HTTP Status: " + statusCode);
return responseBody;
}
}
/**
* 示例调用:authorization_code 模式换取 token
*/
public static void main(String[] args) {
try {
String requestPath = "/v1/oauth/token";
/**
* 注意:
* 这里的 body 字符串就是最终实际发送的 JSON 字符串
* 签名时必须使用这一份 body,发送时也必须发送这一份 body
*/
String body = "{"
+ "\"grantType\":\"authorization_code\","
+ "\"code\":\"auth_code_xxx\","
+ "\"redirectUri\":\"https://client.example.com/callback\","
+ "\"codeVerifier\":\"code_verifier_xxx\""
+ "}";
String response = sendPost(
CLIENT_ID,
CLIENT_SECRET,
requestPath,
"",
body
);
System.out.println("Response: " + response);
} catch (Exception e) {
e.printStackTrace();
}
}
}