
# 签名

## 背景

在 OAuth 授权平台中，获取 access_token接口为三方服务端调用授权服务器的敏感接口。需要校验 OAuth 业务参数合法性，还需要校验请求是否确实由已注册的三方客户端发起，并防止请求在传输过程中被篡改或被重复提交。
因此，在 OAuth 协议参数校验之外，引入基于 HMAC-SHA256 的客户端请求签名机制，用于实现：
- 客户端身份校验
- 请求完整性保护
- 请求防重放

## 请求头定义

| Header 名称  | 是否必填  | 说明                        |
|------------|-------|---------------------------|
| Client-Id  | 是     | 三方客户端标识，对应平台注册的 client_id |
| Sign       | 是     | 请求签名值                     |
| Timestamp  | 是     | 请求发起时间戳，单位毫秒              |
| Nonce      | 是     | 一次性随机串，用于防重放              |

## 签名规则

<strong>签名各字段说明</strong>

- timestamp：请求发起时间戳，单位毫秒，与请求头 `Timestamp` 相同。
- nonce：一次性随机串，与请求头 `Nonce` 相同，用于防重放。
- method：请求方法（POST/GET 等），字母全部大写。
- requestPath：请求接口路径，例如 `/v1/oauth/token`。
- queryString：请求 URL 中 `?` 后的查询字符串，不存在时为空。
- body：请求主体对应的字符串，如果请求没有主体（通常为 GET 请求）则 body 按空字符串处理。

<strong>queryString 为空时，签名格式</strong>

`timestamp + nonce + method.toUpperCase() + requestPath + body`

<strong>queryString 不为空时，签名格式</strong>

`timestamp + nonce + method.toUpperCase() + requestPath + "?" + queryString + body`


<strong>举例说明</strong>

<strong>例1：无 QueryString（获取令牌）</strong>

- timestamp = `1710000000000`
- nonce = `abc123xyz`
- method = `"POST"`
- requestPath = `"/v1/oauth/token"`
- body = `{"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}`

<strong>生成待签名的字符串：</strong>

`1710000000000abc123xyzPOST/v1/oauth/token{"grantType":"authorization_code","code":"code123","redirectUri":"https://client.example.com/callback","codeVerifier":"verifier123"}`


<strong>生成最终签名的步骤</strong>

1. 将待签名字符串 `baseString` 使用客户端密钥 `clientSecret` 进行 HMAC-SHA256 加密，并对加密结果进行 Base64 编码
    - `Sign = Base64(HMAC-SHA256(clientSecret, baseString))`


## 签名示例

### Java

```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();
        }
    }
}
```
