refactor(auth): 优化设备登录逻辑,移除冗余代码并添加设备缓存
Build docker and publish / build (20.15.1) (push) Failing after 6m39s
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:
@@ -1,14 +0,0 @@
|
||||
已经绑定过的设备,删除重装,这个时候绑定邮箱用哪个接口
|
||||
删除重新装: 设备号不变, 现在拿着设备登录;实际上还是老的 邮箱+设备
|
||||
|
||||
1. 该设备已经绑定过邮箱了; 没办法进行绑定 需要解绑后再次绑定
|
||||
|
||||
这个场景 如果走 绑定邮箱 需要先解绑 再调用 bind_email_with_password
|
||||
|
||||
如果直接走登录: /v1/auth/login;
|
||||
|
||||
|
||||
|
||||
新设备,未绑定邮箱,用哪个接口
|
||||
bind_email_with_password
|
||||
|
||||
@@ -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
|
||||
@@ -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: 原有套餐还存在 设备也和邮箱绑定上了
|
||||
以邮箱为主;
|
||||
*/
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user