From dda0c50dec0eb6cc0c5564ed126efd592ea554d9 Mon Sep 17 00:00:00 2001 From: shenlan Date: Thu, 2 Oct 2025 16:55:03 +0800 Subject: [PATCH] feat: allow configuring account TLS and default UI endpoints (#376) --- account/cmd/accountsvc/main.go | 27 ++++++++++++++++--- account/config/account.yaml | 1 + account/config/config.go | 28 ++++++++++++++------ docs/account-service-configuration.md | 4 ++- docs/account-service-deployment.md | 1 + ui/homepage/app/login/LoginContent.tsx | 3 ++- ui/homepage/app/register/RegisterContent.tsx | 3 ++- 7 files changed, 52 insertions(+), 15 deletions(-) diff --git a/account/cmd/accountsvc/main.go b/account/cmd/accountsvc/main.go index 6ff733b..44b2d17 100644 --- a/account/cmd/accountsvc/main.go +++ b/account/cmd/accountsvc/main.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "log/slog" "net" "net/http" @@ -94,11 +95,24 @@ var rootCmd = &cobra.Command{ keyFile := strings.TrimSpace(tlsSettings.KeyFile) clientCAFile := strings.TrimSpace(tlsSettings.ClientCAFile) - useTLS := certFile != "" && keyFile != "" + useTLS := tlsSettings.IsEnabled() var tlsConfig *tls.Config if useTLS { - tlsConfig = &tls.Config{MinVersion: tls.VersionTLS12} + if certFile == "" || keyFile == "" { + return fmt.Errorf("tls is enabled but certFile (%q) or keyFile (%q) is empty", certFile, keyFile) + } + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return fmt.Errorf("failed to load tls certificate: %w", err) + } + + tlsConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + Certificates: []tls.Certificate{cert}, + } + if clientCAFile != "" { caBytes, err := os.ReadFile(clientCAFile) if err != nil { @@ -111,8 +125,13 @@ var rootCmd = &cobra.Command{ tlsConfig.ClientCAs = pool tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert } - } else if clientCAFile != "" { - logger.Warn("client CA configured but TLS certificates are missing; ignoring", "clientCAFile", clientCAFile) + } else { + if certFile != "" || keyFile != "" { + logger.Info("TLS disabled; certificate paths will be ignored", "certFile", certFile, "keyFile", keyFile) + } + if clientCAFile != "" { + logger.Warn("client CA configured but TLS is disabled; ignoring", "clientCAFile", clientCAFile) + } } srv := &http.Server{ diff --git a/account/config/account.yaml b/account/config/account.yaml index a72bf87..609ee55 100644 --- a/account/config/account.yaml +++ b/account/config/account.yaml @@ -6,6 +6,7 @@ server: readTimeout: 15s writeTimeout: 15s tls: + enabled: true certFile: "/etc/ssl/svc.plus.pem" keyFile: "/etc/ssl/svc.plus.rsa.key" clientCAFile: "" diff --git a/account/config/config.go b/account/config/config.go index 34ce73c..bd60fe0 100644 --- a/account/config/config.go +++ b/account/config/config.go @@ -4,6 +4,7 @@ import ( "errors" "os" "path/filepath" + "strings" "time" "gopkg.in/yaml.v3" @@ -26,18 +27,29 @@ type Config struct { // Server defines HTTP server configuration. type Server struct { - Addr string `yaml:"addr"` - ReadTimeout time.Duration `yaml:"readTimeout"` - WriteTimeout time.Duration `yaml:"writeTimeout"` - TLS TLS `yaml:"tls"` + Addr string `yaml:"addr"` + ReadTimeout time.Duration `yaml:"readTimeout"` + WriteTimeout time.Duration `yaml:"writeTimeout"` + TLS TLS `yaml:"tls"` } // TLS describes TLS configuration for the server listener. type TLS struct { - CertFile string `yaml:"certFile"` - KeyFile string `yaml:"keyFile"` - ClientCAFile string `yaml:"clientCAFile"` - RedirectHTTP bool `yaml:"redirectHttp"` + Enabled *bool `yaml:"enabled"` + CertFile string `yaml:"certFile"` + KeyFile string `yaml:"keyFile"` + ClientCAFile string `yaml:"clientCAFile"` + RedirectHTTP bool `yaml:"redirectHttp"` +} + +// IsEnabled reports whether TLS should be enabled for the server listener. When the +// configuration explicitly sets the Enabled field it is respected. Otherwise TLS is +// considered enabled only if both the certificate and key paths are non-empty. +func (t TLS) IsEnabled() bool { + if t.Enabled != nil { + return *t.Enabled + } + return strings.TrimSpace(t.CertFile) != "" && strings.TrimSpace(t.KeyFile) != "" } // Store defines persistence configuration for the account service. diff --git a/docs/account-service-configuration.md b/docs/account-service-configuration.md index d4caaa7..ea784b5 100644 --- a/docs/account-service-configuration.md +++ b/docs/account-service-configuration.md @@ -27,6 +27,7 @@ server: readTimeout: 15s # 读取超时 writeTimeout: 15s # 写入超时 tls: # 启用 HTTPS 时的证书配置 + enabled: true # 显式启用/关闭 TLS(为空时仍根据证书路径推断) certFile: "/etc/ssl/certs/account.pem" keyFile: "/etc/ssl/private/account.key" clientCAFile: "" # (可选)双向 TLS CA @@ -42,7 +43,7 @@ session: ttl: 24h # 登录会话有效期 ``` -**TLS 提示**:当 `certFile` 和 `keyFile` 都非空时,`accountsvc` 会调用 `ListenAndServeTLS` 启动 HTTPS。如果同时希望保留 80 端口,可将 `redirectHttp` 置为 `true`,服务会开启一个额外的明文监听,将请求 301 重定向到 HTTPS。 +**TLS 提示**:当 `tls.enabled` 显式为 `true` 时或 `certFile` 与 `keyFile` 均提供时,`accountsvc` 会调用 `ListenAndServeTLS` 启动 HTTPS。需要在开发环境暂时关闭 TLS,可将 `tls.enabled` 设为 `false`,此时服务会忽略证书路径并仅监听 HTTP。如果同时希望保留 80 端口,可将 `redirectHttp` 置为 `true`,服务会开启一个额外的明文监听,将请求 301 重定向到 HTTPS。 **MFA 相关接口**:账号服务在 `/api/auth/mfa/*` 下提供 MFA 绑定与验证接口,默认无需额外配置即可使用,但生产环境建议将 `server.tls` 打开,确保 MFA 秘钥与 TOTP 码在传输过程中被加密。MFA 挑战 token 默认 10 分钟过期,服务器会接受 ±1 个 30 秒窗口的 TOTP 漂移,因此务必启用 NTP 等时间同步手段,避免合法验证码因时钟偏差被拒绝。 @@ -73,6 +74,7 @@ server: readTimeout: 15s writeTimeout: 15s tls: + enabled: true certFile: "/etc/ssl/certs/account.pem" keyFile: "/etc/ssl/private/account.key" redirectHttp: true diff --git a/docs/account-service-deployment.md b/docs/account-service-deployment.md index 22f36a8..72956c4 100644 --- a/docs/account-service-deployment.md +++ b/docs/account-service-deployment.md @@ -79,6 +79,7 @@ server: addr: ":8443" tls: + enabled: true certFile: "/etc/ssl/certs/account.pem" keyFile: "/etc/ssl/private/account.key" clientCAFile: "" # (可选)配置客户端证书验证 diff --git a/ui/homepage/app/login/LoginContent.tsx b/ui/homepage/app/login/LoginContent.tsx index 058e7ed..83d4e45 100644 --- a/ui/homepage/app/login/LoginContent.tsx +++ b/ui/homepage/app/login/LoginContent.tsx @@ -53,9 +53,10 @@ export default function LoginContent({ children }: LoginContentProps) { [], ) + const accountServiceBaseUrl = (process.env.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || 'https://127.0.0.1:8443').replace(/\/$/, '') const githubAuthUrl = process.env.NEXT_PUBLIC_GITHUB_AUTH_URL || '/api/auth/github' const wechatAuthUrl = process.env.NEXT_PUBLIC_WECHAT_AUTH_URL || '/api/auth/wechat' - const loginUrl = process.env.NEXT_PUBLIC_LOGIN_URL || '/api/auth/login' + const loginUrl = process.env.NEXT_PUBLIC_LOGIN_URL || `${accountServiceBaseUrl}/api/auth/login` const socialButtonsDisabled = true diff --git a/ui/homepage/app/register/RegisterContent.tsx b/ui/homepage/app/register/RegisterContent.tsx index 8bb5671..9bf0334 100644 --- a/ui/homepage/app/register/RegisterContent.tsx +++ b/ui/homepage/app/register/RegisterContent.tsx @@ -22,9 +22,10 @@ export default function RegisterContent() { const searchParams = useSearchParams() const router = useRouter() + const accountServiceBaseUrl = (process.env.NEXT_PUBLIC_ACCOUNT_SERVICE_URL || 'https://127.0.0.1:8443').replace(/\/$/, '') const githubAuthUrl = process.env.NEXT_PUBLIC_GITHUB_AUTH_URL || '/api/auth/github' const wechatAuthUrl = process.env.NEXT_PUBLIC_WECHAT_AUTH_URL || '/api/auth/wechat' - const registerUrl = process.env.NEXT_PUBLIC_REGISTER_URL || '/api/auth/register' + const registerUrl = process.env.NEXT_PUBLIC_REGISTER_URL || `${accountServiceBaseUrl}/api/auth/register` const isSocialAuthVisible = false useEffect(() => {