Go HTTP Basic Auth 入门:用中间件保护内部页面

从一个内部管理页出发,讲 Go 标准库如何实现 HTTP Basic Auth 中间件、常量时间比较、配置注入和测试。

很多小型 Go 服务都有一个内部页面:查看任务队列、触发一次同步、检查缓存状态。它不一定值得接入完整登录系统,但也不能裸奔在公网。HTTP Basic Auth 是一个简单选择。浏览器会弹出用户名密码框,请求头里带上凭据,服务端验证后再允许访问。

Basic Auth 不适合复杂用户体系,也不适合精细权限控制。它适合临时工具、内部后台、预览环境和低频运维页面。本文用 Go 标准库写一个中间件,重点讲清楚几个边界:必须配合 HTTPS、密码不要写死、比较要避免明显时序差异、测试要覆盖成功和失败路径。

最小中间件

Go 的 http.Request 提供了 BasicAuth 方法:

func BasicAuth(username, password string, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		gotUser, gotPass, ok := r.BasicAuth()
		if !ok || gotUser != username || gotPass != password {
			w.Header().Set("WWW-Authenticate", `Basic realm="internal"`)
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

使用方式:

mux := http.NewServeMux()
mux.Handle("/admin", BasicAuth("admin", "secret", adminHandler()))

这段代码能跑,但还不够好。用户名密码写死在代码里不合适,字符串直接比较也不是最稳妥的做法。我们继续改。

从配置注入凭据

凭据应该来自配置:

type AuthConfig struct {
	Username string
	Password string
}

func (c AuthConfig) Validate() error {
	if c.Username == "" {
		return errors.New("basic auth username is required")
	}
	if c.Password == "" {
		return errors.New("basic auth password is required")
	}
	return nil
}

启动时读取环境变量:

cfg := AuthConfig{
	Username: os.Getenv("ADMIN_USER"),
	Password: os.Getenv("ADMIN_PASSWORD"),
}
if err := cfg.Validate(); err != nil {
	log.Fatal(err)
}

不要给生产密码默认值。默认值适合端口、超时,不适合密钥。内部页面的密码如果没配置,服务应该启动失败,而不是使用一个人人都能猜到的默认密码。

常量时间比较

认证比较可以用 crypto/subtle

func secureCompare(a, b string) bool {
	if len(a) != len(b) {
		return false
	}
	return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
}

中间件:

func BasicAuthMiddleware(cfg AuthConfig, next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user, pass, ok := r.BasicAuth()
		if !ok ||
			!secureCompare(user, cfg.Username) ||
			!secureCompare(pass, cfg.Password) {
			w.Header().Set("WWW-Authenticate", `Basic realm="internal"`)
			http.Error(w, "unauthorized", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

常量时间比较不是说 Basic Auth 因此变成高安全登录系统,而是避免明显的字符逐个比较差异。对密码这类敏感值,养成使用专门比较函数的习惯是值得的。

必须配合 HTTPS

Basic Auth 的凭据不是加密登录票据。它只是经过 Base64 编码放在请求头里。没有 HTTPS 时,中间人可以直接看到用户名和密码。内部网络也不要过度信任,尤其是公司 Wi-Fi、代理、测试环境和远程办公链路都可能经过多个节点。

所以 Basic Auth 的前提是:生产环境必须 HTTPS。如果你的 Go 服务在反向代理后面,TLS 可能由 Nginx、Caddy、负载均衡或云平台终止。应用层至少要知道自己是否只在受控内网暴露,不要把 Basic Auth 当成加密方案。

保护一组路由

如果多个内部路由都要保护,可以先建一个子 mux:

adminMux := http.NewServeMux()
adminMux.HandleFunc("/admin/jobs", jobsHandler)
adminMux.HandleFunc("/admin/cache", cacheHandler)

root := http.NewServeMux()
root.Handle("/admin/", BasicAuthMiddleware(cfg, adminMux))

这样认证逻辑集中在入口,而不是每个 handler 里重复写。中间件的意义就是把横切逻辑放到边界上:认证、日志、恢复 panic、请求 ID、限流都适合这么做。

测试认证成功和失败

使用 httptest

func TestBasicAuthSuccess(t *testing.T) {
	cfg := AuthConfig{Username: "admin", Password: "secret"}
	next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNoContent)
	})
	handler := BasicAuthMiddleware(cfg, next)

	req := httptest.NewRequest(http.MethodGet, "/admin", nil)
	req.SetBasicAuth("admin", "secret")
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusNoContent {
		t.Fatalf("status = %d", rec.Code)
	}
}

失败测试:

func TestBasicAuthRejectsWrongPassword(t *testing.T) {
	cfg := AuthConfig{Username: "admin", Password: "secret"}
	handler := BasicAuthMiddleware(cfg, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		t.Fatal("next should not be called")
	}))

	req := httptest.NewRequest(http.MethodGet, "/admin", nil)
	req.SetBasicAuth("admin", "bad")
	rec := httptest.NewRecorder()

	handler.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnauthorized {
		t.Fatalf("status = %d", rec.Code)
	}
}

测试里不要只测成功路径。认证中间件最重要的行为是拦住错误请求,而且拦住时不能调用后面的 handler。

小结

Go 标准库实现 Basic Auth 很直接:用 r.BasicAuth() 取凭据,用中间件包住内部路由,失败时返回 401 并设置 WWW-Authenticate。凭据应该来自配置,启动时校验,比较时使用更稳妥的方式。

Basic Auth 的边界也要说清楚:它必须配合 HTTPS,不适合复杂权限系统,不应该把密码写死在代码里。用在内部工具上,它是一种简单、低成本、容易测试的保护层。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页