hi-server/scripts/test_device_login.go
shanshanzhong df7303738a
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m18s
bug: 无订阅情况 出现下多笔订单 支付状态乱
2026-03-30 00:32:41 -07:00

296 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
// 设备登录测试脚本
// 用法: go run scripts/test_device_login.go
// 功能: 模拟客户端设备登录,自动加密请求体,解密响应,打印 token
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/forgoer/openssl"
)
// ==================== 配置区域 ====================
const (
serverURL = "https://tapi.hifast.biz" // 服务地址
securitySecret = "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx" // device.security_secret
identifier = "test-device-script-001" // 设备唯一标识
userAgent = "TestScript/1.0" // UserAgent
appId = "android-client" // AppSignature.AppSecrets 中的 key
appSecret = "uB4G,XxL2{7b" // AppSignature.AppSecrets 中的 value
)
// ==================== AES 工具(与服务端 pkg/aes/aes.go 一致)====================
func generateKey(key string) []byte {
hash := sha256.Sum256([]byte(key))
return hash[:32]
}
func generateIv(iv, key string) []byte {
h := md5.New()
h.Write([]byte(iv))
return generateKey(hex.EncodeToString(h.Sum(nil)) + key)
}
func aesEncrypt(plainText []byte, keyStr string) (data string, nonce string, err error) {
nonce = fmt.Sprintf("%x", time.Now().UnixNano())
key := generateKey(keyStr)
iv := generateIv(nonce, keyStr)
dst, err := openssl.AesCBCEncrypt(plainText, key, iv, openssl.PKCS7_PADDING)
if err != nil {
return "", "", err
}
return base64.StdEncoding.EncodeToString(dst), nonce, nil
}
func aesDecrypt(cipherText string, keyStr string, ivStr string) (string, error) {
decode, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
key := generateKey(keyStr)
iv := generateIv(ivStr, keyStr)
dst, err := openssl.AesCBCDecrypt(decode, key, iv, openssl.PKCS7_PADDING)
return string(dst), err
}
// ==================== 签名工具(与服务端 pkg/signature 一致)====================
func buildStringToSign(method, path, rawQuery string, body []byte, xAppId, timestamp, nonce string) string {
canonical := canonicalQuery(rawQuery)
bodyHash := sha256Hex(body)
parts := []string{
strings.ToUpper(method),
path,
canonical,
bodyHash,
xAppId,
timestamp,
nonce,
}
return strings.Join(parts, "\n")
}
func canonicalQuery(rawQuery string) string {
if rawQuery == "" {
return ""
}
pairs := strings.Split(rawQuery, "&")
sort.Strings(pairs)
return strings.Join(pairs, "&")
}
func sha256Hex(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%x", h)
}
func buildSignature(stringToSign, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}
func signedRequest(method, url, rawQuery string, body []byte, token string) (*http.Request, error) {
var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewReader(body)
}
req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", token) // 不带 Bearer 前缀,服务端直接 Parse token
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce := fmt.Sprintf("%x", time.Now().UnixNano())
// 提取 path
path := req.URL.Path
sts := buildStringToSign(method, path, rawQuery, body, appId, timestamp, nonce)
sig := buildSignature(sts, appSecret)
req.Header.Set("X-App-Id", appId)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", sig)
return req, nil
}
// ==================== 主逻辑 ====================
func main() {
fmt.Println("=== 设备登录测试 ===")
fmt.Printf("Server: %s\n", serverURL)
fmt.Printf("Identifier: %s\n", identifier)
fmt.Println()
// 1. 构造原始请求体
payload := map[string]string{
"identifier": identifier,
"user_agent": userAgent,
}
plainBytes, err := json.Marshal(payload)
if err != nil {
fmt.Printf("[ERROR] marshal payload: %v\n", err)
return
}
fmt.Printf("原始请求体: %s\n", string(plainBytes))
// 2. AES 加密请求体
encData, nonce, err := aesEncrypt(plainBytes, securitySecret)
if err != nil {
fmt.Printf("[ERROR] encrypt: %v\n", err)
return
}
encBody := map[string]string{
"data": encData,
"time": nonce,
}
encBytes, _ := json.Marshal(encBody)
fmt.Printf("加密请求体: %s\n\n", string(encBytes))
// 3. 发送请求
req, err := signedRequest("POST", serverURL+"/v1/auth/login/device", "", encBytes, "")
if err != nil {
fmt.Printf("[ERROR] new request: %v\n", err)
return
}
req.Header.Set("Login-Type", "device")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("[ERROR] request failed: %v\n", err)
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Printf("HTTP Status: %d\n", resp.StatusCode)
fmt.Printf("原始响应: %s\n\n", string(respBody))
// 4. 解密响应
// 响应格式: {"code":200,"data":{"data":"<encrypted>","time":"<nonce>"},"message":""}
var outer struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(respBody, &outer); err != nil {
fmt.Printf("[ERROR] parse response: %v\n", err)
return
}
if outer.Code != 200 {
fmt.Printf("[FAIL] 登录失败: code=%d message=%s\n", outer.Code, outer.Message)
return
}
// data 字段是加密对象
var encResp struct {
Data string `json:"data"`
Time string `json:"time"`
}
if err := json.Unmarshal(outer.Data, &encResp); err != nil {
// 如果 Device.Enable=falsedata 直接就是明文对象
fmt.Printf("响应 data 非加密格式,直接解析: %s\n", string(outer.Data))
var loginResp struct {
Token string `json:"token"`
}
if err2 := json.Unmarshal(outer.Data, &loginResp); err2 == nil && loginResp.Token != "" {
fmt.Printf("[OK] Token: %s\n", loginResp.Token)
}
return
}
decrypted, err := aesDecrypt(encResp.Data, securitySecret, encResp.Time)
if err != nil {
fmt.Printf("[ERROR] decrypt response: %v\n", err)
return
}
fmt.Printf("解密后响应: %s\n\n", decrypted)
var loginResp struct {
Token string `json:"token"`
}
if err := json.Unmarshal([]byte(decrypted), &loginResp); err != nil {
fmt.Printf("[ERROR] parse decrypted: %v\n", err)
return
}
fmt.Printf("[OK] Token: %s\n", loginResp.Token)
// 5. 用 token 请求订阅列表
fmt.Println("\n=== 请求订阅列表 ===")
subReq, err := signedRequest("GET", serverURL+"/v1/public/subscribe/list", "", nil, loginResp.Token)
if err != nil {
fmt.Printf("[ERROR] build subscribe request: %v\n", err)
return
}
subReq.Header.Set("Login-Type", "device")
subResp, err := client.Do(subReq)
if err != nil {
fmt.Printf("[ERROR] subscribe list request: %v\n", err)
return
}
defer subResp.Body.Close()
subBody, _ := io.ReadAll(subResp.Body)
fmt.Printf("HTTP Status: %d\n", subResp.StatusCode)
fmt.Printf("原始响应: %s\n", string(subBody))
// 解密订阅列表响应
var subOuter struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(subBody, &subOuter); err != nil {
fmt.Printf("[ERROR] parse subscribe response: %v\n", err)
return
}
if subOuter.Code != 200 {
fmt.Printf("[FAIL] 订阅列表失败: code=%d message=%s\n", subOuter.Code, subOuter.Message)
return
}
var subEnc struct {
Data string `json:"data"`
Time string `json:"time"`
}
if err := json.Unmarshal(subOuter.Data, &subEnc); err != nil || subEnc.Data == "" {
// 无加密,直接打印
fmt.Printf("\n[OK] 订阅列表(明文): %s\n", string(subOuter.Data))
return
}
subDecrypted, err := aesDecrypt(subEnc.Data, securitySecret, subEnc.Time)
if err != nil {
fmt.Printf("[ERROR] decrypt subscribe list: %v\n", err)
return
}
fmt.Printf("\n[OK] 订阅列表(解密): %s\n", subDecrypted)
}