feat: update template

This commit is contained in:
2026-08-19 22:05:49 +08:00
parent 0e674e9d56
commit 5a71093b0c
67 changed files with 2070 additions and 585 deletions

View File

@@ -0,0 +1,58 @@
package middleware
import (
"context"
"net/http"
"server/internal/db"
"server/internal/db/sqlc"
"server/internal/pkg/safego"
"time"
)
type AccessLogMiddleware struct {
store *db.Store
}
func NewAccessLogMiddleware(store *db.Store) *AccessLogMiddleware {
return &AccessLogMiddleware{
store: store,
}
}
func (m *AccessLogMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqCtx := GetRequestContext(r.Context())
userCtx := GetUserContext(r.Context())
rw := NewResponseRecorder(w)
next.ServeHTTP(rw, r)
// 写入数据库时是null
userID := &userCtx.UserID
if userCtx.UserID == 0 {
userID = nil
}
durationMs := int32(time.Since(reqCtx.StartTime).Milliseconds())
statusCode := int32(rw.statusCode)
// 异步写入
safego.Go(func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_ = m.store.Queries.CreateAccessLog(ctx, sqlc.CreateAccessLogParams{
RequestID: &reqCtx.RequestID,
UserID: userID,
Ip: &reqCtx.ClientIp,
UserAgent: &reqCtx.UserAgent,
RequestMethod: reqCtx.Method,
RequestPath: reqCtx.Path,
Message: &rw.errorMsg,
Referer: &reqCtx.Referer,
StatusCode: &statusCode,
ResponseTimeMs: &durationMs,
StartedAt: reqCtx.StartTime,
})
})
})
}

View File

@@ -2,47 +2,35 @@ package middleware
import (
"context"
"errors"
"net/http"
"server/internal/config"
"server/internal/db"
"server/internal/db/sqlc"
"server/internal/model/auth"
"server/internal/pkg/cache"
"server/internal/pkg/cache/cachekey"
"server/internal/pkg/errs"
"server/internal/pkg/httputil"
"strings"
"github.com/go-chi/chi/v5"
)
const (
IsAdminKey contextKey = "is_admin"
"github.com/jackc/pgx/v5"
)
type AuthMiddleware struct {
store *db.Store
cache *cache.Caches
cfg *config.Config
}
func NewAuthMiddleware(store *db.Store, cache *cache.Caches) *AuthMiddleware {
func NewAuthMiddleware(store *db.Store, cache *cache.Caches, cfg *config.Config) *AuthMiddleware {
return &AuthMiddleware{
cache: cache,
store: store,
cfg: cfg,
}
}
func UserIsAdmin(uid int32) bool {
if uid == 1 {
return true
}
return false
}
func IsAdmin(ctx context.Context) bool {
isAdmin, ok := ctx.Value(IsAdminKey).(bool)
return ok && isAdmin
}
func (m *AuthMiddleware) hasApiPermission(ctx context.Context, uid int32, requestMethod string, requestPath string) (bool, error) {
var (
apis []sqlc.ListUserApisRow
@@ -72,22 +60,49 @@ func (m *AuthMiddleware) hasApiPermission(ctx context.Context, uid int32, reques
return false, nil
}
func (m *AuthMiddleware) getAuthState(ctx context.Context, id int32) (*sqlc.GetUserAuthStateRow, error) {
return cache.GetOrSetJSON[*sqlc.GetUserAuthStateRow](ctx, m.cache, cachekey.UserAuthState(id), m.cfg.JWTConfig.Expire, func() (*sqlc.GetUserAuthStateRow, error) {
state, err := m.store.GetUserAuthState(ctx, id)
if err != nil {
return nil, err
}
return &state, nil
})
}
func (m *AuthMiddleware) Middleware(router chi.Router) func(handler http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, ok := GetClaims(ctx)
if !ok || claims.UserID == 0 {
userCtx := GetUserContext(ctx)
if userCtx == nil || userCtx.UserID == 0 {
httputil.Fail(w, errs.ErrUnauthorized)
return
}
// 判断token version
state, err := m.getAuthState(ctx, userCtx.UserID)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
httputil.Fail(w, errs.ErrInvalidToken) // 用户被删
return
}
httputil.Fail(w, errs.ErrInternalServer) // 基础设施故障,不杀会话
return
}
if state.TokenVersion != userCtx.TokenVersion {
httputil.Fail(w, errs.ErrInvalidToken) // 被踢 → 401
return
}
// 判断是否有管理员权限
isAdmin := UserIsAdmin(claims.UserID)
isAdmin := auth.IsAdmin(userCtx.UserID)
userCtx.IsAdmin = isAdmin
if isAdmin {
ctx = context.WithValue(ctx, IsAdminKey, isAdmin)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
@@ -97,9 +112,8 @@ func (m *AuthMiddleware) Middleware(router chi.Router) func(handler http.Handler
requestMethod := r.Method
// https://github.com/go-chi/chi/pull/872
api := router.Find(rctx, requestMethod, r.URL.Path)
requestPath := strings.TrimPrefix(api, "/api")
hasPermission, err := m.hasApiPermission(ctx, claims.UserID, requestMethod, requestPath)
hasPermission, err := m.hasApiPermission(ctx, userCtx.UserID, requestMethod, api)
if err != nil {
httputil.Fail(w, err)
return

View File

@@ -0,0 +1,57 @@
package middleware
import (
"context"
"net/netip"
"time"
)
type requestContextKey struct{}
type RequestContext struct {
RequestID string
StartTime time.Time
Method string
Path string
ClientIp netip.Addr
UserAgent string
Referer string
}
func WithRequestContext(ctx context.Context, req *RequestContext) context.Context {
return context.WithValue(ctx, requestContextKey{}, req)
}
func GetRequestContext(ctx context.Context) *RequestContext {
v, ok := ctx.Value(requestContextKey{}).(*RequestContext)
if !ok {
return nil
}
return v
}
// ----------------------------------------------------------------------------------------
type userContextKey struct{}
type UserContext struct {
UserID int32
IsAdmin bool
TokenVersion int32
}
func WithUserContext(ctx context.Context, user *UserContext) context.Context {
return context.WithValue(ctx, userContextKey{}, user)
}
func GetUserContext(ctx context.Context) *UserContext {
v, ok := ctx.Value(userContextKey{}).(*UserContext)
if !ok {
return nil
}
return v
}

View File

@@ -1,10 +1,10 @@
package middleware
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"server/internal/config"
"server/internal/pkg/errs"
@@ -19,32 +19,42 @@ type JWTMiddleware struct {
cfg *config.JWTConfig
}
type contextKey string
const UserContextKey contextKey = "user"
const RefreshTokenType = "refresh"
type Claims struct {
UserID int32 `json:"user_id"`
UserID int32 `json:"user_id"`
TokenVersion int32 `json:"token_version"`
jwt.RegisteredClaims
}
type RefreshClaims struct {
UserID int32 `json:"user_id"`
Type string `json:"type"`
jwt.RegisteredClaims
}
func NewJWTMiddleware(cfg *config.Config) *JWTMiddleware {
func NewJWTMiddleware(cfg *config.Config) (*JWTMiddleware, error) {
jwtCfg := cfg.JWTConfig
return &JWTMiddleware{cfg: &jwtCfg}
// 不允许空密钥
if jwtCfg.Secret == "" {
return nil, fmt.Errorf("jwt.secret 未配置:请在 %s.yaml 中设置 jwt.secret", config.GetEnv())
}
m := &JWTMiddleware{cfg: &jwtCfg}
// 启动时校验签名算法配置
if _, err := m.signingMethod(); err != nil {
return nil, err
}
return m, nil
}
func GetClaims(ctx context.Context) (*Claims, bool) {
claims, ok := ctx.Value(UserContextKey).(*Claims)
return claims, ok
// signingMethod 返回配置的签名算法(仅支持 HMAC 家族)
func (m *JWTMiddleware) signingMethod() (jwt.SigningMethod, error) {
switch strings.ToUpper(m.cfg.SigningMethod) {
case "", "HS256":
return jwt.SigningMethodHS256, nil
case "HS384":
return jwt.SigningMethodHS384, nil
case "HS512":
return jwt.SigningMethodHS512, nil
default:
return nil, fmt.Errorf("不支持的 jwt.signing_method: %q仅支持 HS256/HS384/HS512", m.cfg.SigningMethod)
}
}
// ParseToken 解析accessToken
@@ -52,7 +62,7 @@ func (m *JWTMiddleware) ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(
tokenStr,
&Claims{},
func(token *jwt.Token) (interface{}, error) {
func(token *jwt.Token) (any, error) {
return []byte(m.cfg.Secret), nil
},
)
@@ -96,25 +106,34 @@ func (m *JWTMiddleware) Middleware(next http.Handler) http.Handler {
return
}
ctx := context.WithValue(r.Context(), UserContextKey, claims)
userCtx := GetUserContext(r.Context())
userCtx.UserID = claims.UserID
userCtx.TokenVersion = claims.TokenVersion
next.ServeHTTP(w, r.WithContext(ctx))
next.ServeHTTP(w, r)
})
}
func (m *JWTMiddleware) GenerateAccessToken(userID int32) (string, time.Time, error) {
func (m *JWTMiddleware) GenerateAccessToken(userID int32, tokenVersion int32) (string, time.Time, error) {
method, err := m.signingMethod()
if err != nil {
return "", time.Time{}, err
}
now := time.Now()
expiresAt := now.Add(m.cfg.Expire)
accessClaims := Claims{
UserID: userID,
UserID: userID,
TokenVersion: tokenVersion,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(now),
},
}
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
// 使用配置的签名算法,与 ParseToken 的校验保持一致
accessToken := jwt.NewWithClaims(method, accessClaims)
token, err := accessToken.SignedString([]byte(m.cfg.Secret))
if err != nil {
return "", time.Time{}, err

View File

@@ -1,100 +1,44 @@
package middleware
import (
"encoding/json"
"log/slog"
"net/http"
"server/internal/model/common"
"server/internal/pkg/httputil"
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
)
type LoggerMiddleware struct {
}
type responseWriter struct {
http.ResponseWriter
statusCode int
bytes int
errorMsg string
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func generateRequestID() string {
id, _ := gonanoid.New(16) // 16字符
return id
}
// extractErrorMessage 从响应体中提取错误信息
func (rw *responseWriter) extractErrorMessage(body []byte) string {
var resp common.Response
if err := json.Unmarshal(body, &resp); err == nil && resp.Message != "" {
return resp.Message
}
return string(body)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytes += n
// 只在错误状态码且未记录错误时处理
if rw.statusCode >= 400 && rw.errorMsg == "" && n > 0 {
rw.errorMsg = rw.extractErrorMessage(b[:n])
}
return n, err
}
func NewLoggerMiddleware() *LoggerMiddleware {
return &LoggerMiddleware{}
}
func (m *LoggerMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
reqCtx := GetRequestContext(r.Context())
reqID := generateRequestID()
rw := NewResponseRecorder(w)
wrapped := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next.ServeHTTP(wrapped, r)
duration := time.Since(start)
ip := httputil.ClientIP(r)
fullPath := r.URL.Path
if r.URL.RawQuery != "" {
fullPath = fullPath + "?" + r.URL.RawQuery
}
next.ServeHTTP(rw, r)
fields := []any{
"request_id", reqID,
"method", r.Method,
"path", fullPath,
"status", wrapped.statusCode,
"duration", duration,
"client_ip", ip,
"request_id", reqCtx.RequestID,
"method", reqCtx.Method,
"path", reqCtx.Path,
"status", rw.statusCode,
"start_time", reqCtx.StartTime,
"duration", time.Since(reqCtx.StartTime),
"client_ip", reqCtx.ClientIp,
}
if wrapped.errorMsg != "" {
fields = append(fields, "error", wrapped.errorMsg)
if rw.errorMsg != "" {
fields = append(fields, "error", rw.errorMsg)
}
switch {
case wrapped.statusCode >= 500:
case rw.statusCode >= 500:
slog.Error("request", fields...)
case wrapped.statusCode >= 400:
case rw.statusCode >= 400:
slog.Warn("request", fields...)
default:
slog.Info("request", fields...)

View File

@@ -0,0 +1,53 @@
package middleware
import (
"net/http"
"server/internal/pkg/httputil"
"time"
gonanoid "github.com/matoous/go-nanoid/v2"
)
func generateRequestID() string {
id, _ := gonanoid.New(16) // 16字符
return id
}
type RequestContextMiddleware struct {
}
func NewRequestContextMiddleware() *RequestContextMiddleware {
return &RequestContextMiddleware{}
}
func (m *RequestContextMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqCtx := &RequestContext{
RequestID: generateRequestID(),
StartTime: time.Now(),
Method: r.Method,
Path: r.URL.RequestURI(),
ClientIp: httputil.ClientIP(r),
UserAgent: r.UserAgent(),
Referer: r.Referer(),
}
userCtx := &UserContext{
UserID: 0,
IsAdmin: false,
TokenVersion: 0,
}
ctx := WithRequestContext(
r.Context(),
reqCtx,
)
ctx = WithUserContext(
ctx,
userCtx,
)
next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -0,0 +1,47 @@
package middleware
import (
"encoding/json"
"net/http"
"server/internal/model/common"
)
type ResponseWriter struct {
http.ResponseWriter
statusCode int
bytes int
errorMsg string
}
func NewResponseRecorder(w http.ResponseWriter) *ResponseWriter {
return &ResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (rw *ResponseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// extractErrorMessage 从响应体中提取错误信息
func (rw *ResponseWriter) extractErrorMessage(body []byte) string {
var resp common.Response
if err := json.Unmarshal(body, &resp); err == nil && resp.Message != "" {
return resp.Message
}
return string(body)
}
func (rw *ResponseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytes += n
// 只在错误状态码且未记录错误时处理
if rw.statusCode >= 400 && rw.errorMsg == "" && n > 0 {
rw.errorMsg = rw.extractErrorMessage(b[:n])
}
return n, err
}