You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang实现带用户名密码认证的HTTP代理及BasicAuth报错排查

问题根因

r.BasicAuth()持续解析失败的核心原因是混淆了普通Web服务认证和HTTP代理认证的协议规范:

  • 普通Web服务的Basic Auth读取Authorization请求头,认证失败返回401 Unauthorized,搭配WWW-Authenticate响应头
  • HTTP代理的Basic Auth读取*Proxy-Authorization请求头,认证失败返回407 Proxy Authentication Required,搭配Proxy-Authenticate*响应头

Go标准库的r.BasicAuth()方法仅会解析Authorization头,不会处理代理专用的Proxy-Authorization头;同时原代码直接返回401状态码,客户端不会识别为代理认证要求,不会主动传递配置的账号密码,最终导致认证一直失败。
原代码还存在两处逻辑笔误:

  • 用户名不匹配的分支日志写为“Username provided is correct”,语义完全相反
  • 密码校验分支打印变量错误,输出了用户名u而非密码p,日志语义同样写反
修复方案
  • 手动解析Proxy-Authorization头完成认证校验,不直接使用r.BasicAuth()
  • 认证失败时返回407状态码,同时携带Proxy-Authenticate响应头,告知客户端使用Basic方式传递认证信息
  • 修正日志和判断逻辑的笔误
  • 普通HTTP请求和CONNECT隧道请求复用同一套认证逻辑

修复后可直接运行的完整代码如下:

package main

import (
	"crypto/tls"
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"strings"
	"time"
)

// 替换为实际的代理账号密码
const (
	proxyUser = "USERNAME"
	proxyPass = "PASSWORD"
)

func main() {
	server := &http.Server{
		Addr:         "0.0.0.0:8080",
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// 调试用:打印完整请求内容
			b, err := httputil.DumpRequest(r, true)
			if err == nil {
				fmt.Println("dump request:\n", string(b))
			} else {
				fmt.Println("dump request error:", err)
			}

			// 读取代理专用认证头
			authHeader := r.Header.Get("Proxy-Authorization")
			if authHeader == "" {
				w.Header().Set("Proxy-Authenticate", `Basic realm="Proxy Auth Required"`)
				w.WriteHeader(http.StatusProxyAuthRequired)
				fmt.Println("missing proxy auth header, return 407")
				return
			}

			// 校验认证类型为Basic
			const basicPrefix = "Basic "
			if !strings.HasPrefix(authHeader, basicPrefix) {
				w.Header().Set("Proxy-Authenticate", `Basic realm="Proxy Auth Required"`)
				w.WriteHeader(http.StatusProxyAuthRequired)
				fmt.Println("unsupported auth type, return 407")
				return
			}

			// 解码base64编码的认证信息
			decodedCred, err := base64.StdEncoding.DecodeString(authHeader[len(basicPrefix):])
			if err != nil {
				w.Header().Set("Proxy-Authenticate", `Basic realm="Proxy Auth Required"`)
				w.WriteHeader(http.StatusProxyAuthRequired)
				fmt.Println("decode auth info error:", err)
				return
			}

			// 拆分用户名和密码
			credParts := strings.SplitN(string(decodedCred), ":", 2)
			if len(credParts) != 2 {
				w.Header().Set("Proxy-Authenticate", `Basic realm="Proxy Auth Required"`)
				w.WriteHeader(http.StatusProxyAuthRequired)
				fmt.Println("invalid credential format")
				return
			}
			inputUser, inputPass := credParts[0], credParts[1]

			// 校验账号密码
			if inputUser != proxyUser {
				w.WriteHeader(http.StatusProxyAuthRequired)
				fmt.Printf("invalid username: %s\n", inputUser)
				return
			}
			if inputPass != proxyPass {
				w.WriteHeader(http.StatusProxyAuthRequired)
				fmt.Printf("invalid password for user: %s\n", inputUser)
				return
			}

			// 认证通过,按请求类型转发
			if r.Method == http.MethodConnect {
				handleTunneling(w, r)
			} else {
				handleHTTP(w, r)
			}
		}),
		// 禁用HTTP/2
		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
	}

	log.Fatal(server.ListenAndServe())
}

// 普通HTTP请求转发
func handleHTTP(w http.ResponseWriter, r *http.Request) {
	r.RequestURI = ""
	proxy := httputil.NewSingleHostReverseProxy(r.URL)
	proxy.ServeHTTP(w, r)
}

// HTTPS CONNECT隧道处理
func handleTunneling(w http.ResponseWriter, r *http.Request) {
	destConn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)
	if err != nil {
		http.Error(w, err.Error(), http.StatusServiceUnavailable)
		return
	}
	w.WriteHeader(http.StatusOK)

	hijacker, ok := w.(http.Hijacker)
	if !ok {
		http.Error(w, "hijack not supported", http.StatusInternalServerError)
		destConn.Close()
		return
	}
	clientConn, _, err := hijacker.Hijack()
	if err != nil {
		http.Error(w, err.Error(), http.StatusServiceUnavailable)
		destConn.Close()
		return
	}

	// 双向拷贝流量
	go func() {
		defer clientConn.Close()
		defer destConn.Close()
		_, _ = io.Copy(destConn, clientConn)
	}()
	go func() {
		defer clientConn.Close()
		defer destConn.Close()
		_, _ = io.Copy(clientConn, destConn)
	}()
}
验证注意事项
  • 确认FoxyProxy中配置的账号密码和代码中常量完全一致,无多余空格或特殊字符转义问题
  • 可通过控制台打印的请求dump内容,确认Proxy-Authorization头是否被客户端正确携带
  • 命令行测试可使用带代理参数的curl命令:curl -x http://USERNAME:PASSWORD@127.0.0.1:8080 https://example.com,不要直接请求代理服务地址测试

内容的提问来源于stack exchange,提问作者m shiravand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 03:45:47