refactor(auth): 优化设备登录逻辑,移除冗余代码并添加设备缓存
Build docker and publish / build (20.15.1) (push) Failing after 6m39s

feat(database): 添加用户算法和盐字段的迁移脚本

fix(subscribe): 修复服务器用户列表缓存问题,临时禁用缓存

style(model): 清理用户模型注释,简化代码结构

chore: 删除无用脚本和测试文件

docs: 添加用户绑定流程文档

perf(login): 优化设备登录性能,添加设备缓存键

fix(unbind): 修复设备解绑时的缓存清理逻辑

refactor(verify): 简化邮箱验证逻辑,移除冗余代码

build(docker): 更新Dockerfile配置,使用scratch基础镜像
This commit is contained in:
2025-10-28 20:46:21 -07:00
parent e6bd78aa76
commit 0f38b3fcd3
33 changed files with 226 additions and 1605 deletions
-14
View File
@@ -1,14 +0,0 @@
已经绑定过的设备,删除重装,这个时候绑定邮箱用哪个接口
删除重新装: 设备号不变, 现在拿着设备登录;实际上还是老的 邮箱+设备
1. 该设备已经绑定过邮箱了; 没办法进行绑定 需要解绑后再次绑定
这个场景 如果走 绑定邮箱 需要先解绑 再调用 bind_email_with_password
如果直接走登录: /v1/auth/login;
新设备,未绑定邮箱,用哪个接口
bind_email_with_password
-21
View File
@@ -1,21 +0,0 @@
#!/bin/bash
# build-and-push.sh
set -e
cd /Users/Apple/vpn/ppanel-server
# 固定版本号为latest
VERSION=v1.0
# 构建镜像
echo "Building image with version: $VERSION"
docker build -f Dockerfile --platform linux/amd64 --build-arg TARGETARCH=amd64 -t registry.kxsw.us/ppanel/ario-server:$VERSION .
docker tag registry.kxsw.us/ppanel/ario-server:$VERSION registry.kxsw.us/ppanel/ario-server:$VERSION
# 推送镜像
echo "Pushing image to registry.kxsw.us"
docker push registry.kxsw.us/ppanel/ario-server:$VERSION
docker push registry.kxsw.us/ppanel/ario-server:$VERSION
echo "Build and push completed successfully!"
# docker-compose exec certbot certbot certonly --webroot --webroot-path=/etc/letsencrypt -d api-dev.kxsw.us
-269
View File
@@ -1,269 +0,0 @@
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: 原有套餐还存在 设备也和邮箱绑定上了
以邮箱为主;
*/
-89
View File
@@ -1,89 +0,0 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/gorilla/websocket"
)
func main() {
// 默认配置,可用环境变量覆盖
baseURL := getenvDefault("SERVER_URL", "wss://api.hifast.biz")
userID := getenvDefault("USER_ID", "23")
deviceID := getenvDefault("DEVICE_ID", "c76463cff8512722")
auth := "dkjw6TCQTBlgreyhjN8u32gP0A6RrQ/V50vf8wjNFwFL9hgKJrOOv+ziS03GCQ/8E0fWUzjc4aCoMcVMzUN8vR7CwqR45HbtogoT9iNoElW9rgzpQNbwQ4BHK/Q25WvcgdrhfRzE19nPqUTOcN+4iY6NmeiwHEMLBTzDEeu8wGn/yjVLRMCyh5QJuQizllbrDR5LuTiNEcdSdBSx9cFZYtnJIIyi1b60BZYo4lIyRADCH6smTsLDhoZG0nJvJw3C0XCGvf0jC/4d4u40IvbzKOm1TBSK0lgOzNjvkSfS/DJibAi4l7qNTYmFlQ1wp+iW1MNllqd+OtSavZYoajoZGA=="
// 拼接完整 WS 地址
wsURL := fmt.Sprintf("%s/v1/app/ws/%s/%s", baseURL, userID, deviceID)
// 自定义 header(包含 Authorization
header := http.Header{}
header.Set("Authorization", auth)
// 建立连接
dialer := websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
conn, _, err := dialer.Dial(wsURL, header)
if err != nil {
log.Fatalf("dial error: %v", err)
}
defer conn.Close()
log.Println("connected to", wsURL)
// 优雅退出
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, os.Interrupt)
// 心跳:定时发送 "ping"
ticker := time.NewTicker(25 * time.Second)
defer ticker.Stop()
// 读协程
done := make(chan struct{})
go func() {
defer close(done)
for {
mt, msg, err := conn.ReadMessage()
if err != nil {
log.Printf("read error: %v", err)
return
}
log.Printf("recv [%d]: %s", mt, string(msg))
}
}()
// 连接成功后先发一个 "ping"
if err := conn.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
log.Printf("write ping error: %v", err)
}
for {
select {
case <-done:
return
case <-ticker.C:
if err := conn.WriteMessage(websocket.TextMessage, []byte("ping")); err != nil {
log.Printf("write heartbeat error: %v", err)
return
}
log.Printf("sent heartbeat ping")
case <-interrupt:
log.Println("interrupt, closing...")
_ = conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
return
}
}
}
func getenvDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}