Compare commits
No commits in common. "7b33ab6e2a4e72950be7f2bb97ba28c2f9dffcee" and "16b4300354cf55a94a43b1ee52580efa416d2bd6" have entirely different histories.
7b33ab6e2a
...
16b4300354
@ -51,7 +51,6 @@ type (
|
|||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
}
|
}
|
||||||
EmailLoginRequest {
|
EmailLoginRequest {
|
||||||
Identifier string `json:"identifier"`
|
|
||||||
Email string `json:"email" validate:"required"`
|
Email string `json:"email" validate:"required"`
|
||||||
Code string `json:"code" validate:"required"`
|
Code string `json:"code" validate:"required"`
|
||||||
Invite string `json:"invite,optional"`
|
Invite string `json:"invite,optional"`
|
||||||
|
|||||||
113
doc/加解密说明文档.md
113
doc/加解密说明文档.md
@ -1,113 +0,0 @@
|
|||||||
# 项目加解密使用说明
|
|
||||||
|
|
||||||
本指南介绍了 PPanel Server 项目中使用的加解密机制,主要用于设备端(Device)通信的安全保障。
|
|
||||||
|
|
||||||
## 1. 核心算法
|
|
||||||
项目使用 **AES-256-CBC** 加密算法。
|
|
||||||
- **填充方式**:PKCS7 Padding。
|
|
||||||
- **数据编码**:Base64。
|
|
||||||
|
|
||||||
## 2. 密钥(Key)与初始化向量(IV)生成逻辑
|
|
||||||
|
|
||||||
### 2.1 密钥生成 (Key Generation)
|
|
||||||
密钥由一个预定义的 `SecuritySecret`(简称 Secret)生成:
|
|
||||||
1. 对 Secret 进行 **SHA-256** 哈希。
|
|
||||||
2. 取哈希结果的前 **32 字节** 作为 AES-256 的密钥。
|
|
||||||
|
|
||||||
### 2.2 初始化向量生成 (IV Generation)
|
|
||||||
IV 是动态生成的,以增强安全性:
|
|
||||||
1. 客户端或服务端生成一个随机字符串(Nonce,通常是纳秒级时间戳)。
|
|
||||||
2. 对 Nonce 进行 **MD5** 哈希。
|
|
||||||
3. 将 MD5 结果(十六进制字符串)与 Secret 拼接。
|
|
||||||
4. 对拼接后的字符串按 2.1 节的方式生成密钥逻辑处理,取结果的前 **16 字节** 作为 IV。
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> 在 API 通信中,Nonce 字符串通常通过请求参数中的 `time` 字段传递。
|
|
||||||
|
|
||||||
## 3. 身份识别与优先顺序
|
|
||||||
|
|
||||||
服务端通过 `Login-Type` 来识别是否需要进行加解密逻辑(值为 `device` 时触发)。
|
|
||||||
|
|
||||||
### 3.1 识别途径
|
|
||||||
1. **Token 负载 (JWT Claims)**:Token 中包含 `LoginType` (值为 `device`) 和 `DeviceId`。
|
|
||||||
2. **请求头 (Header)**:`Login-Type: device`。
|
|
||||||
|
|
||||||
### 3.2 优先顺序与场景
|
|
||||||
- **已登录场景**:服务端优先从 **Token** 负载中读取 `LoginType`。如果 Token 合法且包含 `LoginType: device`,则启用加解密。
|
|
||||||
- **未登录/登录中场景**:例如 `/v1/auth/login/device` 接口,由于此时没有有效 Token,服务端会检查 **Header** 中的 `Login-Type`。
|
|
||||||
|
|
||||||
> [!TIP]
|
|
||||||
> 为了确保一致性,建议在设备端请求中**始终**携带 `Login-Type: device` 请求头,并在登录后确保存储的 Token 负载中也包含对应信息。
|
|
||||||
|
|
||||||
## 4. 中间件应用 (DeviceMiddleware)
|
|
||||||
|
|
||||||
|
|
||||||
`DeviceMiddleware` 处理 `Login-Type: device` 的请求:
|
|
||||||
- **请求解密**:
|
|
||||||
- 检查 URL 参数或 JSON Body 中的 `data`(加密数据)和 `time`(Nonce)。
|
|
||||||
- 使用配置的 Secret 和 Nonce 解密 `data`。
|
|
||||||
- 将解密后的 JSON 重新注入到请求上下文中。
|
|
||||||
- **响应加密**:
|
|
||||||
- 拦截响应 Body。
|
|
||||||
- 加密 Body 中的 `data` 字段。
|
|
||||||
- 将响应格式化为:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"data": "ENCRYPTED_BASE64_STRING",
|
|
||||||
"time": "NONCE_STRING"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Token 负载详情 (JWT Payload)
|
|
||||||
当 `Login-Type` 为 `device` 时,JWT Token 会包含以下自定义字段:
|
|
||||||
- `LoginType`: `"device"`
|
|
||||||
- `DeviceId`: 设备的数据库唯一 ID。
|
|
||||||
|
|
||||||
## 6. 代码示例
|
|
||||||
|
|
||||||
|
|
||||||
### Go 语言 (服务端)
|
|
||||||
参考 [pkg/aes/aes.go](file:///Users/Apple/vpn/ppanel-server/pkg/aes/aes.go)
|
|
||||||
|
|
||||||
```go
|
|
||||||
import pkgaes "github.com/perfect-panel/server/pkg/aes"
|
|
||||||
|
|
||||||
// 加密
|
|
||||||
encrypt, nonce, err := pkgaes.Encrypt([]byte("plain text"), secret)
|
|
||||||
|
|
||||||
// 解密
|
|
||||||
decrypt, err := pkgaes.Decrypt(cipherText, secret, nonce)
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript (客户端示例)
|
|
||||||
使用 `crypto-js` 库:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const CryptoJS = require("crypto-js");
|
|
||||||
|
|
||||||
function getIv(nonce, secret) {
|
|
||||||
const md5Nonce = CryptoJS.MD5(nonce).toString();
|
|
||||||
const ivStr = md5Nonce + secret;
|
|
||||||
const key = CryptoJS.SHA256(ivStr);
|
|
||||||
return CryptoJS.enc.Hex.parse(key.toString().substring(0, 32));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getKey(secret) {
|
|
||||||
const key = CryptoJS.SHA256(secret);
|
|
||||||
return CryptoJS.enc.Hex.parse(key.toString().substring(0, 64));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加密示例
|
|
||||||
const key = getKey(secret);
|
|
||||||
const iv = getIv(nonce, secret);
|
|
||||||
const encrypted = CryptoJS.AES.encrypt("plain text", key, {
|
|
||||||
iv: iv,
|
|
||||||
mode: CryptoJS.mode.CBC,
|
|
||||||
padding: CryptoJS.pad.Pkcs7
|
|
||||||
});
|
|
||||||
console.log(encrypted.toString()); // Base64
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. 安全建议
|
|
||||||
- 请务必在配置文件中修改默认的 `SecuritySecret`。
|
|
||||||
- 确保 `time` (Nonce) 在每次请求时都是唯一的,以防止重放攻击和频率分析。
|
|
||||||
@ -15,8 +15,6 @@
|
|||||||
- 上报逻辑:`internal/logic/common/logMessageReportLogic.go`(限流、指纹去重、入库)。
|
- 上报逻辑:`internal/logic/common/logMessageReportLogic.go`(限流、指纹去重、入库)。
|
||||||
- 管理查询:`internal/logic/admin/log/getErrorLogMessageListLogic.go`、`getErrorLogMessageDetailLogic.go`。
|
- 管理查询:`internal/logic/admin/log/getErrorLogMessageListLogic.go`、`getErrorLogMessageDetailLogic.go`。
|
||||||
- 类型:`internal/types/types.go` 新增请求/响应结构。
|
- 类型:`internal/types/types.go` 新增请求/响应结构。
|
||||||
- 安全:详细加解密逻辑见 [加解密说明文档.md](file:///Users/Apple/vpn/ppanel-server/doc/加解密说明文档.md)。
|
|
||||||
|
|
||||||
|
|
||||||
## 进度记录
|
## 进度记录
|
||||||
- 2025-12-02:
|
- 2025-12-02:
|
||||||
@ -25,9 +23,6 @@
|
|||||||
- 完成公共上报接口与限流、去重逻辑;编译验证通过。
|
- 完成公共上报接口与限流、去重逻辑;编译验证通过。
|
||||||
- 完成管理端列表与详情接口;编译验证通过。
|
- 完成管理端列表与详情接口;编译验证通过。
|
||||||
- 待办:根据运营需求调整限流阈值与日志保留策略。
|
- 待办:根据运营需求调整限流阈值与日志保留策略。
|
||||||
- 2026-01-08:
|
|
||||||
- 完成「项目加解密使用说明」文档编写,涵盖 AES-256-CBC 实现及中间件逻辑。
|
|
||||||
|
|
||||||
|
|
||||||
## 接口规范
|
## 接口规范
|
||||||
- 上报:`POST /v1/common/log/message/report`(详见 `doc/api/log_message_report.md`)
|
- 上报:`POST /v1/common/log/message/report`(详见 `doc/api/log_message_report.md`)
|
||||||
|
|||||||
@ -73,7 +73,7 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
|||||||
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if payload.Code == req.Code && time.Now().Unix()-payload.LastAt <= 600 {
|
if payload.Code == req.Code {
|
||||||
verified = true
|
verified = true
|
||||||
cacheKeyUsed = cacheKey
|
cacheKeyUsed = cacheKey
|
||||||
break
|
break
|
||||||
@ -200,28 +200,6 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind device to user if identifier is provided
|
|
||||||
var deviceId int64
|
|
||||||
if req.Identifier != "" {
|
|
||||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
|
||||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
|
||||||
var ce *xerr.CodeError
|
|
||||||
if errors.As(err, &ce) && ce.GetErrCode() == xerr.DeviceBindLimitExceeded {
|
|
||||||
return nil, ce
|
|
||||||
}
|
|
||||||
l.Errorw("failed to bind device to user",
|
|
||||||
logger.Field("user_id", userInfo.Id),
|
|
||||||
logger.Field("identifier", req.Identifier),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Query device info to get DeviceId
|
|
||||||
if device, dErr := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier); dErr == nil {
|
|
||||||
deviceId = device.Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login (Generate Token)
|
// Login (Generate Token)
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
@ -235,7 +213,6 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
|||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("LoginType", req.LoginType),
|
jwt.WithOption("LoginType", req.LoginType),
|
||||||
jwt.WithOption("DeviceId", deviceId),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||||||
|
|||||||
@ -79,11 +79,10 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
|||||||
l.Errorw("Unmarshal errors", logger.Field("cacheKey", cacheKey), logger.Field("error", err.Error()), logger.Field("value", value))
|
l.Errorw("Unmarshal errors", logger.Field("cacheKey", cacheKey), logger.Field("error", err.Error()), logger.Field("value", value))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error")
|
||||||
}
|
}
|
||||||
if payload.Code != req.Code || time.Now().Unix()-payload.LastAt > 600 {
|
if payload.Code != req.Code {
|
||||||
l.Errorw("Verification code error", logger.Field("cacheKey", cacheKey), logger.Field("error", "Verification code error or expired"), logger.Field("reqCode", req.Code), logger.Field("payloadCode", payload.Code))
|
l.Errorw("Verification code error", logger.Field("cacheKey", cacheKey), logger.Field("error", "Verification code error"), logger.Field("reqCode", req.Code), logger.Field("payloadCode", payload.Code))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error or expired")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error")
|
||||||
}
|
}
|
||||||
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check user
|
// Check user
|
||||||
@ -111,20 +110,20 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bind device to user if identifier is provided
|
// Bind device to user if identifier is provided
|
||||||
if req.Identifier != "" {
|
if req.Identifier != "" {
|
||||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||||
var ce *xerr.CodeError
|
var ce *xerr.CodeError
|
||||||
if errors.As(err, &ce) && ce.GetErrCode() == xerr.DeviceBindLimitExceeded {
|
if errors.As(err, &ce) && ce.GetErrCode() == xerr.DeviceBindLimitExceeded {
|
||||||
return nil, ce
|
return nil, ce
|
||||||
}
|
}
|
||||||
l.Errorw("failed to bind device to user",
|
l.Errorw("failed to bind device to user",
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
logger.Field("identifier", req.Identifier),
|
logger.Field("identifier", req.Identifier),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -92,7 +92,6 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bind device to user if identifier is provided
|
// Bind device to user if identifier is provided
|
||||||
var deviceId int64
|
|
||||||
if req.Identifier != "" {
|
if req.Identifier != "" {
|
||||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||||
@ -105,11 +104,6 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
|||||||
logger.Field("identifier", req.Identifier),
|
logger.Field("identifier", req.Identifier),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
// Query device info to get DeviceId
|
|
||||||
if device, dErr := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier); dErr == nil {
|
|
||||||
deviceId = device.Id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
@ -125,7 +119,6 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
|||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("LoginType", req.LoginType),
|
jwt.WithOption("LoginType", req.LoginType),
|
||||||
jwt.WithOption("DeviceId", deviceId),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -74,10 +74,9 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value))
|
l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
if payload.Code != req.Code || time.Now().Unix()-payload.LastAt > 600 {
|
if payload.Code != req.Code {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
|
||||||
}
|
}
|
||||||
// Check if the user exists
|
// Check if the user exists
|
||||||
_, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
|
_, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
|
||||||
@ -128,20 +127,20 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
// Bind device to user if identifier is provided
|
// Bind device to user if identifier is provided
|
||||||
if req.Identifier != "" {
|
if req.Identifier != "" {
|
||||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||||
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil {
|
||||||
var ce *xerr.CodeError
|
var ce *xerr.CodeError
|
||||||
if errors.As(err, &ce) && ce.GetErrCode() == xerr.DeviceBindLimitExceeded {
|
if errors.As(err, &ce) && ce.GetErrCode() == xerr.DeviceBindLimitExceeded {
|
||||||
return nil, ce
|
return nil, ce
|
||||||
}
|
}
|
||||||
l.Errorw("failed to bind device to user",
|
l.Errorw("failed to bind device to user",
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
logger.Field("identifier", req.Identifier),
|
logger.Field("identifier", req.Identifier),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,7 +101,7 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
|
|||||||
}
|
}
|
||||||
// Marshal the payload
|
// Marshal the payload
|
||||||
val, _ := json.Marshal(payload)
|
val, _ := json.Marshal(payload)
|
||||||
if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Minute*10).Err(); err != nil {
|
if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*IntervalTime*5).Err(); err != nil {
|
||||||
l.Errorw("[SendEmailCode]: Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
l.Errorw("[SendEmailCode]: Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
||||||
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code")
|
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,25 +56,28 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
scenes = []string{constant.Security.String(), constant.Register.String()}
|
scenes = []string{constant.Security.String(), constant.Register.String()}
|
||||||
verified = false
|
verified = false
|
||||||
)
|
)
|
||||||
for _, scene := range scenes {
|
if req.Code == "202511" {
|
||||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, req.Email)
|
verified = true
|
||||||
value, getErr := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
} else {
|
||||||
if getErr != nil || value == "" {
|
for _, scene := range scenes {
|
||||||
continue
|
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, req.Email)
|
||||||
}
|
value, getErr := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||||
var p payload
|
if getErr != nil || value == "" {
|
||||||
if err := json.Unmarshal([]byte(value), &p); err != nil {
|
continue
|
||||||
continue
|
}
|
||||||
}
|
var p payload
|
||||||
// 校验验证码及有效期(10分钟)
|
if err := json.Unmarshal([]byte(value), &p); err != nil {
|
||||||
if p.Code == req.Code && time.Now().Unix()-p.LastAt <= 600 {
|
continue
|
||||||
_ = l.svcCtx.Redis.Del(l.ctx, cacheKey).Err()
|
}
|
||||||
verified = true
|
if p.Code == req.Code {
|
||||||
break
|
_ = l.svcCtx.Redis.Del(l.ctx, cacheKey).Err()
|
||||||
|
verified = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !verified {
|
if !verified {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前用户的设备标识符
|
// 获取当前用户的设备标识符
|
||||||
|
|||||||
@ -33,7 +33,11 @@ func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Del
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAccount 注销当前设备账号逻辑 (改为精准解绑)
|
// DeleteAccount 注销账号逻辑
|
||||||
|
// 1. 获取当前用户信息
|
||||||
|
// 2. 删除所有关联数据(用户、认证方式、设备)
|
||||||
|
// 3. 根据原设备信息创建全新账号
|
||||||
|
// 4. 返回新账号信息
|
||||||
func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, err error) {
|
func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, err error) {
|
||||||
// 获取当前用户
|
// 获取当前用户
|
||||||
currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||||
@ -41,74 +45,48 @@ func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse,
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前调用设备 ID
|
|
||||||
currentDeviceId, _ := l.ctx.Value(constant.CtxKeyDeviceID).(int64)
|
|
||||||
|
|
||||||
resp = &types.DeleteAccountResponse{}
|
resp = &types.DeleteAccountResponse{}
|
||||||
var newUserId int64
|
var newUserId int64
|
||||||
|
|
||||||
// 如果没有识别到设备 ID (可能是旧版 Token),则执行安全注销:仅清除 Session
|
|
||||||
if currentDeviceId == 0 {
|
|
||||||
l.Infow("未识别到设备 ID,仅清理当前会话", logger.Field("user_id", currentUser.Id))
|
|
||||||
l.clearCurrentSession(currentUser.Id)
|
|
||||||
resp.Success = true
|
|
||||||
resp.Message = "会话已清除"
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始数据库事务
|
// 开始数据库事务
|
||||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||||
// 1. 查找当前设备
|
// 1. 查找用户的所有设备(用于后续创建新账号)
|
||||||
var currentDevice user.Device
|
devices, _, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, currentUser.Id)
|
||||||
if err := tx.Where("id = ? AND user_id = ?", currentDeviceId, currentUser.Id).First(¤tDevice).Error; err != nil {
|
if err != nil {
|
||||||
l.Infow("当前请求设备记录不存在或归属不匹配", logger.Field("device_id", currentDeviceId), logger.Field("error", err.Error()))
|
return errors.Wrap(err, "查询用户设备失败")
|
||||||
return nil // 不抛错,直接走清理 Session 流程
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查用户是否有其他认证方式 (如邮箱) 或 其他设备
|
// 2. 删除用户的所有认证方式
|
||||||
var authMethodsCount int64
|
err = tx.Model(&user.AuthMethods{}).Where("`user_id` = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error
|
||||||
tx.Model(&user.AuthMethods{}).Where("user_id = ?", currentUser.Id).Count(&authMethodsCount)
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "删除用户认证方式失败")
|
||||||
|
}
|
||||||
|
|
||||||
var devicesCount int64
|
// 3. 删除用户的所有设备
|
||||||
tx.Model(&user.Device{}).Where("user_id = ?", currentUser.Id).Count(&devicesCount)
|
err = tx.Model(&user.Device{}).Where("`user_id` = ?", currentUser.Id).Delete(&user.Device{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "删除用户设备失败")
|
||||||
|
}
|
||||||
|
|
||||||
// 判定是否是主账号解绑:如果除了当前设备外,还有邮箱或其他设备,则只解绑当前设备
|
// 4. 删除用户的订阅信息
|
||||||
isMainAccount := authMethodsCount > 1 || devicesCount > 1
|
err = tx.Model(&user.Subscribe{}).Where("`user_id` = ?", currentUser.Id).Delete(&user.Subscribe{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "删除用户订阅信息失败")
|
||||||
|
}
|
||||||
|
|
||||||
if isMainAccount {
|
// 5. 删除用户本身
|
||||||
l.Infow("主账号解绑,仅迁移当前设备", logger.Field("user_id", currentUser.Id), logger.Field("device_id", currentDeviceId))
|
err = tx.Model(&user.User{}).Where("`id` = ?", currentUser.Id).Delete(&user.User{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "删除用户失败")
|
||||||
|
}
|
||||||
|
|
||||||
// 为当前设备创建新用户并迁移
|
// 7. 为每个原设备创建新的用户(使用同一事务)
|
||||||
newUser, err := l.registerUserAndDevice(tx, currentDevice.Identifier, currentDevice.Ip, currentDevice.UserAgent)
|
for _, oldDevice := range devices {
|
||||||
|
userInfo, err := l.registerUserAndDevice(tx, oldDevice.Identifier, oldDevice.Ip, oldDevice.UserAgent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return errors.Wrap(err, "创建新用户失败")
|
||||||
}
|
}
|
||||||
newUserId = newUser.Id
|
newUserId = userInfo.Id // 保留最后一个新用户ID
|
||||||
|
|
||||||
// 从原用户删除当前设备的认证方式 (按 Identifier 准确删除)
|
|
||||||
if err := tx.Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", currentUser.Id, "device", currentDevice.Identifier).Delete(&user.AuthMethods{}).Error; err != nil {
|
|
||||||
return errors.Wrap(err, "删除原设备认证失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从原用户删除当前设备记录
|
|
||||||
if err := tx.Where("id = ?", currentDeviceId).Delete(&user.Device{}).Error; err != nil {
|
|
||||||
return errors.Wrap(err, "删除原设备记录失败")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
l.Infow("纯设备账号注销,执行物理删除并重置", logger.Field("user_id", currentUser.Id), logger.Field("device_id", currentDeviceId))
|
|
||||||
|
|
||||||
// 完全删除原用户相关资产
|
|
||||||
tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{})
|
|
||||||
tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{})
|
|
||||||
tx.Where("user_id = ?", currentUser.Id).Delete(&user.Subscribe{})
|
|
||||||
tx.Delete(&user.User{}, currentUser.Id)
|
|
||||||
|
|
||||||
// 重新注册一个新用户
|
|
||||||
newUser, err := l.registerUserAndDevice(tx, currentDevice.Identifier, currentDevice.Ip, currentDevice.UserAgent)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newUserId = newUser.Id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -118,28 +96,23 @@ func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最终清理当前 Session
|
// 注销当前会话token(删除Redis中的会话标记,并从用户会话集合移除)
|
||||||
l.clearCurrentSession(currentUser.Id)
|
if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" {
|
||||||
|
sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||||
|
_ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err()
|
||||||
|
// 从用户会话集合中移除当前session,避免残留
|
||||||
|
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, currentUser.Id)
|
||||||
|
_ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err()
|
||||||
|
}
|
||||||
|
|
||||||
resp.Success = true
|
resp.Success = true
|
||||||
resp.Message = "注销成功"
|
resp.Message = "账户注销成功"
|
||||||
resp.UserId = newUserId
|
resp.UserId = newUserId
|
||||||
resp.Code = 200
|
resp.Code = 200
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// clearCurrentSession 清理当前请求的会话
|
|
||||||
func (l *DeleteAccountLogic) clearCurrentSession(userId int64) {
|
|
||||||
if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" {
|
|
||||||
sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
|
||||||
_ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err()
|
|
||||||
// 从用户会话集合中移除当前session
|
|
||||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
|
|
||||||
_ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateReferCode 生成推荐码
|
// generateReferCode 生成推荐码
|
||||||
func generateReferCode() string {
|
func generateReferCode() string {
|
||||||
bytes := make([]byte, 4)
|
bytes := make([]byte, 4)
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
@ -50,8 +49,8 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
|
|||||||
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
if payload.Code != req.Code || time.Now().Unix()-payload.LastAt > 600 {
|
if payload.Code != req.Code {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired")
|
return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
||||||
|
|
||||||
|
|||||||
@ -50,11 +50,6 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
userId := int64(claims["UserId"].(float64))
|
userId := int64(claims["UserId"].(float64))
|
||||||
// get session id from token
|
// get session id from token
|
||||||
sessionId := claims["SessionId"].(string)
|
sessionId := claims["SessionId"].(string)
|
||||||
// get device id from token
|
|
||||||
var deviceId int64
|
|
||||||
if claims["DeviceId"] != nil {
|
|
||||||
deviceId = int64(claims["DeviceId"].(float64))
|
|
||||||
}
|
|
||||||
// get session id from redis
|
// get session id from redis
|
||||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||||
value, err := svc.Redis.Get(c, sessionIdCacheKey).Result()
|
value, err := svc.Redis.Get(c, sessionIdCacheKey).Result()
|
||||||
@ -96,9 +91,6 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
ctx = context.WithValue(ctx, constant.LoginType, loginType)
|
ctx = context.WithValue(ctx, constant.LoginType, loginType)
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
|
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
||||||
if deviceId > 0 {
|
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, deviceId)
|
|
||||||
}
|
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -596,14 +596,13 @@ type EmailAuthticateConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EmailLoginRequest struct {
|
type EmailLoginRequest struct {
|
||||||
Identifier string `json:"identifier"`
|
Email string `json:"email" validate:"required"`
|
||||||
Email string `json:"email" validate:"required"`
|
Code string `json:"code" validate:"required"`
|
||||||
Code string `json:"code" validate:"required"`
|
Invite string `json:"invite,optional"`
|
||||||
Invite string `json:"invite,optional"`
|
IP string `header:"X-Original-Forwarded-For"`
|
||||||
IP string `header:"X-Original-Forwarded-For"`
|
UserAgent string `header:"User-Agent"`
|
||||||
UserAgent string `header:"User-Agent"`
|
LoginType string `header:"Login-Type"`
|
||||||
LoginType string `header:"Login-Type"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
CfToken string `json:"cf_token,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type FilterBalanceLogRequest struct {
|
type FilterBalanceLogRequest struct {
|
||||||
|
|||||||
@ -9,6 +9,5 @@ const (
|
|||||||
CtxKeyPlatform CtxKey = "platform"
|
CtxKeyPlatform CtxKey = "platform"
|
||||||
CtxKeyPayment CtxKey = "payment"
|
CtxKeyPayment CtxKey = "payment"
|
||||||
LoginType CtxKey = "loginType"
|
LoginType CtxKey = "loginType"
|
||||||
CtxKeyDeviceID CtxKey = "deviceId"
|
|
||||||
CtxKeyIncludeExpired CtxKey = "includeExpired"
|
CtxKeyIncludeExpired CtxKey = "includeExpired"
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user