feat(admin): 运营后台(登录/查看/审核编辑/补全)+ 写入API + 审计留痕
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
// Package auth provides minimal single-account authentication for the admin
|
||||
// console: a bcrypt-verified login and a stdlib HMAC-SHA256 signed token
|
||||
// (JWT-compatible) plus a chi middleware that guards write routes.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Authenticator holds the single admin credential and token signing secret.
|
||||
type Authenticator struct {
|
||||
username string
|
||||
passwordHash []byte
|
||||
secret []byte
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// New builds an Authenticator. passwordHash must be a bcrypt hash.
|
||||
func New(username string, passwordHash, secret []byte, ttl time.Duration) *Authenticator {
|
||||
return &Authenticator{username: username, passwordHash: passwordHash, secret: secret, ttl: ttl}
|
||||
}
|
||||
|
||||
// ErrInvalidCredentials is returned when login fails.
|
||||
var ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
|
||||
// Login verifies the username/password and returns a signed token on success.
|
||||
func (a *Authenticator) Login(username, password string) (string, error) {
|
||||
if username != a.username {
|
||||
// Still run bcrypt to keep timing roughly constant.
|
||||
_ = bcrypt.CompareHashAndPassword(a.passwordHash, []byte(password))
|
||||
return "", ErrInvalidCredentials
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword(a.passwordHash, []byte(password)); err != nil {
|
||||
return "", ErrInvalidCredentials
|
||||
}
|
||||
return a.issue(username)
|
||||
}
|
||||
|
||||
type claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Exp int64 `json:"exp"`
|
||||
}
|
||||
|
||||
func b64(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
|
||||
|
||||
func (a *Authenticator) sign(signingInput string) string {
|
||||
mac := hmac.New(sha256.New, a.secret)
|
||||
mac.Write([]byte(signingInput))
|
||||
return b64(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func (a *Authenticator) issue(sub string) (string, error) {
|
||||
header := b64([]byte(`{"alg":"HS256","typ":"JWT"}`))
|
||||
payloadJSON, err := json.Marshal(claims{Sub: sub, Exp: time.Now().Add(a.ttl).Unix()})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
payload := b64(payloadJSON)
|
||||
signingInput := header + "." + payload
|
||||
return signingInput + "." + a.sign(signingInput), nil
|
||||
}
|
||||
|
||||
// Verify checks a token's signature and expiry, returning the subject.
|
||||
func (a *Authenticator) Verify(token string) (string, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return "", errors.New("malformed token")
|
||||
}
|
||||
signingInput := parts[0] + "." + parts[1]
|
||||
if !hmac.Equal([]byte(a.sign(signingInput)), []byte(parts[2])) {
|
||||
return "", errors.New("bad signature")
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var c claims
|
||||
if err := json.Unmarshal(payload, &c); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if time.Now().Unix() >= c.Exp {
|
||||
return "", errors.New("token expired")
|
||||
}
|
||||
return c.Sub, nil
|
||||
}
|
||||
|
||||
type ctxKey int
|
||||
|
||||
const userKey ctxKey = 0
|
||||
|
||||
// UserFrom returns the authenticated subject from the request context.
|
||||
func UserFrom(ctx context.Context) string {
|
||||
if v, ok := ctx.Value(userKey).(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Middleware rejects requests without a valid Bearer token.
|
||||
func (a *Authenticator) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := r.Header.Get("Authorization")
|
||||
token := strings.TrimPrefix(h, "Bearer ")
|
||||
if token == h || token == "" {
|
||||
http.Error(w, `{"error":{"code":"unauthorized","message":"missing token"}}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
sub, err := a.Verify(token)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":{"code":"unauthorized","message":"invalid token"}}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), userKey, sub)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user