From baef42f920c82584ecea3433b5dde657ff45b342 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 26 May 2026 09:37:42 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E9=89=B4=E6=9D=83=E9=85=8D=E7=BD=AE=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E5=8C=85=E6=8B=AC=E7=94=A8=E6=88=B7=E5=90=8D?= =?UTF-8?q?=E5=92=8C=E5=AF=86=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openflare_server/model/proxy_route.go | 6 + openflare_server/service/config_version.go | 37 ++++-- openflare_server/service/proxy_route.go | 23 ++++ .../components/proxy-route-config-page.tsx | 122 ++++++++++++++++++ .../components/proxy-route-create-drawer.tsx | 3 + .../web/features/proxy-routes/helpers.ts | 8 ++ .../web/features/proxy-routes/types.ts | 6 + 7 files changed, 195 insertions(+), 10 deletions(-) diff --git a/openflare_server/model/proxy_route.go b/openflare_server/model/proxy_route.go index f45cb3cb..17dd35e6 100644 --- a/openflare_server/model/proxy_route.go +++ b/openflare_server/model/proxy_route.go @@ -26,6 +26,9 @@ type ProxyRoute struct { CustomHeaders string `json:"custom_headers" gorm:"type:text;not null;default:'[]'"` PoWEnabled bool `json:"pow_enabled" gorm:"column:pow_enabled;not null;default:false"` PoWConfig string `json:"pow_config" gorm:"column:pow_config;type:text;not null;default:'{}'"` + BasicAuthEnabled bool `json:"basic_auth_enabled" gorm:"not null;default:false"` + BasicAuthUsername string `json:"basic_auth_username" gorm:"size:255;not null;default:''"` + BasicAuthPassword string `json:"basic_auth_password" gorm:"size:255;not null;default:''"` Remark string `json:"remark" gorm:"size:255"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -80,6 +83,9 @@ func (route *ProxyRoute) Update() error { "custom_headers": route.CustomHeaders, "pow_enabled": route.PoWEnabled, "pow_config": route.PoWConfig, + "basic_auth_enabled": route.BasicAuthEnabled, + "basic_auth_username": route.BasicAuthUsername, + "basic_auth_password": route.BasicAuthPassword, "remark": route.Remark, }).Error } diff --git a/openflare_server/service/config_version.go b/openflare_server/service/config_version.go index 3a840c45..3f52ec8a 100644 --- a/openflare_server/service/config_version.go +++ b/openflare_server/service/config_version.go @@ -2,6 +2,7 @@ package service import ( "crypto/sha256" + "encoding/base64" "encoding/hex" "encoding/json" "errors" @@ -929,7 +930,7 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) builder.WriteString(renderNamedUpstreamBlock(upstreamConfig)) } if !route.EnableHTTPS { - builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, cfg)) + builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) continue } certIDs, err := decodeStoredCertIDs(route.CertIDs, route.CertID) @@ -990,7 +991,7 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) if route.RedirectHTTP { if len(httpOnlyDomains) > 0 { - builder.WriteString(renderHTTPProxyServer(renderServerNames(httpOnlyDomains), route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, cfg)) + builder.WriteString(renderHTTPProxyServer(renderServerNames(httpOnlyDomains), route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) } for _, certID := range certIDs { assignedDomains := domainsByCertID[certID] @@ -1000,14 +1001,14 @@ func renderRouteConfig(routes []*model.ProxyRoute, cfg openRestyConfigSnapshot) builder.WriteString(renderHTTPRedirectServer(renderServerNames(assignedDomains))) } } else { - builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, cfg)) + builder.WriteString(renderHTTPProxyServer(serverNames, route.OriginURL, route.OriginHost, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) } for _, certID := range certIDs { assignedDomains := domainsByCertID[certID] if len(assignedDomains) == 0 { continue } - builder.WriteString(renderHTTPSServer(renderServerNames(assignedDomains), route.OriginURL, route.OriginHost, certID, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, cfg)) + builder.WriteString(renderHTTPSServer(renderServerNames(assignedDomains), route.OriginURL, route.OriginHost, certID, customHeaders, cacheConfig, limitConfig, upstreamConfig, route.PoWEnabled, route.BasicAuthEnabled, route.BasicAuthUsername, route.BasicAuthPassword, cfg)) } } return builder.String(), dedupeSupportFiles(supportFiles), nil @@ -1123,6 +1124,22 @@ func renderPowAccessBlock(powEnabled bool) string { return fmt.Sprintf(" access_by_lua_file %s/pow/check.lua;\n", nginxLuaDirPlaceholder) } +func renderBasicAuthBlock(enabled bool, username, password string) string { + if !enabled || username == "" || password == "" { + return "" + } + credentials := username + ":" + password + encoded := base64.StdEncoding.EncodeToString([]byte(credentials)) + return fmt.Sprintf(` rewrite_by_lua_block { + local auth = ngx.var.http_authorization + if auth ~= "Basic %s" then + ngx.header["WWW-Authenticate"] = 'Basic realm="Restricted"' + return ngx.exit(401) + } + } +`, encoded) +} + func renderPowLocationBlocks(powEnabled bool) string { if !powEnabled { return "" @@ -1250,21 +1267,21 @@ func nextVersionNumber(now time.Time) (string, error) { return fmt.Sprintf("%s-%03d", prefix, count+1), nil } -func renderHTTPProxyServer(serverNames string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, cfg openRestyConfigSnapshot) string { - return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, renderPowLocationBlocks(powEnabled), renderPowAccessBlock(powEnabled), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) +func renderHTTPProxyServer(serverNames string, originURL string, originHost string, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg openRestyConfigSnapshot) string { + return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n%s location / {\n%s%s%s%s%s%s }\n%s}\n\n", serverNames, renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderPowAccessBlock(powEnabled), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) } func renderHTTPRedirectServer(serverNames string) string { return fmt.Sprintf("server {\n listen 80;\n server_name %s;\n\n return 301 https://$host$request_uri;\n}\n\n", serverNames) } -func renderHTTPSServer(serverNames string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, cfg openRestyConfigSnapshot) string { +func renderHTTPSServer(serverNames string, originURL string, originHost string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg openRestyConfigSnapshot) string { certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID)) - return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s location / {\n%s%s%s%s%s }\n%s}\n\n", serverNames, certPath, keyPath, renderPowLocationBlocks(powEnabled), renderPowAccessBlock(powEnabled), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) + return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n%s location / {\n%s%s%s%s%s%s }\n%s}\n\n", serverNames, certPath, keyPath, renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderPowAccessBlock(powEnabled), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) } -func renderHTTPSServerWithCertificates(serverNames string, originURL string, originHost string, certificateIDs []uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, cfg openRestyConfigSnapshot) string { +func renderHTTPSServerWithCertificates(serverNames string, originURL string, originHost string, certificateIDs []uint, customHeaders []ProxyRouteCustomHeaderInput, cacheConfig routeCacheConfig, limitConfig routeLimitConfig, upstreamConfig routeUpstreamConfig, powEnabled bool, basicAuthEnabled bool, basicAuthUsername string, basicAuthPassword string, cfg openRestyConfigSnapshot) string { var certificateBlock strings.Builder for _, certificateID := range certificateIDs { certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) @@ -1272,7 +1289,7 @@ func renderHTTPSServerWithCertificates(serverNames string, originURL string, ori certificateBlock.WriteString(fmt.Sprintf(" ssl_certificate %s;\n", certPath)) certificateBlock.WriteString(fmt.Sprintf(" ssl_certificate_key %s;\n", keyPath)) } - return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n%s\n location / {\n%s%s%s%s }\n}\n\n", serverNames, certificateBlock.String(), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig)) + return fmt.Sprintf("server {\n listen 443 ssl;\n http2 on;\n server_name %s;\n%s%s\n location / {\n%s%s%s%s%s%s }\n%s}\n\n", serverNames, certificateBlock.String(), renderPowLocationBlocks(powEnabled), renderBasicAuthBlock(basicAuthEnabled, basicAuthUsername, basicAuthPassword), renderPowAccessBlock(powEnabled), renderProxyHeaderBlock(originURL, originHost, customHeaders, upstreamConfig), renderRouteLimitBlock(limitConfig), renderRouteCacheBlock(cacheConfig, cfg), renderProxyPassBlock(originURL, upstreamConfig), renderPowStaticLocationBlock(powEnabled)) } func renderServerNames(domains []string) string { diff --git a/openflare_server/service/proxy_route.go b/openflare_server/service/proxy_route.go index 3ff9ad32..ee30f2e4 100644 --- a/openflare_server/service/proxy_route.go +++ b/openflare_server/service/proxy_route.go @@ -56,6 +56,9 @@ type ProxyRouteInput struct { CustomHeaders []ProxyRouteCustomHeaderInput `json:"custom_headers"` PoWEnabled bool `json:"pow_enabled"` PoWConfig string `json:"pow_config"` + BasicAuthEnabled bool `json:"basic_auth_enabled"` + BasicAuthUsername string `json:"basic_auth_username"` + BasicAuthPassword string `json:"basic_auth_password"` Remark string `json:"remark"` } @@ -88,6 +91,9 @@ type ProxyRouteView struct { CustomHeaderList []ProxyRouteCustomHeaderInput `json:"custom_header_list"` PoWEnabled bool `json:"pow_enabled"` PoWConfig *ProxyRoutePoWConfig `json:"pow_config"` + BasicAuthEnabled bool `json:"basic_auth_enabled"` + BasicAuthUsername string `json:"basic_auth_username"` + BasicAuthPassword string `json:"basic_auth_password"` Remark string `json:"remark"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -259,6 +265,17 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro return nil, errors.New("redirect_http requires enable_https") } + if input.BasicAuthEnabled { + input.BasicAuthUsername = strings.TrimSpace(input.BasicAuthUsername) + input.BasicAuthPassword = strings.TrimSpace(input.BasicAuthPassword) + if input.BasicAuthUsername == "" || input.BasicAuthPassword == "" { + return nil, errors.New("basic_auth_username and basic_auth_password cannot be empty when basic auth is enabled") + } + } else { + input.BasicAuthUsername = "" + input.BasicAuthPassword = "" + } + if route == nil { route = &model.ProxyRoute{} } @@ -284,6 +301,9 @@ func buildProxyRoute(route *model.ProxyRoute, input ProxyRouteInput) (*model.Pro route.CustomHeaders = string(customHeadersJSON) route.PoWEnabled = input.PoWEnabled route.PoWConfig = string(powConfigJSON) + route.BasicAuthEnabled = input.BasicAuthEnabled + route.BasicAuthUsername = input.BasicAuthUsername + route.BasicAuthPassword = input.BasicAuthPassword route.Remark = remark return route, nil } @@ -366,6 +386,9 @@ func buildProxyRouteView(route *model.ProxyRoute) (*ProxyRouteView, error) { CustomHeaderList: customHeaders, PoWEnabled: route.PoWEnabled, PoWConfig: powConfig, + BasicAuthEnabled: route.BasicAuthEnabled, + BasicAuthUsername: route.BasicAuthUsername, + BasicAuthPassword: route.BasicAuthPassword, Remark: route.Remark, CreatedAt: route.CreatedAt, UpdatedAt: route.UpdatedAt, diff --git a/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx b/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx index 9db98207..5ea0ee63 100644 --- a/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx +++ b/openflare_server/web/features/proxy-routes/components/proxy-route-config-page.tsx @@ -1020,6 +1020,118 @@ function PowSection({ ); } +const basicAuthSchema = z + .object({ + basic_auth_enabled: z.boolean(), + basic_auth_username: z.string(), + basic_auth_password: z.string(), + }) + .superRefine((value, context) => { + if (value.basic_auth_enabled) { + if (!value.basic_auth_username.trim()) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['basic_auth_username'], + message: '请输入账号', + }); + } + if (!value.basic_auth_password.trim()) { + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['basic_auth_password'], + message: '请输入密码', + }); + } + } + }); + +type BasicAuthValues = z.infer; + +function BasicAuthSection({ + route, + saving, + onSave, +}: { + route: ProxyRouteItem; + saving: boolean; + onSave: SaveHandler; +}) { + const form = useForm({ + resolver: zodResolver(basicAuthSchema), + defaultValues: { + basic_auth_enabled: route.basic_auth_enabled, + basic_auth_username: route.basic_auth_username || '', + basic_auth_password: route.basic_auth_password || '', + }, + }); + + useEffect(() => { + form.reset({ + basic_auth_enabled: route.basic_auth_enabled, + basic_auth_username: route.basic_auth_username || '', + basic_auth_password: route.basic_auth_password || '', + }); + }, [form, route]); + + const watchedEnabled = form.watch('basic_auth_enabled'); + + return ( + +
{ + onSave( + buildPayloadFromRoute(route, { + basic_auth_enabled: values.basic_auth_enabled, + basic_auth_username: values.basic_auth_username.trim(), + basic_auth_password: values.basic_auth_password.trim(), + }), + { message: '认证配置已保存。' }, + ); + })} + > + + form.setValue('basic_auth_enabled', checked, { shouldDirty: true }) + } + /> + + + + + + + + + +
+ ); +} + export function ProxyRouteConfigPage({ routeId, initialSection, @@ -1240,6 +1352,16 @@ export function ProxyRouteConfigPage({ } /> ) : null} + + {currentSection === 'auth' ? ( + + saveMutation.mutate({ payload, context }) + } + /> + ) : null} diff --git a/openflare_server/web/features/proxy-routes/components/proxy-route-create-drawer.tsx b/openflare_server/web/features/proxy-routes/components/proxy-route-create-drawer.tsx index 99cdf652..97e78f46 100644 --- a/openflare_server/web/features/proxy-routes/components/proxy-route-create-drawer.tsx +++ b/openflare_server/web/features/proxy-routes/components/proxy-route-create-drawer.tsx @@ -192,6 +192,9 @@ export function ProxyRouteCreateDrawer({ custom_headers: [], pow_enabled: false, pow_config: '{}', + basic_auth_enabled: false, + basic_auth_username: '', + basic_auth_password: '', remark: values.remark.trim(), }); }, diff --git a/openflare_server/web/features/proxy-routes/helpers.ts b/openflare_server/web/features/proxy-routes/helpers.ts index b17edefa..eb999fd1 100644 --- a/openflare_server/web/features/proxy-routes/helpers.ts +++ b/openflare_server/web/features/proxy-routes/helpers.ts @@ -30,6 +30,11 @@ export const websiteConfigSections = [ label: 'PoW 防护', description: '配置 Proof-of-Work 反爬虫策略。', }, + { + key: 'auth', + label: '认证配置', + description: '配置基础鉴权访问,需要输入账号密码才能访问网站。', + }, ] as const; export type WebsiteConfigSectionKey = @@ -292,6 +297,9 @@ export function buildPayloadFromRoute( remark: route.remark || '', pow_enabled: route.pow_enabled, pow_config: JSON.stringify(route.pow_config), + basic_auth_enabled: route.basic_auth_enabled, + basic_auth_username: route.basic_auth_username, + basic_auth_password: route.basic_auth_password, ...overrides, }; } diff --git a/openflare_server/web/features/proxy-routes/types.ts b/openflare_server/web/features/proxy-routes/types.ts index e2c0c2d5..42461c35 100644 --- a/openflare_server/web/features/proxy-routes/types.ts +++ b/openflare_server/web/features/proxy-routes/types.ts @@ -49,6 +49,9 @@ export interface ProxyRouteItem { custom_header_list: ProxyRouteCustomHeader[]; pow_enabled: boolean; pow_config: ProxyRoutePoWConfig; + basic_auth_enabled: boolean; + basic_auth_username: string; + basic_auth_password: string; remark: string; created_at: string; updated_at: string; @@ -81,6 +84,9 @@ export interface ProxyRouteMutationPayload { custom_headers: ProxyRouteCustomHeader[]; pow_enabled: boolean; pow_config: string; + basic_auth_enabled: boolean; + basic_auth_username?: string; + basic_auth_password?: string; remark: string; }