feat: update template
This commit is contained in:
58
internal/middleware/access_log.go
Normal file
58
internal/middleware/access_log.go
Normal 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,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
57
internal/middleware/context.go
Normal file
57
internal/middleware/context.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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...)
|
||||
|
||||
53
internal/middleware/request_context.go
Normal file
53
internal/middleware/request_context.go
Normal 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))
|
||||
})
|
||||
}
|
||||
47
internal/middleware/response_writer.go
Normal file
47
internal/middleware/response_writer.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user