hi-server/script/test_device_login.go
shanshanzhong fde3210a88
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m41s
feat(用户): 实现邮箱绑定功能并返回登录凭证
修改绑定邮箱接口返回登录凭证,优化用户数据迁移流程
添加用户缓存清理逻辑,确保设备绑定后数据一致性
完善邮箱验证和绑定逻辑的注释和错误处理
2025-10-23 10:07:59 -07:00

270 lines
6.7 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
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"time"
)
func sha256Sum(b []byte) []byte {
sum := sha256.Sum256(b)
return sum[:]
}
func md5Hex(s string) string {
h := md5.Sum([]byte(s))
return hex.EncodeToString(h[:])
}
func deriveKey(secret string) []byte {
return sha256Sum([]byte(secret)) // 32 bytes for AES-256
}
func deriveIV(secret, nonce string) []byte {
ivFull := sha256Sum([]byte(md5Hex(nonce) + secret)) // 32 bytes
return ivFull[:aes.BlockSize] // 16 bytes IV
}
func pkcs7Pad(data []byte, blockSize int) []byte {
pad := blockSize - len(data)%blockSize
padding := bytes.Repeat([]byte{byte(pad)}, pad)
return append(data, padding...)
}
func pkcs7Unpad(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("invalid data length")
}
pad := int(data[len(data)-1])
if pad <= 0 || pad > aes.BlockSize || pad > len(data) {
return nil, errors.New("invalid padding")
}
for i := 0; i < pad; i++ {
if data[len(data)-1-i] != byte(pad) {
return nil, errors.New("invalid padding content")
}
}
return data[:len(data)-pad], nil
}
func genNonce() string {
return fmt.Sprintf("%x", time.Now().UnixNano())
}
func encryptPayload(plain map[string]interface{}, secret string) ([]byte, string, error) {
nonce := genNonce()
key := deriveKey(secret)
iv := deriveIV(secret, nonce)
b, err := json.Marshal(plain)
if err != nil {
return nil, "", err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, "", err
}
mode := cipher.NewCBCEncrypter(block, iv)
padded := pkcs7Pad(b, aes.BlockSize)
cipherText := make([]byte, len(padded))
mode.CryptBlocks(cipherText, padded)
wrapper := map[string]string{
"data": base64.StdEncoding.EncodeToString(cipherText),
"time": nonce,
}
out, err := json.Marshal(wrapper)
return out, nonce, err
}
func decryptResponseBody(respBody []byte, secret string) (map[string]interface{}, error) {
var top map[string]interface{}
if err := json.Unmarshal(respBody, &top); err != nil {
return nil, err
}
// 响应格式可能是:
// { "code": 0, "msg": "ok", "data": { "data": "...", "time": "..." } }
// 或者直接就是 { "data": "...", "time": "..." }
var wrapper map[string]interface{}
if v, ok := top["data"].(map[string]interface{}); ok && v["data"] != nil && v["time"] != nil {
wrapper = v
} else {
wrapper = top
}
cipherB64, _ := wrapper["data"].(string)
nonce, _ := wrapper["time"].(string)
if cipherB64 == "" || nonce == "" {
return nil, errors.New("response missing data/time fields")
}
key := deriveKey(secret)
iv := deriveIV(secret, nonce)
cipherBytes, err := base64.StdEncoding.DecodeString(cipherB64)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mode := cipher.NewCBCDecrypter(block, iv)
plainPadded := make([]byte, len(cipherBytes))
mode.CryptBlocks(plainPadded, cipherBytes)
plain, err := pkcs7Unpad(plainPadded)
if err != nil {
return nil, err
}
var out map[string]interface{}
if err := json.Unmarshal(plain, &out); err != nil {
return nil, err
}
return out, nil
}
func bindEmailWithPassword(serverURL, secret, token, email, password, userAgent string) error {
plain := map[string]interface{}{
"email": email,
"password": password,
}
body, _, err := encryptPayload(plain, secret)
if err != nil {
return err
}
req, err := http.NewRequest("POST", serverURL+"/v1/public/user/bind_email_with_password", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", token)
req.Header.Set("Login-Type", "device")
req.Header.Set("User-Agent", userAgent)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBytes, _ := io.ReadAll(resp.Body)
fmt.Println("[绑定邮箱响应]", resp.StatusCode, string(respBytes))
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("bind_email_with_password failed: %s", string(respBytes))
}
return nil
}
func main() {
secret := "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx"
serverURL := "http://127.0.0.1:8080"
identifier := "AP4A.241205.A17"
userAgent := "ppanel-go-test/1.0"
plain := map[string]interface{}{
"identifier": identifier,
"user_agent": userAgent,
}
body, _, err := encryptPayload(plain, secret)
if err != nil {
fmt.Println("加密失败:", err)
os.Exit(2)
}
req, err := http.NewRequest("POST", serverURL+"/v1/auth/login/device", bytes.NewReader(body))
if err != nil {
fmt.Println("请求创建失败:", err)
os.Exit(3)
}
req.Header.Set("Login-Type", "device")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
req.Header.Set("X-Original-Forwarded-For", "127.0.0.1")
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("请求失败:", err)
os.Exit(4)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("读取响应失败:", err)
os.Exit(5)
}
fmt.Println("[加密响应原文]", string(respBytes))
decrypted, err := decryptResponseBody(respBytes, secret)
if err != nil {
fmt.Println("解密失败:", err)
os.Exit(6)
}
fmt.Println("[解密响应明文]", decrypted)
var token string
if t, ok := decrypted["token"].(string); ok && t != "" {
token = t
fmt.Println("✅ 登录成功token =", token)
} else {
fmt.Println("⚠️ 未获取到 token")
}
// 新增:根据邮箱密码绑定设备号(需提供 EMAIL 和 PASSWORD 环境变量)
email := "client@qq.com"
password := "123456"
if token != "" && email != "" && password != "" {
if err := bindEmailWithPassword(serverURL, secret, token, email, password, userAgent); err != nil {
fmt.Println("绑定邮箱失败:", err)
} else {
fmt.Println("✅ 绑定邮箱成功")
}
} else {
fmt.Println("跳过绑定:缺少 token、EMAIL 或 PASSWORD")
}
}
/*
测试 1:
设备A AP4A.241205.017 3节点
邮箱B client05@gmail.com 无套餐
设备A 绑定 邮箱B
结果:
设备A 设备A 没有套餐了
邮箱B 套餐到了 邮箱B的体系下 设备也和邮箱绑定上了
以邮箱为主;
测试 2:
设备A AP4A.241205.018 无套餐
邮箱B client06@gmail.com 3节点
设备A 绑定 邮箱B
结果:
设备A 设备A 没有套餐了
邮箱B 原有套餐还存在 设备也和邮箱绑定上了
以邮箱为主;
测试 3:
设备A AP4A.241205.019 3节点 2025/11/2 13:12:21 2025/10/23 13:12:21
邮箱B client07@gmail.com day套餐
设备A 绑定 邮箱B
结果:
设备A 设备A 没有套餐了
邮箱B 原有套餐还存在 设备也和邮箱绑定上了
以邮箱为主;
*/