This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
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=false,data 直接就是明文对象
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user