Golang实现HTTPS、多主机与自动证书管理的最佳实践

Golang实现HTTPS、多主机与自动证书管理的最佳实践 我在此寻求帮助,是因为我难以找到关于如何将 autocert 用于 net/http 多主机的示例。我并非想要一个复制粘贴的解决方案,而是真心希望看到并探索实现此目标的方法。

使用 certbot 的 NGINX 反向代理方案不符合我的需求。我需要通过 net/http + autocert 或类似的 ACME2 autocert 等效方案来实现。

为什么 NGINX 不是解决方案: 我需要“实时”处理请求日志记录,并且不能中断请求的流程。通过 Alice 中间件,我启动一个 Go 协程来独立处理请求日志记录(传递给一个独立的微服务),这与正常的 HTTP 流程分开。这就是 NGINX 方案行不通的原因。

我花了数周时间研究 chi、echo(作为新用户无法添加更多链接)等框架的文档、示例和讨论,但它们似乎主要面向 API 开发。虽然我将在其他服务中使用 chi 来处理任何 API 需求,但要找到一个使用 autocert 实现 net/http + 多主机的解决方案确实很困难。

多个主机通过 http.FileServer 提供静态内容服务,任何 API 路径则被重定向到其他服务。

像 Caddy 等方案在性能上相较于 net/http 有所不足,因此也不是可行的解决方案。

创建一个 HTTP 解决方案对我来说没有问题,但在升级到 HTTPS 解决方案时,我的“多主机”需求遇到了瓶颈,因此我向 Go 社区寻求帮助。

我接触 Go 的时间相对较短(约 1 年经验),尽管我从事软件开发已有 20 多年。

任何指点、链接或建议都将不胜感激。


更多关于Golang实现HTTPS、多主机与自动证书管理的最佳实践的实战教程也可以访问 https://www.itying.com/category-94-b0.html

4 回复

我不完全确定我理解了你的使用场景,但你见过这个吗?:https://brendanr.net/blog/go-docker-https/

更多关于Golang实现HTTPS、多主机与自动证书管理的最佳实践的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


我上面写的大部分内容,根据我的理解,结果都是错误的。我将保留之前的示例,因为它们可能仍然有助于理解这个问题。

本质上,我问题中的“多主机”部分是我误解的根源:

func main() {

    var m *autocert.Manager

    var httpsSrv *http.Server

    // 设置中间件
    reqProcRoute := alice.New(middleware.VMonitor).Then(router.Router)

    prod := true

    log.Printf("PROD: %v",prod  )

    if prod {
        m = &autocert.Manager{
            Prompt:     autocert.AcceptTOS,
            HostPolicy: autocert.HostWhitelist("site1.dab", "www.site1.dab", "site2.dab", "www.site2.dab"),
            Cache:      autocert.DirCache("./certs-cache"),
            // 如何使用 Letsencrypt 的测试服务器进行测试
            Client: &acme.Client{
                DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory",
            },
        }

        httpsSrv = makeHTTPServer()
        httpsSrv.Addr = ":443"
        httpsSrv.Handler = reqProcRoute
        httpsSrv.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate}

        go func() {
            fmt.Printf("Starting HTTPS server on %s\n", httpsSrv.Addr)
            err := httpsSrv.ListenAndServeTLS("", "")
            if err != nil {
                log.Fatalf("HTTP TLS Server failed with %s", err)
            }
        }()
    }

    // 端口 :80 上的传入请求将在此处处理
    var httpSrv = &http.Server{
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 5 * time.Second,
        IdleTimeout:  120 * time.Second,
        Addr: ":80",
        Handler: reqProcRoute,
    }

    handleRedirect := func(w http.ResponseWriter, r *http.Request) {
        newURI := "https://" + r.Host + r.URL.String()
        http.Redirect(w, r, newURI, http.StatusMovedPermanently)
    }

    //如果 Autocert.Manager != nil,则处理 letsencrypt 的 :80 流量
    if m != nil {
        httpSrv.Handler = m.HTTPHandler(httpSrv.Handler)
    } else {
        // 将所有 HTTP 重定向到 HTTPS。注意:这对于 Letsencrypt 的挑战将不起作用
        httpSrv.Handler = http.HandlerFunc(handleRedirect)
    }

    log.Fatal(httpSrv.ListenAndServe())

} // main 函数结束。

请注意 redirectHandler,它将解析任何请求的 ‘host’ 值,无论有多少个主机,因此一个适用于所有主机的单一重定向处理程序就能工作。我之前误以为需要单独处理“每个主机”!

现在,“多主机”问题已经解决,我现在只剩下一直以来的核心问题:

如何将所有 HTTP 流量重定向到 HTTPS,同时仍然允许 Letsencrypt 通过 HTTP 访问以进行其 HTTP 挑战?

虽然感谢你的回复,但和我见过的大多数类似例子一样,例如这个示例,它没有处理多个主机从HTTP到HTTPS的重定向问题。

对于那些可能关注这个讨论的人,以下是我目前已有的部分代码片段和结构说明:

每个主机的根目录/public_html目录都位于一个sites目录下:sites -> siteAsites -> siteB 等等。这是为每个主机提供静态内容的地方。

我创建了我的路由器/mux:package router

package router

type dmux struct {
    StaticRoutes map[string]http.Handler
    APIRoutes    map[string]http.HandlerFunc
}

var Router = dmux{
    StaticRoutes: make(map[string]http.Handler),
    APIRoutes:    make(map[string]http.HandlerFunc),
}

dmux结构体上的LoadStaticSites方法:

func (mx dmux) LoadStaticSites() error {

	fileServer := http.FileServer(http.Dir(path.Join("/var/www/sites", "siteA")))
	mx.StaticRoutes["siteA"] = fileServer
	
	fileServer = http.FileServer(http.Dir(path.Join("/var/www/sites", "siteB")))
	mx.StaticRoutes["siteB"] = fileServer

 return nil
}

路由器/mux的ServeHTTP方法是:

func (mx dmux) ServeHTTP(w http.ResponseWriter, r *http.Request) {

if staticHandler := mx.StaticRoutes[r.Host]; staticHandler != nil {

	// This check is done here so that every hosted site will have access to the API handlers
	if APIHandler := mx.APIRoutes[r.URL.Path]; APIHandler != nil {
		// Check for an API Route
		APIHandler.ServeHTTP(w, r)
	} else {
		staticHandler.ServeHTTP(w, r)
	}

} else {
	// Handle host names for which no handler is registered
	http.Error(w, "Forbidden", 403) // Or Redirect?
}
}

来自main.gopackage main

func init() {

    err := router.Router.LoadStaticSites()
    if err != nil {
	    fmt.Println("Failed to load sites.")
	    panic(err)
    }

    // Load API routes
    router.Router.APIRoutes[`/email`] = apiHandlers.Email
    router.Router.APIRoutes[`/samples`] = apiHandlers.Samples

}

func main() {

var m *autocert.Manager

var httpsSrv *http.Server

prod := true

log.Printf("PROD: %v",prod  )

if prod {
	m = &autocert.Manager{
		Prompt:     autocert.AcceptTOS,
		HostPolicy: autocert.HostWhitelist("site1.dab", "www.site1.dab", "site2.dab", "www.site2.dab"),
		Cache:      autocert.DirCache("./certs-cache"),
		// Use Letsencrypts staging server for testing
		Client: &acme.Client{
			DirectoryURL: "https://acme-staging-v02.api.letsencrypt.org/directory",
		},
	}

	httpsSrv = makeHTTPServer()
	httpsSrv.Addr = ":443"
	httpsSrv.Handler = router.Router
	httpsSrv.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate}

	go func() {
		fmt.Printf("Starting HTTPS server on %s\n", httpsSrv.Addr)
		err := httpsSrv.ListenAndServeTLS("", "")
		if err != nil {
			log.Fatalf("HTTP TLS Server failed with %s", err)
		}
	}()
}

var httpSrv *http.Server

if prod {

	httpSrv = makeHTTPToHTTPSRedirectServer()
} else {
	log.Println("Running HTTP")
	httpSrv = makeHTTPServer()

	httpSrv.Handler = router.Router
}

//allow autocert handle Let's Encrypt callbacks over http
if m != nil {
	httpSrv.Handler = m.HTTPHandler(httpSrv.Handler)
}

httpSrv.Addr = ":80"

log.Fatal(httpSrv.ListenAndServe())
} // End of main

func makeHTTPServer() *http.Server {
   return &http.Server{
	ReadTimeout:  5 * time.Second,
	WriteTimeout: 5 * time.Second,
	IdleTimeout:  120 * time.Second,
   }
}

当我尝试运行HTTPS时,这个函数会出现各种问题,例如循环重定向……HTTP运行正常。

func makeHTTPToHTTPSRedirectServer() *http.Server {
    handleRedirect := func(w http.ResponseWriter, r *http.Request) {
	        newURI := "https://" + r.Host + r.URL.String()
	        http.Redirect(w, r, newURI, http.StatusMovedPermanently)
        }
    router.Router.APIRoutes[`/`] = handleRedirect
    router.Router.StaticRoutes[`/`] = http.HandlerFunc(handleRedirect)
    return makeHTTPServer()
}

正如我在原帖中所写,我接触Go语言相对较新,大约有一年经验,但这是我第一次接触“面向公众”的HTTP/HTTPS服务器。

package main

import (
    "context"
    "crypto/tls"
    "fmt"
    "log"
    "net/http"
    "time"

    "golang.org/x/crypto/acme/autocert"
)

// 多主机证书管理器
type multiHostManager struct {
    hosts []string
    manager *autocert.Manager
}

func newMultiHostManager(hosts []string, cacheDir string) *multiHostManager {
    m := &autocert.Manager{
        Prompt:     autocert.AcceptTOS,
        HostPolicy: autocert.HostWhitelist(hosts...),
        Cache:      autocert.DirCache(cacheDir),
        RenewBefore: 30 * 24 * time.Hour, // 证书到期前30天自动续期
    }
    
    return &multiHostManager{
        hosts:   hosts,
        manager: m,
    }
}

// 静态文件处理器
func staticHandler(host string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 验证主机头
        if r.Host != host {
            http.Error(w, "Invalid host", http.StatusBadRequest)
            return
        }
        
        // 静态文件服务
        fs := http.FileServer(http.Dir("./static/" + host))
        fs.ServeHTTP(w, r)
    })
}

// API处理器示例
func apiHandler(host string) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 异步日志记录
        go func() {
            logEntry := fmt.Sprintf("[%s] %s %s %s", 
                time.Now().Format(time.RFC3339),
                r.Host,
                r.Method,
                r.URL.Path)
            // 这里可以发送到日志微服务
            fmt.Println("Async log:", logEntry)
        }()
        
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"host": "%s", "status": "ok"}`, host)
    })
}

// 主路由处理器
func mainHandler(host string) http.Handler {
    mux := http.NewServeMux()
    
    // 静态文件路由
    mux.Handle("/static/", http.StripPrefix("/static/", staticHandler(host)))
    
    // API路由
    mux.HandleFunc("/api/", apiHandler(host).ServeHTTP)
    
    // 默认路由
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
            http.NotFound(w, r)
            return
        }
        fmt.Fprintf(w, "Welcome to %s", host)
    })
    
    return mux
}

func main() {
    // 配置多个主机
    hosts := []string{
        "example1.com",
        "example2.com",
        "example3.com",
    }
    
    // 创建证书管理器
    certManager := newMultiHostManager(hosts, "./certs")
    
    // 创建主服务器
    mainServer := &http.Server{
        Addr: ":443",
        TLSConfig: &tls.Config{
            GetCertificate: certManager.manager.GetCertificate,
            MinVersion: tls.VersionTLS12, // 强制TLS 1.2+
        },
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
    
    // 为每个主机注册处理器
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        host := r.Host
        
        // 根据主机选择处理器
        switch host {
        case "example1.com":
            mainHandler("example1.com").ServeHTTP(w, r)
        case "example2.com":
            mainHandler("example2.com").ServeHTTP(w, r)
        case "example3.com":
            mainHandler("example3.com").ServeHTTP(w, r)
        default:
            http.Error(w, "Host not found", http.StatusNotFound)
        }
    })
    
    mainServer.Handler = handler
    
    // HTTP重定向到HTTPS
    go func() {
        redirectServer := &http.Server{
            Addr: ":80",
            Handler: certManager.manager.HTTPHandler(nil),
            ReadTimeout:  5 * time.Second,
            WriteTimeout: 5 * time.Second,
        }
        
        if err := redirectServer.ListenAndServe(); err != nil {
            log.Fatalf("HTTP redirect server failed: %v", err)
        }
    }()
    
    // 启动HTTPS服务器
    log.Println("Starting HTTPS server on :443")
    if err := mainServer.ListenAndServeTLS("", ""); err != nil {
        log.Fatalf("HTTPS server failed: %v", err)
    }
}
// 使用中间件的增强版本
package main

import (
    "net/http"
    "time"
    
    "github.com/justinas/alice"
)

// 日志中间件
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // 异步处理日志
        go func() {
            logData := map[string]interface{}{
                "timestamp": time.Now().UTC(),
                "host":      r.Host,
                "method":    r.Method,
                "path":      r.URL.Path,
                "user_agent": r.UserAgent(),
                "duration":  time.Since(start).Seconds(),
            }
            // 发送到日志微服务
            sendToLogService(logData)
        }()
        
        next.ServeHTTP(w, r)
    })
}

// 主机验证中间件
func hostValidationMiddleware(allowedHosts []string) func(http.Handler) http.Handler {
    hostMap := make(map[string]bool)
    for _, host := range allowedHosts {
        hostMap[host] = true
    }
    
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !hostMap[r.Host] {
                http.Error(w, "Invalid host", http.StatusBadRequest)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// 使用Alice中间件链
func createHandlerChain(host string) http.Handler {
    common := alice.New(
        loggingMiddleware,
        hostValidationMiddleware([]string{host}),
    )
    
    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", apiHandler)
    mux.Handle("/static/", http.StripPrefix("/static/", staticHandler(host)))
    
    return common.Then(mux)
}

func sendToLogService(data map[string]interface{}) {
    // 实现日志发送逻辑
    fmt.Printf("Log entry: %+v\n", data)
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte(`{"status": "success"}`))
}
// 生产环境配置示例
package config

import (
    "os"
    "strconv"
)

type Config struct {
    Hosts        []string
    CacheDir     string
    ReadTimeout  int
    WriteTimeout int
    IdleTimeout  int
}

func LoadConfig() *Config {
    hosts := []string{
        os.Getenv("HOST1"),
        os.Getenv("HOST2"),
        os.Getenv("HOST3"),
    }
    
    // 过滤空值
    validHosts := []string{}
    for _, host := range hosts {
        if host != "" {
            validHosts = append(validHosts, host)
        }
    }
    
    readTimeout, _ := strconv.Atoi(os.Getenv("READ_TIMEOUT"))
    if readTimeout == 0 {
        readTimeout = 5
    }
    
    return &Config{
        Hosts:        validHosts,
        CacheDir:     os.Getenv("CERT_CACHE_DIR"),
        ReadTimeout:  readTimeout,
        WriteTimeout: 10,
        IdleTimeout:  120,
    }
}

这个实现提供了:

  1. 多主机自动证书管理
  2. 异步请求日志处理
  3. 静态文件和API路由分离
  4. HTTP到HTTPS自动重定向
  5. 中间件支持
  6. 生产环境配置管理

证书会自动从Let’s Encrypt获取并缓存,支持多个主机名的TLS证书管理。

回到顶部