feat: 引入签名认证、加密工具包及大量goctl代码生成模板,并更新API、Admin和Node服务逻辑。

This commit is contained in:
2026-02-27 08:49:37 -08:00
parent 9dacd85a89
commit e1896f677c
166 changed files with 2913 additions and 489 deletions
@@ -0,0 +1,158 @@
package middleware
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/config"
"github.com/zero-ppanel/zero-ppanel/pkg/cryptox"
"github.com/zero-ppanel/zero-ppanel/pkg/xerr"
"github.com/zeromicro/go-zero/rest/httpx"
)
type DecryptMiddleware struct {
conf config.Config
}
func NewDecryptMiddleware(c config.Config) *DecryptMiddleware {
return &DecryptMiddleware{conf: c}
}
func (m *DecryptMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !m.conf.Security.Enable {
next(w, r)
return
}
if r.Header.Get("Login-Type") != "device" {
next(w, r)
return
}
secret := m.conf.Security.SecuritySecret
rw := newEncryptResponseWriter(w, secret)
// 解密 GET query
query := r.URL.Query()
dataStr := query.Get("data")
timeStr := query.Get("time")
if dataStr != "" && timeStr != "" {
if plain, err := cryptox.Decrypt(dataStr, secret, timeStr); err == nil {
params := map[string]interface{}{}
if json.Unmarshal(plain, &params) == nil {
for k, v := range params {
query.Set(k, fmt.Sprintf("%v", v))
}
query.Del("data")
query.Del("time")
rawQuery := query.Encode()
if strings.Contains(r.RequestURI, "?") {
r.RequestURI = r.RequestURI[:strings.Index(r.RequestURI, "?")] + "?" + rawQuery
}
r.URL.RawQuery = rawQuery
}
}
}
// 解密 POST body
if r.Body != nil {
body, err := io.ReadAll(r.Body)
if err != nil || len(body) == 0 {
// body 为空或读取失败,直接放行
r.Body = io.NopCloser(bytes.NewBuffer(body))
next(rw, r)
rw.flush()
return
}
var envelope struct {
Data string `json:"data"`
Time string `json:"time"`
}
if err := json.Unmarshal(body, &envelope); err != nil || envelope.Data == "" {
httpx.Error(w, xerr.NewErrCode(xerr.DecryptFailed))
return
}
plain, err := cryptox.Decrypt(envelope.Data, secret, envelope.Time)
if err != nil {
httpx.Error(w, xerr.NewErrCode(xerr.DecryptFailed))
return
}
r.Body = io.NopCloser(bytes.NewBuffer(plain))
}
next(rw, r)
rw.flush()
}
}
// encryptResponseWriter 拦截响应,加密 data 字段
type encryptResponseWriter struct {
http.ResponseWriter
body *bytes.Buffer
secret string
status int
}
func newEncryptResponseWriter(w http.ResponseWriter, secret string) *encryptResponseWriter {
return &encryptResponseWriter{
ResponseWriter: w,
body: new(bytes.Buffer),
secret: secret,
status: http.StatusOK,
}
}
func (rw *encryptResponseWriter) WriteHeader(code int) {
rw.status = code
}
func (rw *encryptResponseWriter) Write(data []byte) (int, error) {
return rw.body.Write(data)
}
func (rw *encryptResponseWriter) WriteString(s string) (int, error) {
return rw.body.WriteString(s)
}
func (rw *encryptResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return rw.ResponseWriter.(http.Hijacker).Hijack()
}
func (rw *encryptResponseWriter) flush() {
buf := rw.body.Bytes()
out := buf
// 尝试加密 data 字段
params := map[string]interface{}{}
if err := json.Unmarshal(buf, &params); err == nil {
if data := params["data"]; data != nil {
var jsonData []byte
if str, ok := data.(string); ok {
jsonData = []byte(str)
} else {
jsonData, _ = json.Marshal(data)
}
if dataB64, nonce, err := cryptox.Encrypt(jsonData, rw.secret); err == nil {
params["data"] = map[string]interface{}{
"data": dataB64,
"time": nonce,
}
if enc, err := json.Marshal(params); err == nil {
out = enc
}
}
}
}
rw.ResponseWriter.WriteHeader(rw.status)
rw.ResponseWriter.Write(out)
}
@@ -0,0 +1,83 @@
package middleware
import (
"bytes"
"io"
"net/http"
"strings"
"github.com/zero-ppanel/zero-ppanel/apps/api/internal/config"
"github.com/zero-ppanel/zero-ppanel/pkg/signature"
"github.com/zero-ppanel/zero-ppanel/pkg/xerr"
"github.com/zeromicro/go-zero/rest/httpx"
)
type SignatureMiddleware struct {
conf config.Config
validator *signature.Validator
}
func NewSignatureMiddleware(c config.Config, store signature.NonceStore) *SignatureMiddleware {
return &SignatureMiddleware{
conf: c,
validator: signature.NewValidator(c.AppSignature, store),
}
}
func (m *SignatureMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
appId := r.Header.Get("X-App-Id")
// X-App-Id 为空,提示非法访问
if appId == "" {
httpx.Error(w, xerr.NewErrCode(xerr.InvalidAccess))
return
}
// SkipPrefixes 白名单
for _, prefix := range m.conf.AppSignature.SkipPrefixes {
if strings.HasPrefix(r.URL.Path, prefix) {
next(w, r)
return
}
}
timestamp := r.Header.Get("X-Timestamp")
nonce := r.Header.Get("X-Nonce")
sig := r.Header.Get("X-Signature")
if timestamp == "" || nonce == "" || sig == "" {
httpx.Error(w, xerr.NewErrCode(xerr.SignatureMissing))
return
}
// 读取 body(签名对原始 body bytes 计算)
var bodyBytes []byte
if r.Body != nil {
bodyBytes, _ = io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
}
sts := signature.BuildStringToSign(r.Method, r.URL.Path, r.URL.RawQuery, bodyBytes, appId, timestamp, nonce)
if err := m.validator.Validate(r.Context(), appId, timestamp, nonce, sig, sts); err != nil {
code := mapSignatureErr(err)
httpx.Error(w, xerr.NewErrCode(code))
return
}
next(w, r)
}
}
func mapSignatureErr(err error) int {
switch err {
case signature.ErrSignatureMissing:
return xerr.SignatureMissing
case signature.ErrSignatureExpired:
return xerr.SignatureExpired
case signature.ErrSignatureReplay:
return xerr.SignatureReplay
default:
return xerr.SignatureInvalid
}
}