From 0c80af80869ebeb170c146a4e335d9676f5b410a Mon Sep 17 00:00:00 2001 From: Noxiven <41963680+Noxiven@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:20:33 +0800 Subject: [PATCH] feat(auth): restrict endpoint access with user whitelist --- README.md | 3 +++ README_zh.md | 3 +++ config.example.yaml | 1 + internal/apps/oauth/middlewares.go | 6 +++++ internal/common/errs.go | 1 + internal/config/model.go | 43 ++++++++++++++++++++---------- mise.toml | 3 +++ 7 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 mise.toml diff --git a/README.md b/README.md index 18fdbff1..0fada36a 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ pnpm dev | Option | Description | Example | |--------|-------------|---------| | `app.addr` | Backend service listening address | `:8000` | +| `app.user_whitelist` | Allowed LINUX DO usernames; omitted or empty means unrestricted | `["alice", "bob"]` | | `worker.port` | Worker probe port | `8001` | | `scheduler.port` | Scheduler probe port | `8002` | | `oauth2.client_id` | OAuth2 Client ID | `your_client_id` | @@ -162,6 +163,8 @@ pnpm dev For detailed configuration instructions, please refer to the `config.example.yaml` file. +Set a non-empty `app.user_whitelist` in `config.yaml` and restart the service to restrict endpoints protected by the `LoginRequired` middleware to the listed users, including existing sessions. Other logged-in users receive HTTP 403. Usernames must match exactly, including case and surrounding whitespace. + ## 🔧 Development Guide ### Backend Development diff --git a/README_zh.md b/README_zh.md index e5a98768..0f39f9ab 100644 --- a/README_zh.md +++ b/README_zh.md @@ -146,6 +146,7 @@ pnpm dev | 配置项 | 说明 | 示例 | |--------|------|------| | `app.addr` | 后端服务监听地址 | `:8000` | +| `app.user_whitelist` | 允许访问的 LINUX DO 用户名;未配置或为空时不限制 | `["alice", "bob"]` | | `worker.port` | Worker 探针端口 | `8001` | | `scheduler.port` | Scheduler 探针端口 | `8002` | | `oauth2.client_id` | OAuth2 客户端 ID | `your_client_id` | @@ -162,6 +163,8 @@ pnpm dev 详细配置说明请参考 `config.example.yaml` 文件。 +在 `config.yaml` 中设置非空的 `app.user_whitelist` 并重启服务后,使用 `LoginRequired` 中间件的接口仅允许白名单用户访问;其他已登录用户返回 HTTP 403。用户名必须完全一致,区分大小写,首尾空格也参与匹配。 + ## 🔧 开发指南 ### 后端开发 diff --git a/config.example.yaml b/config.example.yaml index 945c4139..2007e528 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -17,6 +17,7 @@ app: api_prefix: "/api" frontend_url: "http://localhost:3000" frontend_pay_url: "http://localhost:3000/paying" + user_whitelist: [] # 允许访问的 LINUX DO 用户名,必须完全一致(区分大小写);未配置或为空时不限制,例如 ["alice", "bob"] # OAuth2/OIDC(优先) oauth2: diff --git a/internal/apps/oauth/middlewares.go b/internal/apps/oauth/middlewares.go index 34471531..9985ac80 100644 --- a/internal/apps/oauth/middlewares.go +++ b/internal/apps/oauth/middlewares.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin" "github.com/linux-do/credit/internal/common" + "github.com/linux-do/credit/internal/config" "github.com/linux-do/credit/internal/db" "github.com/linux-do/credit/internal/model" "github.com/linux-do/credit/internal/otel_trace" @@ -59,6 +60,11 @@ func LoginRequired() gin.HandlerFunc { return } + if !config.Config.App.IsUserAllowed(user.Username) { + c.AbortWithStatusJSON(http.StatusForbidden, util.Err(common.UserNotWhitelisted)) + return + } + // log LogForAudit(ctx, &user, c) diff --git a/internal/common/errs.go b/internal/common/errs.go index 8d4c2c37..6e1b5a79 100644 --- a/internal/common/errs.go +++ b/internal/common/errs.go @@ -29,6 +29,7 @@ const ( TestModeCannotProcessOrder = "测试模式下无法处理订单" TestModeOrderRemark = "[测试模式] 此订单为测试订单,未实际扣款" UnAuthorized = "未登录" + UserNotWhitelisted = "用户不在白名单中" RedEnvelopeDisabled = "红包功能未启用" RedEnvelopeAmountExceeded = "红包金额超过单个红包最大限额" RedEnvelopeDailyLimitExceeded = "今日发红包数量已达上限" diff --git a/internal/config/model.go b/internal/config/model.go index 947e816a..a769fd70 100644 --- a/internal/config/model.go +++ b/internal/config/model.go @@ -35,20 +35,21 @@ type configModel struct { // appConfig 应用基本配置 type appConfig struct { - AppName string `mapstructure:"app_name"` - Env string `mapstructure:"env"` - Addr string `mapstructure:"addr"` - NodeID int64 `mapstructure:"node_id"` - APIPrefix string `mapstructure:"api_prefix"` - GracefulShutdownTimeout int `mapstructure:"graceful_shutdown_timeout"` - FrontendURL string `mapstructure:"frontend_url"` - FrontendPayURL string `mapstructure:"frontend_pay_url"` - SessionCookieName string `mapstructure:"session_cookie_name"` - SessionSecret string `mapstructure:"session_secret"` - SessionDomain string `mapstructure:"session_domain"` - SessionAge int `mapstructure:"session_age"` - SessionHttpOnly bool `mapstructure:"session_http_only"` - SessionSecure bool `mapstructure:"session_secure"` + AppName string `mapstructure:"app_name"` + Env string `mapstructure:"env"` + Addr string `mapstructure:"addr"` + NodeID int64 `mapstructure:"node_id"` + APIPrefix string `mapstructure:"api_prefix"` + GracefulShutdownTimeout int `mapstructure:"graceful_shutdown_timeout"` + FrontendURL string `mapstructure:"frontend_url"` + FrontendPayURL string `mapstructure:"frontend_pay_url"` + SessionCookieName string `mapstructure:"session_cookie_name"` + SessionSecret string `mapstructure:"session_secret"` + SessionDomain string `mapstructure:"session_domain"` + SessionAge int `mapstructure:"session_age"` + SessionHttpOnly bool `mapstructure:"session_http_only"` + SessionSecure bool `mapstructure:"session_secure"` + UserWhitelist []string `mapstructure:"user_whitelist"` } // IsProduction 检查当前环境是否为生产环境 @@ -56,6 +57,20 @@ func (a *appConfig) IsProduction() bool { return a.Env == "production" } +// IsUserAllowed 检查用户名是否允许访问,未配置白名单时不限制访问。 +func (a *appConfig) IsUserAllowed(username string) bool { + if len(a.UserWhitelist) == 0 { + return true + } + + for _, allowed := range a.UserWhitelist { + if allowed == username { + return true + } + } + return false +} + // OAuth2Config OAuth2/OIDC认证配置 type OAuth2Config struct { ClientID string `mapstructure:"client_id"` diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..9be2def7 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +go = "1.26" +node = "24"