diff --git a/doc/加解密说明文档.md b/doc/加解密说明文档.md new file mode 100644 index 0000000..a599849 --- /dev/null +++ b/doc/加解密说明文档.md @@ -0,0 +1,113 @@ +# 项目加解密使用说明 + +本指南介绍了 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) 在每次请求时都是唯一的,以防止重放攻击和频率分析。 diff --git a/doc/说明文档.md b/doc/说明文档.md index 4368af2..7ddc2e4 100644 --- a/doc/说明文档.md +++ b/doc/说明文档.md @@ -15,6 +15,8 @@ - 上报逻辑:`internal/logic/common/logMessageReportLogic.go`(限流、指纹去重、入库)。 - 管理查询:`internal/logic/admin/log/getErrorLogMessageListLogic.go`、`getErrorLogMessageDetailLogic.go`。 - 类型:`internal/types/types.go` 新增请求/响应结构。 +- 安全:详细加解密逻辑见 [加解密说明文档.md](file:///Users/Apple/vpn/ppanel-server/doc/加解密说明文档.md)。 + ## 进度记录 - 2025-12-02: @@ -23,6 +25,9 @@ - 完成公共上报接口与限流、去重逻辑;编译验证通过。 - 完成管理端列表与详情接口;编译验证通过。 - 待办:根据运营需求调整限流阈值与日志保留策略。 +- 2026-01-08: + - 完成「项目加解密使用说明」文档编写,涵盖 AES-256-CBC 实现及中间件逻辑。 + ## 接口规范 - 上报:`POST /v1/common/log/message/report`(详见 `doc/api/log_message_report.md`) diff --git a/internal/logic/auth/emailLoginLogic.go b/internal/logic/auth/emailLoginLogic.go index cebd312..d259a9e 100644 --- a/internal/logic/auth/emailLoginLogic.go +++ b/internal/logic/auth/emailLoginLogic.go @@ -73,7 +73,7 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types. if err := json.Unmarshal([]byte(value), &payload); err != nil { continue } - if payload.Code == req.Code { + if payload.Code == req.Code && time.Now().Unix()-payload.LastAt <= 600 { verified = true cacheKeyUsed = cacheKey break diff --git a/internal/logic/auth/resetPasswordLogic.go b/internal/logic/auth/resetPasswordLogic.go index bd4cb2e..86df816 100644 --- a/internal/logic/auth/resetPasswordLogic.go +++ b/internal/logic/auth/resetPasswordLogic.go @@ -79,10 +79,11 @@ 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)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error") } - if payload.Code != req.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") + if payload.Code != req.Code || time.Now().Unix()-payload.LastAt > 600 { + 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)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "Verification code error or expired") } + l.svcCtx.Redis.Del(l.ctx, cacheKey) } // Check user @@ -110,20 +111,20 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res } // Bind device to user if identifier is provided - 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()), - ) - } - } + 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()), + ) + } + } if l.ctx.Value(constant.LoginType) != nil { req.LoginType = l.ctx.Value(constant.LoginType).(string) } diff --git a/internal/logic/auth/userRegisterLogic.go b/internal/logic/auth/userRegisterLogic.go index 1ac69a9..edcfbcb 100644 --- a/internal/logic/auth/userRegisterLogic.go +++ b/internal/logic/auth/userRegisterLogic.go @@ -74,9 +74,10 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * l.Errorw("Unmarshal Error", logger.Field("error", err.Error()), logger.Field("value", value)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") } - if payload.Code != req.Code { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + if payload.Code != req.Code || time.Now().Unix()-payload.LastAt > 600 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired") } + l.svcCtx.Redis.Del(l.ctx, cacheKey) } // Check if the user exists _, err = l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email) @@ -127,20 +128,20 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp * return nil }) // Bind device to user if identifier is provided - 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()), - ) - } - } + 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()), + ) + } + } if l.ctx.Value(constant.LoginType) != nil { req.LoginType = l.ctx.Value(constant.LoginType).(string) } diff --git a/internal/logic/common/sendEmailCodeLogic.go b/internal/logic/common/sendEmailCodeLogic.go index 1764ed9..345ed7a 100644 --- a/internal/logic/common/sendEmailCodeLogic.go +++ b/internal/logic/common/sendEmailCodeLogic.go @@ -101,7 +101,7 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty } // Marshal the payload val, _ := json.Marshal(payload) - if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*IntervalTime*5).Err(); err != nil { + if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Minute*10).Err(); err != nil { 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") } diff --git a/internal/logic/public/user/bindEmailWithVerificationLogic.go b/internal/logic/public/user/bindEmailWithVerificationLogic.go index fa34dbb..1b492f3 100644 --- a/internal/logic/public/user/bindEmailWithVerificationLogic.go +++ b/internal/logic/public/user/bindEmailWithVerificationLogic.go @@ -56,28 +56,25 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi scenes = []string{constant.Security.String(), constant.Register.String()} verified = false ) - if req.Code == "202511" { - verified = true - } else { - for _, scene := range scenes { - cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, req.Email) - value, getErr := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() - if getErr != nil || value == "" { - continue - } - var p payload - if err := json.Unmarshal([]byte(value), &p); err != nil { - continue - } - if p.Code == req.Code { - _ = l.svcCtx.Redis.Del(l.ctx, cacheKey).Err() - verified = true - break - } + for _, scene := range scenes { + cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, scene, req.Email) + value, getErr := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() + if getErr != nil || value == "" { + continue + } + var p payload + if err := json.Unmarshal([]byte(value), &p); err != nil { + continue + } + // 校验验证码及有效期(10分钟) + if p.Code == req.Code && time.Now().Unix()-p.LastAt <= 600 { + _ = l.svcCtx.Redis.Del(l.ctx, cacheKey).Err() + verified = true + break } } if !verified { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired") } // 获取当前用户的设备标识符 diff --git a/internal/logic/public/user/verifyEmailLogic.go b/internal/logic/public/user/verifyEmailLogic.go index 4d48df1..27362d6 100644 --- a/internal/logic/public/user/verifyEmailLogic.go +++ b/internal/logic/public/user/verifyEmailLogic.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/model/user" @@ -49,8 +50,8 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey)) return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") } - if payload.Code != req.Code { - return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error") + if payload.Code != req.Code || time.Now().Unix()-payload.LastAt > 600 { + return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired") } l.svcCtx.Redis.Del(l.ctx, cacheKey)