在分布式系统或多语言服务协作的场景中,经常需要Go编写的服务端和PHP编写的业务端进行HTTP POST请求交互,而API签名验证是保障请求合法性的核心环节,两端签名规则不一致会直接导致请求校验失败。
核心签名规则设计
要实现两端一致的签名验证,首先需要统一签名的计算规则,通常包含以下步骤:
- 收集所有请求参数,排除签名本身参数
- 按照参数名的ASCII码从小到大排序
- 拼接成键值对字符串,格式为key1=value1&key2=value2
- 在拼接字符串末尾追加约定的签名密钥
- 对最终字符串进行哈希加密,常用SHA256算法
PHP端实现签名与POST请求
PHP侧首先实现签名生成函数,再构造POST请求携带签名参数。
<?php
/**
* 生成API签名
* @param array $params 请求参数
* @param string $secret 签名密钥
* @return string 签名结果
*/
function generateSign($params, $secret) {
// 排除签名参数
unset($params['sign']);
// 参数名ASCII排序
ksort($params);
// 拼接键值对字符串
$str = '';
foreach ($params as $key => $value) {
if ($str != '') {
$str .= '&';
}
$str .= $key . '=' . $value;
}
// 追加密钥
$str .= $secret;
// SHA256加密
return hash('sha256', $str);
}
// 构造请求参数
$params = [
'app_id' => '10001',
'timestamp' => time(),
'nonce' => uniqid(),
'data' => 'test_post_data'
];
$secret = 'api_secret_key_123';
// 生成签名
$params['sign'] = generateSign($params, $secret);
// 发送POST请求
$url = 'http://127.0.0.1:8080/api/test';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
?>
Go端实现签名验证与POST请求
Go侧需要实现相同的签名计算逻辑用于验证请求,同时如果需要主动发起POST请求,也要保证签名生成规则一致。
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"time"
)
// 生成API签名,逻辑与PHP端完全一致
func generateSign(params url.Values, secret string) string {
// 排除签名参数
params.Del("sign")
// 获取所有参数名并排序
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// 拼接键值对字符串
str := ""
for _, k := range keys {
if str != "" {
str += "&"
}
str += fmt.Sprintf("%s=%s", k, params.Get(k))
}
// 追加密钥
str += secret
// SHA256加密
h := sha256.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
// 验证请求签名
func verifySignHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Write([]byte("请求方法错误"))
return
}
r.ParseForm()
params := r.Form
secret := "api_secret_key_123"
clientSign := params.Get("sign")
serverSign := generateSign(params, secret)
if clientSign == serverSign {
w.Write([]byte("签名验证通过"))
} else {
w.Write([]byte("签名验证失败"))
}
}
// Go主动发起POST请求
func sendPostRequest() {
params := url.Values{}
params.Add("app_id", "10001")
params.Add("timestamp", fmt.Sprintf("%d", time.Now().Unix()))
params.Add("nonce", fmt.Sprintf("%d", time.Now().UnixNano()))
params.Add("data", "test_post_data")
secret := "api_secret_key_123"
// 生成签名
sign := generateSign(params, secret)
params.Add("sign", sign)
resp, err := http.PostForm("http://ipipp.com/api/test", params)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println("响应结果:", string(body))
}
func main() {
http.HandleFunc("/api/test", verifySignHandler)
// 启动服务端用于接收PHP请求
go http.ListenAndServe(":8080", nil)
// 测试Go发起POST请求
sendPostRequest()
// 阻塞主进程
select {}
}
常见问题排查
如果两端签名不一致,可以从以下几个点排查:
- 参数排序规则是否一致,是否都按照参数名ASCII升序排列
- 参数拼接时,空值参数是否处理一致,建议统一排除空值参数
- 签名密钥是否两端完全一致,没有多余空格或字符
- 哈希算法的输出格式是否一致,PHP的hash默认输出小写十六进制,Go的hex.EncodeToString也是小写,无需额外转换
- POST请求的Content-Type是否一致,表单提交建议使用application/x-www-form-urlencoded
总结
只要统一签名的计算规则,包括参数排序、拼接方式、加密算法和密钥,Go和PHP就能实现完全一致的HTTP POST请求与API签名验证。开发时可以先单独测试签名函数的输出结果,确保两端相同输入得到相同签名后,再对接完整的请求逻辑,能有效减少调试时间。
GoPHPHTTP_POSTAPI_signature_verification修改时间:2026-06-22 14:07:04