Revel框架中需手动解析X-Forwarded-For获取真实IP,且仅信任可信代理网段后最右的非私有IP;须注册HTTP中间件预处理,并将结果注入context以确保日志和审计使用真实IP。

Revel框架里直接调 req.RemoteAddr 拿不到真实IP
因为 req.RemoteAddr 返回的是 TCP 连接对端地址,也就是最后一跳代理(比如 Nginx、HAProxy 或云 LB)的 IP。只要前面有四层代理,这个值就一定是内网地址,比如 10.0.8.5 或 172.16.0.12。Revel 默认不解析 HTTP 头,也不会自动信任 X-Forwarded-For —— 它压根不碰这个头,除非你手动处理。
必须手动解析 X-Forwarded-For,但不能直接取第一个
常见错误是写成 strings.Split(header, ",")[0] 就完事。这会忽略两个关键事实:
-
X-Forwarded-For可被客户端任意伪造,比如发个X-Forwarded-For: 1.2.3.4, 5.6.7.8,你的服务就信了 - 多层代理下,列表长度可能大于 1,但“最左”不等于“可信”。如果中间某层代理不可信(比如公网 CDN),它后面追加的 IP 就不该被采信
正确做法是:只信任来自已知代理网段的请求,并从该请求携带的 X-Forwarded-For 中取最右的、非私有地址:
// 假设可信代理段为 10.0.0.0/8 和 172.16.0.0/12
func getClientIP(req *http.Request) string {
remoteIP := net.ParseIP(req.RemoteAddr[:strings.LastIndex(req.RemoteAddr, ":")])
if !isTrustedProxy(remoteIP) {
return remoteIP.String()
}
<pre class="brush:php;toolbar:false;">xff := req.Header.Get("X-Forwarded-For")
if xff == "" {
return remoteIP.String()
}
ips := strings.Split(xff, ",")
for i := len(ips) - 1; i >= 0; i-- {
ip := strings.TrimSpace(ips[i])
if net.ParseIP(ip) == nil {
continue
}
if !isPrivateIP(ip) {
return ip
}
}
return remoteIP.String()}
Revel 没有内置 realip 中间件,得自己注册 HTTP 中间件
Revel 的拦截器(Interceptor)运行在路由匹配之后,而 IP 解析应该在请求进入业务逻辑前完成。更稳妥的方式是注册一个标准的 Go http.Handler 包裹器:
- 在
app/init.go的init()函数里,用revel.InterceptFunc拦截所有请求前,或更推荐——直接 wraprevel.App.HttpServer - 不要在每个 Controller 里重复调
getClientIP(),否则容易漏、难维护 - 如果用了 Nginx + Proxy Protocol(比如 Doris 或 MySQL 协议场景),
X-Forwarded-For根本不会出现,此时必须启用ngx_http_realip_module并配置set_real_ip_from,再让 Revel 读req.RemoteAddr—— 因为 realip 模块已把它替换成真实 IP
最容易被忽略的点:Revel 日志和审计字段没联动
即使你在 Controller 里正确拿到了真实 IP,revel.INFO.Printf 默认日志、revel.App.Log 记录的仍是 req.RemoteAddr。这意味着:
- 访问日志里看到的还是代理内网 IP
- 审计日志里的
user_ip字段如果直接填c.Request.RemoteAddr,就完全失效
必须显式把解析结果塞进 context 或自定义日志字段。例如在拦截器中:
c.Request.Context() = context.WithValue(c.Request.Context(), "clientIP", getClientIP(c.Request))
后续日志输出时读这个 value,而不是硬编码 RemoteAddr。否则整套解析逻辑再严谨,也只活在业务代码里,日志和安全系统照样瞎。


















