HTTP Basic Auth 的认证过程远没有想象中复杂。客户端第一次访问受保护资源时,服务端返回 401 状态码,并在响应头里带上 WWW-Authenticate: Basic realm="..."。浏览器收到后弹出一个用户名密码输入框,用户提交后,浏览器把用户名和密码以冒号拼接,再做 Base64 编码,最终通过 Authorization: Basic <encoded> 头回传给服务端。服务端只需要反向解码并比对凭证即可。Go 的 net/http 包把反向解析封装成了 r.BasicAuth(),不用再手工处理编码。

一、Go标准库的BasicAuth方法
在 handler 里直接使用 r.BasicAuth() 是最短路径。该方法返回三个值:用户名、密码和一个布尔值。布尔值为 false 通常表示请求头里没有 Authorization 字段,或者认证方案不是 Basic。这意味着它已经帮你完成了前缀检查、Base64 解码以及按冒号拆分用户名密码的工作。很多从其它语言转过来的开发者会自己读取头、手动 base64.StdEncoding.DecodeString,但其实完全没有必要。
下面是一个最小可运行示例,把它挂到 /private 路径上即可。
package main
import (
"net/http"
)
func privateHandler(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "admin" || password != "secret" {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
w.Write([]byte("Welcome to the private area"))
}
func main() {
http.HandleFunc("/private", privateHandler)
http.ListenAndServe(":8080", nil)
}
这段代码的逻辑很直观。需要注意的是 r.BasicAuth() 并不会修改任何状态,它只是解析请求头并返回结果。认证失败时,返回 401 前必须设置 WWW-Authenticate 头,否则浏览器不会弹出登录框,只会显示一串无意义的错误信息。同时 http.Error 会输出纯文本,对于 API 服务可以用 json.NewEncoder 输出 JSON,但状态码仍需保持 401。
二、把认证逻辑抽成中间件
如果每个需要保护的接口都重复上面这段判断,项目规模稍微增长就会变得很难维护。Go 社区处理这类横切关注点的惯用方式是编写中间件:一个接收 http.Handler 并返回新的 http.Handler 的函数。中间件内部完成认证,通过后调用原来的 handler,未通过则直接返回 401。这样做的好处是路由注册仍然清晰,业务 handler 完全不需要感知认证逻辑。
下面这个 BasicAuthMiddleware 就是一个典型实现。它把用户名和密码做成闭包变量,避免在全局暴露凭证。
package main
import (
"net/http"
)
func BasicAuthMiddleware(username, password string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok || u != username || p != password {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
使用时可以像下面这样把中间件套在已有的 http.Handler 上。如果用的是 http.ServeMux,要注意中间件的包裹顺序:越靠外的中间件越先执行。认证中间件通常放在最外层,这样后续的日志、CORS 等中间件就可以只处理已经通过认证的请求。
mux := http.NewServeMux()
mux.HandleFunc("/private", privateHandler)
handler := BasicAuthMiddleware("admin", "secret", mux)
http.ListenAndServe(":8080", handler)
这里把整个 mux 包进认证中间件,意味着所有路由都要求认证。如果只想保护部分路由,可以拆成两个不同的 mux,或者在路由注册时按路径分别包裹。后一种方式在路由数量不多时更直观。无论哪种方式,中间件都保持了业务代码的干净。
三、手动解析与401细节
虽然 r.BasicAuth() 覆盖了大多数场景,但了解它背后的解析过程对排查问题仍有帮助。标准方法要求的 Authorization 头格式是 Basic base64(user:pass)。其中 Base64 编码的是 username:password 的字节序列,注意用户名和密码本身都不能包含冒号,否则拆分时会出错。服务端收到后先按空格分割,取第一部分确认为 Basic,再解码剩余部分。如果请求头是 Bearer token 或其它方案,r.BasicAuth() 会直接返回 false。
手动解析的意义在于定制错误处理。例如某些客户端会发送格式错误但内容合法的 Base64 字符串,r.BasicAuth() 内部会静默忽略解码错误并返回 false。如果你希望记录更详细的审计日志,可以自己读取 r.Header.Get("Authorization"),用 strings.SplitN 和 base64.StdEncoding.DecodeString 处理。不过对于绝大多数业务系统来说,直接使用标准方法并配合正确的 401 响应已经足够。
另一个容易忽略的细节是 WWW-Authenticate 头中的 realm 字段。它相当于一个保护区域的标识,浏览器会根据 realm 缓存用户凭据。如果同一服务有多个保护区域,应该给它们设置不同的 realm 值,否则浏览器可能会把 A 区域的凭证自动发送给 B 区域。这个字段的值需要用双引号包裹,但在 Go 字符串中可以用反引号避免转义。
四、安全边界与测试思路
Basic Auth 最大的软肋是凭证以明文形式参与传输。虽然 Base64 编码看起来像密文,但它只是编码不是加密,任何拿到请求的人都可以立即解码。因此生产环境使用 Basic Auth 必须搭配 TLS,让请求在 HTTPS 通道内传输。即使这样,也不建议把权限较高的账号密码长期放在客户端,更不要把同一个密码复用到多个系统。对于更严格的场景,应迁移到 OAuth2 或基于签名的认证方案。
测试 Basic Auth 中间件时,可以借助 net/http/httptest 包模拟请求。先构造一个简单的下游 handler,再包上中间件,通过 httptest.NewRequest 设置 Authorization 头。分别测试无头、错误密码、正确密码三种情况,断言状态码和响应体。测试代码不需要真正启动 TCP 端口,执行速度很快。
func TestBasicAuthMiddleware(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
handler := BasicAuthMiddleware("admin", "secret", next)
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.SetBasicAuth("admin", "secret")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
}
在这个测试里,req.SetBasicAuth 会自动生成合法的 Authorization 头。需要手动覆盖时,也可以直接 req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:secret")))。注意不要在生产代码里写死密码,测试用例中的凭证应与配置系统隔离。
Go语言HTTP Basic Auth认证中间件修改时间:2026-10-07 06:09:57