移除 backend .env,暂时关闭 JWT,改为一次性登录
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
# 拷贝为 web/backend/.env 后填入真实值。.env 已被 .gitignore 排除。
|
||||
# 首次启动时,若 auth.db 中没有任何 admin 用户,会用下面这一对凭据创建管理员;
|
||||
# 一旦 admin 已存在,这两个变量会被忽略,改它们不会改密码。
|
||||
ADMIN_USER=admin
|
||||
ADMIN_PASS=changeme
|
||||
|
||||
# JWT 签名密钥;生成方式:openssl rand -hex 32
|
||||
JWT_SECRET=replace-with-32-bytes-hex
|
||||
@@ -3,15 +3,13 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ListenAddr string
|
||||
DatabaseURL string
|
||||
AuthDBPath string
|
||||
JWTSecret []byte
|
||||
TushareAPIURL string
|
||||
ListenAddr string
|
||||
DatabaseURL string
|
||||
AuthDBPath string
|
||||
TushareAPIURL string
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
@@ -24,11 +22,6 @@ func Load() (*Config, error) {
|
||||
if cfg.DatabaseURL == "" {
|
||||
return nil, fmt.Errorf("DATABASE_URL 环境变量未设置")
|
||||
}
|
||||
secret := strings.TrimSpace(os.Getenv("JWT_SECRET"))
|
||||
if len(secret) < 16 {
|
||||
return nil, fmt.Errorf("JWT_SECRET 必须至少 16 个字符 (建议 openssl rand -hex 32)")
|
||||
}
|
||||
cfg.JWTSecret = []byte(secret)
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -54,13 +54,9 @@ func (d *Deps) Login(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, http.StatusUnauthorized, "用户名或密码错误")
|
||||
return
|
||||
}
|
||||
token, _, err := d.JWT.Issue(u.ID, u.Username, u.Role)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "issue token failed")
|
||||
return
|
||||
}
|
||||
// 暂时不用 JWT,返回固定 token
|
||||
writeJSON(w, http.StatusOK, loginResp{
|
||||
Token: token,
|
||||
Token: "noop",
|
||||
User: publicUserView{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
|
||||
@@ -5,16 +5,14 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"trade/web/internal/auth"
|
||||
"trade/web/internal/store"
|
||||
)
|
||||
|
||||
// Deps 是所有 handler 需要的运行时依赖,在 router 装配时一次性注入。
|
||||
type Deps struct {
|
||||
Auth *store.AuthStore
|
||||
Futures *store.FuturesStore
|
||||
JWT *auth.Manager
|
||||
TushareURL string
|
||||
Auth *store.AuthStore
|
||||
Futures *store.FuturesStore
|
||||
TushareURL string
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
|
||||
@@ -3,9 +3,7 @@ package middleware
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"trade/web/internal/auth"
|
||||
"trade/web/internal/store"
|
||||
)
|
||||
|
||||
@@ -24,32 +22,14 @@ func FromContext(ctx context.Context) (CtxUser, bool) {
|
||||
return u, ok
|
||||
}
|
||||
|
||||
// RequireUser 校验 Authorization Bearer JWT,通过后把 CtxUser 写入 context。
|
||||
// 同时校验数据库里的 disabled 状态,被禁用的账户即使持有 token 也会被拒。
|
||||
func RequireUser(mgr *auth.Manager, s *store.AuthStore) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tok := bearer(r)
|
||||
if tok == "" {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing token"})
|
||||
return
|
||||
}
|
||||
claims, err := mgr.Parse(tok)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"})
|
||||
return
|
||||
}
|
||||
u, err := s.GetByID(claims.UserID)
|
||||
if err != nil || u.Disabled {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "account disabled or removed"})
|
||||
return
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), userKey, CtxUser{
|
||||
ID: u.ID, Username: u.Username, Role: u.Role,
|
||||
})
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
// RequireUser 不再校验 JWT,直接注入默认管理员用户,所有请求放行。
|
||||
func RequireUser(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := context.WithValue(r.Context(), userKey, CtxUser{
|
||||
ID: 1, Username: "admin", Role: store.RoleAdmin,
|
||||
})
|
||||
}
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func RequireAdmin(next http.Handler) http.Handler {
|
||||
@@ -62,12 +42,3 @@ func RequireAdmin(next http.Handler) http.Handler {
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func bearer(r *http.Request) string {
|
||||
h := r.Header.Get("Authorization")
|
||||
const p = "Bearer "
|
||||
if strings.HasPrefix(h, p) {
|
||||
return strings.TrimSpace(h[len(p):])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -7,13 +7,11 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"trade/web/internal/auth"
|
||||
"trade/web/internal/handlers"
|
||||
mw "trade/web/internal/middleware"
|
||||
"trade/web/internal/store"
|
||||
)
|
||||
|
||||
func New(d *handlers.Deps, mgr *auth.Manager, authStore *store.AuthStore, dist fs.FS) http.Handler {
|
||||
func New(d *handlers.Deps, dist fs.FS) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(mw.Recover)
|
||||
r.Use(mw.Logger)
|
||||
@@ -22,7 +20,7 @@ func New(d *handlers.Deps, mgr *auth.Manager, authStore *store.AuthStore, dist f
|
||||
r.Post("/login", d.Login)
|
||||
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(mw.RequireUser(mgr, authStore))
|
||||
r.Use(mw.RequireUser)
|
||||
|
||||
r.Post("/logout", d.Logout)
|
||||
r.Get("/me", d.Me)
|
||||
|
||||
@@ -40,8 +40,7 @@ func main() {
|
||||
log.Fatalf("bootstrap: %v", err)
|
||||
}
|
||||
|
||||
mgr := auth.NewManager(cfg.JWTSecret)
|
||||
deps := &handlers.Deps{Auth: authDB, Futures: futures, JWT: mgr, TushareURL: cfg.TushareAPIURL}
|
||||
deps := &handlers.Deps{Auth: authDB, Futures: futures, TushareURL: cfg.TushareAPIURL}
|
||||
|
||||
dist, err := fs.Sub(distFS, "dist")
|
||||
if err != nil {
|
||||
@@ -50,7 +49,7 @@ func main() {
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
Handler: router.New(deps, mgr, authDB, dist),
|
||||
Handler: router.New(deps, dist),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user