feat: 调整客户信息表删除字段为del_dlag
This commit is contained in:
parent
433b4469de
commit
f7cb916e01
|
|
@ -9,7 +9,7 @@ CREATE TABLE t_platform_user (
|
||||||
last_login_time TIMESTAMP NULL COMMENT '最后登录时间',
|
last_login_time TIMESTAMP NULL COMMENT '最后登录时间',
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
deleted TINYINT DEFAULT 0 COMMENT '软删除:0-未删,1-已删',
|
delFlag TINYINT DEFAULT 0 COMMENT '软删除:0-未删,1-已删',
|
||||||
UNIQUE KEY uk_platform_openid (platform_type, platform_openid),
|
UNIQUE KEY uk_platform_openid (platform_type, platform_openid),
|
||||||
CONSTRAINT fk_platform_user_user_id FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE
|
CONSTRAINT fk_platform_user_user_id FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台用户关联表(微信/抖音小程序用户信息)';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台用户关联表(微信/抖音小程序用户信息)';
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,5 @@ CREATE TABLE t_user (
|
||||||
status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
status TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-正常',
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
deleted TINYINT DEFAULT 0 COMMENT '软删除:0-未删,1-已删'
|
delFlag TINYINT DEFAULT 0 COMMENT '软删除:0-未删,1-已删'
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户基础信息表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户基础信息表';
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ CREATE TABLE t_platform_user (
|
||||||
last_login_time TIMESTAMP,
|
last_login_time TIMESTAMP,
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
deleted SMALLINT DEFAULT 0,
|
delFlag SMALLINT DEFAULT 0,
|
||||||
UNIQUE (platform_type, platform_openid),
|
UNIQUE (platform_type, platform_openid),
|
||||||
CONSTRAINT fk_platform_user_user_id FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE
|
CONSTRAINT fk_platform_user_user_id FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
@ -25,7 +25,7 @@ COMMENT ON COLUMN t_platform_user.platform_extra IS '平台扩展字段(如抖
|
||||||
COMMENT ON COLUMN t_platform_user.last_login_time IS '最后登录时间';
|
COMMENT ON COLUMN t_platform_user.last_login_time IS '最后登录时间';
|
||||||
COMMENT ON COLUMN t_platform_user.create_time IS '创建时间';
|
COMMENT ON COLUMN t_platform_user.create_time IS '创建时间';
|
||||||
COMMENT ON COLUMN t_platform_user.update_time IS '更新时间';
|
COMMENT ON COLUMN t_platform_user.update_time IS '更新时间';
|
||||||
COMMENT ON COLUMN t_platform_user.deleted IS '软删除:0-未删,1-已删';
|
COMMENT ON COLUMN t_platform_user.delFlag IS '软删除:0-未删,1-已删';
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION set_update_time()
|
CREATE OR REPLACE FUNCTION set_update_time()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ CREATE TABLE t_user (
|
||||||
status SMALLINT DEFAULT 1,
|
status SMALLINT DEFAULT 1,
|
||||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
deleted SMALLINT DEFAULT 0,
|
delFlag SMALLINT DEFAULT 0,
|
||||||
UNIQUE (phone)
|
UNIQUE (phone)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ COMMENT ON COLUMN t_user.gender IS '性别:0-未知,1-男,2-女';
|
||||||
COMMENT ON COLUMN t_user.status IS '状态:0-禁用,1-正常';
|
COMMENT ON COLUMN t_user.status IS '状态:0-禁用,1-正常';
|
||||||
COMMENT ON COLUMN t_user.create_time IS '创建时间';
|
COMMENT ON COLUMN t_user.create_time IS '创建时间';
|
||||||
COMMENT ON COLUMN t_user.update_time IS '更新时间';
|
COMMENT ON COLUMN t_user.update_time IS '更新时间';
|
||||||
COMMENT ON COLUMN t_user.deleted IS '软删除:0-未删,1-已删';
|
COMMENT ON COLUMN t_user.delFlag IS '软删除:0-未删,1-已删';
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION set_update_time()
|
CREATE OR REPLACE FUNCTION set_update_time()
|
||||||
RETURNS TRIGGER AS $$
|
RETURNS TRIGGER AS $$
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# 前端工作清单:请求/响应参数加密对接
|
||||||
|
|
||||||
|
本文档用于前端工程师对接后端新增的“请求/响应参数加密”能力。
|
||||||
|
|
||||||
|
## 一、背景与目标
|
||||||
|
|
||||||
|
- 后端新增可配置的参数加密能力(默认关闭)。
|
||||||
|
- 当服务端开启时,前端需按约定对请求体进行加密,并能解密响应体。
|
||||||
|
|
||||||
|
## 二、开关与触发规则
|
||||||
|
|
||||||
|
- 只有当后端配置开启时才需要启用前端加密。
|
||||||
|
- 通过请求头 `X-App-Encrypt: 1` 标记本次请求体已加密。
|
||||||
|
- 如果后端开启“响应加密”,返回会带响应头 `X-App-Encrypt: 1`,响应体为加密载荷。
|
||||||
|
- 白名单路径(如 `/swagger/`)不会被加解密;前端无需处理。
|
||||||
|
|
||||||
|
## 三、加密协议与格式
|
||||||
|
|
||||||
|
- 加密算法:AES-GCM。
|
||||||
|
- 密钥:后端配置 `payload_crypto.secret_key`,需与前端一致。
|
||||||
|
- 请求/响应体加密后的 JSON 结构:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "nonce": "base64", "ciphertext": "base64" }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `nonce` 为 AES-GCM 的随机随机数(Base64),`ciphertext` 为密文(Base64)。
|
||||||
|
|
||||||
|
## 四、前端需要实现的功能
|
||||||
|
|
||||||
|
1. **请求加密**
|
||||||
|
- 当后端开启 `payload_crypto.request.enable` 时,对请求 JSON 体加密。
|
||||||
|
- 请求头添加 `X-App-Encrypt: 1`。
|
||||||
|
- 发送的请求体替换为加密后的 JSON 结构(`nonce` + `ciphertext`)。
|
||||||
|
|
||||||
|
2. **响应解密**
|
||||||
|
- 如果响应头包含 `X-App-Encrypt: 1`,则对响应体解密。
|
||||||
|
- 解密后得到原始 JSON,再进入业务处理流程。
|
||||||
|
|
||||||
|
3. **错误处理**
|
||||||
|
- 解密失败时要能捕获并上报(日志/埋点),避免页面卡死。
|
||||||
|
- 解密失败可提示“数据解析失败,请重试”。
|
||||||
|
|
||||||
|
4. **降级与兼容**
|
||||||
|
- 若响应体不是加密结构(未带头或非 JSON),直接走原逻辑。
|
||||||
|
- 与后端保持兼容:后端若未开启加密,不应影响现有请求。
|
||||||
|
|
||||||
|
## 五、实现建议
|
||||||
|
|
||||||
|
- 统一封装在请求拦截器/响应拦截器层:
|
||||||
|
- Web:Axios 拦截器。
|
||||||
|
- 小程序:封装 `wx.request` 统一处理。
|
||||||
|
- AES-GCM 需支持:
|
||||||
|
- Web: Web Crypto API(`window.crypto.subtle`)。
|
||||||
|
- 小程序: 引入可用的 AES-GCM 实现库(需确认平台支持)。
|
||||||
|
- `nonce` 长度以 AES-GCM 要求为准(通常 12 字节)。
|
||||||
|
|
||||||
|
## 六、需要后端确认的信息
|
||||||
|
|
||||||
|
- `payload_crypto.secret_key` 的实际值(生产环境)。
|
||||||
|
- 是否强制 `payload_crypto.request.required`(如果强制,所有接口必须加密)。
|
||||||
|
- 哪些接口在白名单(默认 `/swagger/`)。
|
||||||
|
|
||||||
|
## 七、联调检查清单
|
||||||
|
|
||||||
|
- 请求体加密后,后端可以正常解析并返回业务数据。
|
||||||
|
- 响应解密后,数据结构与未加密时一致。
|
||||||
|
- 错误场景:
|
||||||
|
- 缺少 `X-App-Encrypt` 头且后端强制加密时应收到 400。
|
||||||
|
- 非 JSON 响应不应被尝试解密。
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Package common 参数加解密工具
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncryptedPayload 加密载荷结构
|
||||||
|
type EncryptedPayload struct {
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
Ciphertext string `json:"ciphertext"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptPayload 加密响应内容 (AES-GCM + Base64)
|
||||||
|
func EncryptPayload(plaintext []byte, secret string) (EncryptedPayload, error) {
|
||||||
|
key, err := deriveAESKey(secret)
|
||||||
|
if err != nil {
|
||||||
|
return EncryptedPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return EncryptedPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return EncryptedPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return EncryptedPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
||||||
|
return EncryptedPayload{
|
||||||
|
Nonce: base64.StdEncoding.EncodeToString(nonce),
|
||||||
|
Ciphertext: base64.StdEncoding.EncodeToString(ciphertext),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptPayload 解密请求内容 (AES-GCM + Base64)
|
||||||
|
func DecryptPayload(payload EncryptedPayload, secret string) ([]byte, error) {
|
||||||
|
key, err := deriveAESKey(secret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, err := base64.StdEncoding.DecodeString(payload.Nonce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(nonce) != gcm.NonceSize() {
|
||||||
|
return nil, errors.New("invalid nonce size")
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(payload.Ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveAESKey(secret string) ([]byte, error) {
|
||||||
|
secret = strings.TrimSpace(secret)
|
||||||
|
if secret == "" {
|
||||||
|
return nil, errors.New("secret is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if decoded, err := base64.StdEncoding.DecodeString(secret); err == nil {
|
||||||
|
if len(decoded) == 16 || len(decoded) == 24 || len(decoded) == 32 {
|
||||||
|
return decoded, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(secret) == 16 || len(secret) == 24 || len(secret) == 32 {
|
||||||
|
return []byte(secret), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256([]byte(secret))
|
||||||
|
return sum[:], nil
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,19 @@ security:
|
||||||
header_key: X-App-Sign
|
header_key: X-App-Sign
|
||||||
secret_key: yts@2025#secure
|
secret_key: yts@2025#secure
|
||||||
|
|
||||||
|
payload_crypto:
|
||||||
|
enable: true
|
||||||
|
header_key: X-App-Encrypt
|
||||||
|
secret_key: "1"
|
||||||
|
whitelist:
|
||||||
|
- /swagger/
|
||||||
|
request:
|
||||||
|
enable: false
|
||||||
|
required: false
|
||||||
|
response:
|
||||||
|
enable: false
|
||||||
|
required: false
|
||||||
|
|
||||||
rate_limit:
|
rate_limit:
|
||||||
enable: true
|
enable: true
|
||||||
default:
|
default:
|
||||||
|
|
@ -42,9 +55,9 @@ database:
|
||||||
host: 10.13.13.1
|
host: 10.13.13.1
|
||||||
#port: 3306
|
#port: 3306
|
||||||
port: 5432
|
port: 5432
|
||||||
database: fast-common-db
|
database: wz-db
|
||||||
username: user_3W72AM
|
username: wz-db
|
||||||
password: "password_KAwdZW"
|
password: "sYpphaZWYpEEtrS7"
|
||||||
charset: utf8mb4
|
charset: utf8mb4
|
||||||
max_idle_conns: 20
|
max_idle_conns: 20
|
||||||
max_open_conns: 100
|
max_open_conns: 100
|
||||||
|
|
@ -66,6 +79,7 @@ wechat:
|
||||||
app_secret: "ed3fd9089dcfbd1d886eddeca69c07bd"
|
app_secret: "ed3fd9089dcfbd1d886eddeca69c07bd"
|
||||||
|
|
||||||
app_config:
|
app_config:
|
||||||
|
tenantId: 000000
|
||||||
app:
|
app:
|
||||||
min_version: "1.2.0"
|
min_version: "1.2.0"
|
||||||
latest_version: "1.3.5"
|
latest_version: "1.3.5"
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type appConfig struct {
|
||||||
Log LogConfig `yaml:"log"`
|
Log LogConfig `yaml:"log"`
|
||||||
Server ServerConfig `yaml:"server"`
|
Server ServerConfig `yaml:"server"`
|
||||||
Security SecurityConfig `yaml:"security"`
|
Security SecurityConfig `yaml:"security"`
|
||||||
|
PayloadCrypto PayloadCryptoConfig `yaml:"payload_crypto"`
|
||||||
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||||
Swagger SwaggerConfig `yaml:"swagger"`
|
Swagger SwaggerConfig `yaml:"swagger"`
|
||||||
Database DatabaseConfig `yaml:"database"`
|
Database DatabaseConfig `yaml:"database"`
|
||||||
|
|
@ -44,6 +45,22 @@ type SecurityConfig struct {
|
||||||
SecretKey string `yaml:"secret_key"` // 签名密钥
|
SecretKey string `yaml:"secret_key"` // 签名密钥
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PayloadCryptoConfig 请求/响应参数加密配置
|
||||||
|
type PayloadCryptoConfig struct {
|
||||||
|
Enable bool `yaml:"enable"` // 是否启用
|
||||||
|
HeaderKey string `yaml:"header_key"` // 加密标记请求头字段名
|
||||||
|
SecretKey string `yaml:"secret_key"` // 加密密钥
|
||||||
|
Whitelist []string `yaml:"whitelist"` // 白名单路径
|
||||||
|
Request PayloadCryptoDirectionConfig `yaml:"request"` // 请求加密配置
|
||||||
|
Response PayloadCryptoDirectionConfig `yaml:"response"` // 响应加密配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayloadCryptoDirectionConfig 方向配置
|
||||||
|
type PayloadCryptoDirectionConfig struct {
|
||||||
|
Enable bool `yaml:"enable"` // 是否启用
|
||||||
|
Required bool `yaml:"required"` // 是否强制
|
||||||
|
}
|
||||||
|
|
||||||
// RateLimitConfig 限流配置
|
// RateLimitConfig 限流配置
|
||||||
type RateLimitConfig struct {
|
type RateLimitConfig struct {
|
||||||
Enable bool `yaml:"enable"` // 是否启用
|
Enable bool `yaml:"enable"` // 是否启用
|
||||||
|
|
@ -104,6 +121,7 @@ type AppVersionConfig struct {
|
||||||
TTLSeconds int `yaml:"ttl_seconds"`
|
TTLSeconds int `yaml:"ttl_seconds"`
|
||||||
Disabled bool `yaml:"disabled"`
|
Disabled bool `yaml:"disabled"`
|
||||||
DisableReason string `yaml:"disable_reason"`
|
DisableReason string `yaml:"disable_reason"`
|
||||||
|
TenantId string `yaml:"tenantId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppClientConfig 客户端版本配置
|
// AppClientConfig 客户端版本配置
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@ security:
|
||||||
header_key: X-App-Sign
|
header_key: X-App-Sign
|
||||||
secret_key: yts@2025#secure
|
secret_key: yts@2025#secure
|
||||||
|
|
||||||
|
payload_crypto:
|
||||||
|
enable: false
|
||||||
|
header_key: X-App-Encrypt
|
||||||
|
secret_key: ""
|
||||||
|
whitelist:
|
||||||
|
- /swagger/
|
||||||
|
request:
|
||||||
|
enable: false
|
||||||
|
required: false
|
||||||
|
response:
|
||||||
|
enable: false
|
||||||
|
required: false
|
||||||
|
|
||||||
rate_limit:
|
rate_limit:
|
||||||
enable: true
|
enable: true
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@ security:
|
||||||
header_key: X-App-Sign
|
header_key: X-App-Sign
|
||||||
secret_key: yts@2025#secure
|
secret_key: yts@2025#secure
|
||||||
|
|
||||||
|
payload_crypto:
|
||||||
|
enable: false
|
||||||
|
header_key: X-App-Encrypt
|
||||||
|
secret_key: ""
|
||||||
|
whitelist:
|
||||||
|
- /swagger/
|
||||||
|
request:
|
||||||
|
enable: false
|
||||||
|
required: false
|
||||||
|
response:
|
||||||
|
enable: false
|
||||||
|
required: false
|
||||||
|
|
||||||
rate_limit:
|
rate_limit:
|
||||||
enable: true
|
enable: true
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,8 @@ func main() {
|
||||||
// API 路由组
|
// API 路由组
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
|
|
||||||
// 中间件顺序: 安全校验 -> 限流 -> 登录鉴权
|
// 中间件顺序: 参数加解密 -> 安全校验 -> 限流 -> 登录鉴权
|
||||||
|
api.Use(middleware.PayloadCryptoMiddleware())
|
||||||
api.Use(middleware.SecurityMiddleware())
|
api.Use(middleware.SecurityMiddleware())
|
||||||
api.Use(middleware.RateLimitMiddleware())
|
api.Use(middleware.RateLimitMiddleware())
|
||||||
api.Use(middleware.AuthMiddleware())
|
api.Use(middleware.AuthMiddleware())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
// Package middleware 参数加解密中间件
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"server/common"
|
||||||
|
"server/config"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PayloadCryptoMiddleware 请求/响应参数加解密中间件
|
||||||
|
func PayloadCryptoMiddleware() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
cfg := config.AppConfig.PayloadCrypto
|
||||||
|
if !cfg.Enable {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if isPayloadCryptoWhitelist(path, cfg.Whitelist) {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(cfg.SecretKey) == "" {
|
||||||
|
common.Warn("参数加密开启但secret_key为空,已跳过 Path=%s", path)
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Request.Enable {
|
||||||
|
if err := maybeDecryptRequest(c, cfg); err != nil {
|
||||||
|
common.Warn("请求解密失败: %v Path=%s", err, path)
|
||||||
|
common.Error(c, http.StatusBadRequest, "请求解密失败")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.Response.Enable {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
originWriter := c.Writer
|
||||||
|
writer := newCryptoResponseWriter(originWriter)
|
||||||
|
c.Writer = writer
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
|
||||||
|
c.Writer = originWriter
|
||||||
|
if err := writeEncryptedResponse(c, writer, cfg); err != nil {
|
||||||
|
common.Warn("响应加密失败: %v Path=%s", err, path)
|
||||||
|
writer.writePlain(originWriter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeDecryptRequest(c *gin.Context, cfg config.PayloadCryptoConfig) error {
|
||||||
|
headerVal := strings.TrimSpace(c.GetHeader(cfg.HeaderKey))
|
||||||
|
needDecrypt := headerVal != "" && headerVal != "0" && strings.ToLower(headerVal) != "false"
|
||||||
|
if cfg.Request.Required && !needDecrypt {
|
||||||
|
return errRequiredEncrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needDecrypt {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(c.Request.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(bodyBytes) == 0 {
|
||||||
|
return errEmptyBody
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload common.EncryptedPayload
|
||||||
|
if err := json.Unmarshal(bodyBytes, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err := common.DecryptPayload(payload, cfg.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Request.Body = io.NopCloser(bytes.NewReader(plaintext))
|
||||||
|
c.Request.ContentLength = int64(len(plaintext))
|
||||||
|
if c.Request.Header.Get("Content-Type") == "" {
|
||||||
|
c.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeEncryptedResponse(c *gin.Context, writer *cryptoResponseWriter, cfg config.PayloadCryptoConfig) error {
|
||||||
|
status := writer.Status()
|
||||||
|
if status == http.StatusNoContent || status == http.StatusNotModified {
|
||||||
|
writer.writePlain(c.Writer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := c.Writer.Header().Get("Content-Type")
|
||||||
|
if contentType != "" && !strings.HasPrefix(contentType, "application/json") {
|
||||||
|
writer.writePlain(c.Writer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if writer.body.Len() == 0 {
|
||||||
|
writer.writePlain(c.Writer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := common.EncryptPayload(writer.body.Bytes(), cfg.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Writer.Header().Set(cfg.HeaderKey, "1")
|
||||||
|
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
c.Writer.Header().Del("Content-Length")
|
||||||
|
c.Writer.WriteHeader(status)
|
||||||
|
_, err = c.Writer.Write(respBytes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPayloadCryptoWhitelist(path string, whitelist []string) bool {
|
||||||
|
for _, white := range whitelist {
|
||||||
|
if len(path) >= len(white) && path[:len(white)] == white {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type cryptoResponseWriter struct {
|
||||||
|
gin.ResponseWriter
|
||||||
|
body *bytes.Buffer
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCryptoResponseWriter(w gin.ResponseWriter) *cryptoResponseWriter {
|
||||||
|
return &cryptoResponseWriter{
|
||||||
|
ResponseWriter: w,
|
||||||
|
body: &bytes.Buffer{},
|
||||||
|
status: http.StatusOK,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) WriteHeader(code int) {
|
||||||
|
if code > 0 {
|
||||||
|
w.status = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) WriteHeaderNow() {
|
||||||
|
if w.status == 0 {
|
||||||
|
w.status = http.StatusOK
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
return w.body.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) WriteString(s string) (int, error) {
|
||||||
|
return w.body.WriteString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) Status() int {
|
||||||
|
if w.status == 0 {
|
||||||
|
return http.StatusOK
|
||||||
|
}
|
||||||
|
return w.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) Size() int {
|
||||||
|
return w.body.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) Written() bool {
|
||||||
|
return w.body.Len() > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) Flush() {}
|
||||||
|
|
||||||
|
func (w *cryptoResponseWriter) writePlain(origin gin.ResponseWriter) {
|
||||||
|
origin.Header().Del("Content-Length")
|
||||||
|
origin.WriteHeader(w.Status())
|
||||||
|
if w.body.Len() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = origin.Write(w.body.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errRequiredEncrypted = errors.New("request body must be encrypted")
|
||||||
|
errEmptyBody = errors.New("request body is empty")
|
||||||
|
)
|
||||||
|
|
@ -99,7 +99,7 @@ func (s *WechatMiniProgramService) Login(req *apiDto.WechatMiniLoginRequest) (*a
|
||||||
Phone: phonePtr,
|
Phone: phonePtr,
|
||||||
Gender: 0,
|
Gender: 0,
|
||||||
Status: 1,
|
Status: 1,
|
||||||
Deleted: 0,
|
DelFlag: 0,
|
||||||
}
|
}
|
||||||
if phone != "" {
|
if phone != "" {
|
||||||
salt := uuid.NewString()[:8]
|
salt := uuid.NewString()[:8]
|
||||||
|
|
@ -125,7 +125,7 @@ func (s *WechatMiniProgramService) Login(req *apiDto.WechatMiniLoginRequest) (*a
|
||||||
PlatformSessionKey: session.SessionKey,
|
PlatformSessionKey: session.SessionKey,
|
||||||
PlatformExtra: req.PlatformExtra,
|
PlatformExtra: req.PlatformExtra,
|
||||||
LastLoginTime: &now,
|
LastLoginTime: &now,
|
||||||
Deleted: 0,
|
DelFlag: 0,
|
||||||
}
|
}
|
||||||
if err := s.platformUserMapper.Create(platform); err != nil {
|
if err := s.platformUserMapper.Create(platform); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ type PlatformUser struct {
|
||||||
LastLoginTime *time.Time `gorm:"column:last_login_time;comment:最后登录时间" json:"lastLoginTime"`
|
LastLoginTime *time.Time `gorm:"column:last_login_time;comment:最后登录时间" json:"lastLoginTime"`
|
||||||
CreateTime time.Time `gorm:"column:create_time;autoCreateTime;comment:创建时间" json:"createTime"`
|
CreateTime time.Time `gorm:"column:create_time;autoCreateTime;comment:创建时间" json:"createTime"`
|
||||||
UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime;comment:更新时间" json:"updateTime"`
|
UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime;comment:更新时间" json:"updateTime"`
|
||||||
Deleted int8 `gorm:"column:deleted;comment:软删除:0-未删,1-已删" json:"deleted"`
|
DelFlag int8 `gorm:"column:del_flag;comment:软删除:0-未删,1-已删" json:"delFlag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ type User struct {
|
||||||
Status int8 `gorm:"column:status;comment:状态:0-禁用,1-正常" json:"status"`
|
Status int8 `gorm:"column:status;comment:状态:0-禁用,1-正常" json:"status"`
|
||||||
CreateTime time.Time `gorm:"column:create_time;autoCreateTime;comment:创建时间" json:"createTime"`
|
CreateTime time.Time `gorm:"column:create_time;autoCreateTime;comment:创建时间" json:"createTime"`
|
||||||
UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime;comment:更新时间" json:"updateTime"`
|
UpdateTime time.Time `gorm:"column:update_time;autoUpdateTime;comment:更新时间" json:"updateTime"`
|
||||||
Deleted int8 `gorm:"column:deleted;comment:软删除:0-未删,1-已删" json:"deleted"`
|
DelFlag int8 `gorm:"column:del_flag;comment:软删除:0-未删,1-已删" json:"delFlag"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TableName 指定表名
|
// TableName 指定表名
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ func (m *PlatformUserMapper) baseDB() *gorm.DB {
|
||||||
|
|
||||||
// GetDB 获取数据库实例,默认过滤软删除
|
// GetDB 获取数据库实例,默认过滤软删除
|
||||||
func (m *PlatformUserMapper) GetDB() *gorm.DB {
|
func (m *PlatformUserMapper) GetDB() *gorm.DB {
|
||||||
return m.baseDB().Where("deleted = 0")
|
return m.baseDB().Where("delFlag = 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindAll 分页查询
|
// FindAll 分页查询
|
||||||
|
|
@ -73,5 +73,5 @@ func (m *PlatformUserMapper) UpdateFields(id int64, fields map[string]interface{
|
||||||
|
|
||||||
// Delete 逻辑删除
|
// Delete 逻辑删除
|
||||||
func (m *PlatformUserMapper) Delete(id int64) error {
|
func (m *PlatformUserMapper) Delete(id int64) error {
|
||||||
return m.baseDB().Model(&entity.PlatformUser{}).Where("id = ?", id).Update("deleted", 1).Error
|
return m.baseDB().Model(&entity.PlatformUser{}).Where("id = ?", id).Update("delFlag", 1).Error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ func (m *UserMapper) baseDB() *gorm.DB {
|
||||||
|
|
||||||
// GetDB 获取数据库实例,默认过滤软删除
|
// GetDB 获取数据库实例,默认过滤软删除
|
||||||
func (m *UserMapper) GetDB() *gorm.DB {
|
func (m *UserMapper) GetDB() *gorm.DB {
|
||||||
return m.baseDB().Where("deleted = 0")
|
return m.baseDB().Where("delFlag = 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindAll 分页查询
|
// FindAll 分页查询
|
||||||
|
|
@ -66,5 +66,5 @@ func (m *UserMapper) UpdateFields(id int64, fields map[string]interface{}) error
|
||||||
|
|
||||||
// Delete 逻辑删除
|
// Delete 逻辑删除
|
||||||
func (m *UserMapper) Delete(id int64) error {
|
func (m *UserMapper) Delete(id int64) error {
|
||||||
return m.baseDB().Model(&entity.User{}).Where("id = ?", id).Update("deleted", 1).Error
|
return m.baseDB().Model(&entity.User{}).Where("id = ?", id).Update("delFlag", 1).Error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func (s *PlatformUserService) Create(req *dto.CreatePlatformUserRequest) (*entit
|
||||||
PlatformSessionKey: req.PlatformSessionKey,
|
PlatformSessionKey: req.PlatformSessionKey,
|
||||||
PlatformExtra: req.PlatformExtra,
|
PlatformExtra: req.PlatformExtra,
|
||||||
LastLoginTime: req.LastLoginTime,
|
LastLoginTime: req.LastLoginTime,
|
||||||
Deleted: 0,
|
DelFlag: 0,
|
||||||
}
|
}
|
||||||
if err := s.mapper.Create(item); err != nil {
|
if err := s.mapper.Create(item); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ func (s *UserService) Create(req *dto.CreateUserRequest) (*entity.User, error) {
|
||||||
Phone: phone,
|
Phone: phone,
|
||||||
Gender: 0,
|
Gender: 0,
|
||||||
Status: 1,
|
Status: 1,
|
||||||
Deleted: 0,
|
DelFlag: 0,
|
||||||
}
|
}
|
||||||
if req.Gender != nil {
|
if req.Gender != nil {
|
||||||
user.Gender = *req.Gender
|
user.Gender = *req.Gender
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 任务执行摘要
|
||||||
|
|
||||||
|
## 会话 ID: codex
|
||||||
|
- [2026-03-16 16:48:26]
|
||||||
|
- **执行原因**: 用户询问是否存在接口返回/请求参数加密配置。
|
||||||
|
- **执行过程**: 1. 检索仓库内与加密/签名相关的配置与实现。 2. 查看安全校验中间件与配置文件,核对是否有参数加密配置。
|
||||||
|
- **执行结果**: 未发现通用的请求/响应参数加密配置,仅存在签名校验配置及业务内的密码/微信数据加解密实现。
|
||||||
|
|
||||||
|
# 任务执行摘要
|
||||||
|
|
||||||
|
## 会话 ID: codex
|
||||||
|
- [2026-03-16 17:08:33]
|
||||||
|
- **执行原因**: 用户请求补充请求/响应参数加密的配置与控制能力。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 新增 `payload_crypto` 配置结构与开发/测试/生产配置示例。
|
||||||
|
2. 增加 AES-GCM 加解密工具与参数加解密中间件,并接入路由中间件链。
|
||||||
|
- **执行结果**: 已支持通过配置开关控制请求解密与响应加密(AES-GCM),并提供白名单与加密标记请求头。
|
||||||
|
|
||||||
|
# 任务执行摘要
|
||||||
|
|
||||||
|
## 会话 ID: codex
|
||||||
|
- [2026-03-16 17:13:59]
|
||||||
|
- **执行原因**: 用户要求输出前端对接参数加密的工作清单文档。
|
||||||
|
- **执行过程**:
|
||||||
|
1. 编写前端对接说明与任务清单(加密协议、触发规则、实现建议)。
|
||||||
|
2. 保存为独立 Markdown 文档供前端工程师使用。
|
||||||
|
- **执行结果**: 已生成 `frontend_payload_crypto_tasks.md`。
|
||||||
|
|
||||||
Loading…
Reference in New Issue