跳到主要内容

IP 地址

IP 地址在 HTTP 中起着基础性的作用;它用于访问控制、审计、基于地理位置的访问分析等。Slim 为此提供了方便的方法 Context#RealIP() 轻松获取。

但是,从请求中检索真实 IP 地址并非易事,尤其是在我们使用了某些代理技术时。在这种情况下,需要在 HTTP 层将代理程序中的真实 IP 中继到我们的应用程序上,所以我们不能无条件地信任 HTTP 报头,否则可能得到虚假地址,产生安全风险!

若要可靠且安全地检索 IP 地址,必须让应用程序了解基础架构的整个体系结构。在 Slim 中,可以通过适当配置 Slim#IPExtractor 来完成,本指南向您展示了原因和方法。

默认行为

如果我们不显式地为 Slim 实例属性 Slim#IPExtractor 设置值,Context#RealIP() 会使用以下回退逻辑:

  1. 首先检查 X-Forwarded-For 标头,取第一个 IP 地址
  2. 如果没有,检查 X-Real-IP 标头
  3. 最后回退到 Request.RemoteAddr
func (c *contextImpl) RealIP() string {
if c.slim.IPExtractor != nil {
return c.slim.IPExtractor(c.request)
}
// 回退到默认行为
if ip := c.request.Header.Get("X-Forwarded-For"); ip != "" {
// 取第一个 IP
i := strings.IndexAny(ip, ",")
if i > 0 {
return strings.TrimSpace(ip[:i])
}
return ip
}
if ip := c.request.Header.Get("X-Real-IP"); ip != "" {
return ip
}
// 从 RemoteAddr 提取 IP
ra, _, _ := net.SplitHostPort(c.request.RemoteAddr)
return ra
}
注意

默认行为可能不安全!如果客户端可以直接访问你的应用(没有代理),他们可以伪造 X-Forwarded-ForX-Real-IP 标头。

自定义 IP 提取器

IPExtractor 是一个函数类型,用于从 http.Request 中提取 IP 地址:

type IPExtractor func(*http.Request) string

你可以根据自己的基础架构实现自定义的 IP 提取器。

场景 1:没有代理(直连)

如果你没有放置代理(例如:面向互联网的服务),你只需要(并且必须)使用网络层的 IP 地址。任何 HTTP 标头都是不可信的,因为客户端可以完全控制要设置的标头。

直接从 RemoteAddr 提取 IP
s := slim.New()

// 只信任 RemoteAddr,忽略所有标头
s.IPExtractor = func(r *http.Request) string {
ra, _, _ := net.SplitHostPort(r.RemoteAddr)
return ra
}

场景 2:使用可信代理

如果你的应用位于一个或多个可信代理之后(如 Nginx、云负载均衡器等),你需要根据代理设置的标头来提取 IP。

使用 X-Real-IP 标头

一些代理(如 Nginx)使用 X-Real-IP 标头传递客户端 IP:

从 X-Real-IP 提取
s.IPExtractor = func(r *http.Request) string {
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
// 回退到 RemoteAddr
ra, _, _ := net.SplitHostPort(r.RemoteAddr)
return ra
}

使用 X-Forwarded-For 标头

X-Forwarded-For (XFF) 是用于中继客户端 IP 地址的常用标头。在代理的每一跃点,它们都会在标头末尾附加请求 IP 地址。

以下示例关系图演示了此行为:

            ┌──────────┐            ┌──────────┐            ┌──────────┐
───────────>│ Proxy 1 │───────────>│ Proxy 2 │───────────>│ Your app │
客户端 (a) │ (IP: b) │ │ (IP: c) │ │ │
└──────────┘ └──────────┘ └──────────┘

情况 1. 客户端直连 Proxy 1
XFF: "" "a" "a, b"
~~~~~~

情况 2. 客户端已通过代理 x
XFF: "x" "x, a" "x, a, b"
~~~~~~~~~
↑ 你的应用程序将看到的内容

从左边提取(最不安全)

从 XFF 提取第一个 IP(不推荐)
s.IPExtractor = func(r *http.Request) string {
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
ra, _, _ := net.SplitHostPort(r.RemoteAddr)
return ra
}
// 取第一个 IP
i := strings.IndexAny(xff, ",")
if i > 0 {
return strings.TrimSpace(xff[:i])
}
return xff
}
危险

这种方式不安全!客户端可以伪造 X-Forwarded-For 标头。只有当你确定所有流量都经过可信代理时才使用。

从右边提取(推荐)

如果你知道代理的数量(例如:1 个负载均衡器),可以从右边倒数提取:

从 XFF 提取倒数第 N 个 IP
// 假设有 1 个可信代理
s.IPExtractor = func(r *http.Request) string {
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
ra, _, _ := net.SplitHostPort(r.RemoteAddr)
return ra
}

ips := strings.Split(xff, ",")
// 从右边数第 2 个(跳过最后一个代理添加的 IP)
if len(ips) >= 2 {
return strings.TrimSpace(ips[len(ips)-2])
}
// 如果只有一个 IP,直接返回
return strings.TrimSpace(ips[0])
}

场景 3:云平台(AWS、GCP 等)

云平台通常有自己的标头:

AWS ALB/ELB
// AWS 使用 X-Forwarded-For
s.IPExtractor = func(r *http.Request) string {
// AWS ALB 会在 XFF 的最左边添加客户端 IP
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
ra, _, _ := net.SplitHostPort(r.RemoteAddr)
return ra
}

ips := strings.Split(xff, ",")
// 取第一个(客户端真实 IP)
return strings.TrimSpace(ips[0])
}
Cloudflare
// Cloudflare 使用 CF-Connecting-IP
s.IPExtractor = func(r *http.Request) string {
if ip := r.Header.Get("CF-Connecting-IP"); ip != "" {
return ip
}
ra, _, _ := net.SplitHostPort(r.RemoteAddr)
return ra
}

完整示例

生产环境示例
package main

import (
"net"
"net/http"
"strings"

"go-slim.dev/slim"
)

func main() {
s := slim.New()

// 配置 IP 提取器(根据你的基础架构选择)
s.IPExtractor = extractIPFromXFF

s.GET("/", func(c slim.Context) error {
ip := c.RealIP()
return c.String(http.StatusOK, "Your IP: " + ip)
})

s.Start(":1324")
}

// 从 X-Forwarded-For 提取 IP(假设有 1 个可信代理)
func extractIPFromXFF(r *http.Request) string {
xff := r.Header.Get("X-Forwarded-For")
if xff == "" {
ra, _, _ := net.SplitHostPort(r.RemoteAddr)
return ra
}

ips := strings.Split(xff, ",")
if len(ips) >= 2 {
// 跳过最后一个(代理的 IP)
return strings.TrimSpace(ips[len(ips)-2])
}
return strings.TrimSpace(ips[0])
}

最佳实践

  1. 了解你的架构:明确知道有多少代理层、它们使用什么标头
  2. 不要盲目信任标头:如果没有代理,不要使用 X-Forwarded-For 等标头
  3. 从右边提取:如果使用 XFF,从右边倒数提取,跳过已知的代理 IP
  4. 测试:在生产环境前测试 IP 提取逻辑
  5. 日志记录:记录提取的 IP 和原始标头,便于调试

相关 API

Context#RealIP()

func (c Context) RealIP() string

返回客户端的真实 IP 地址。行为由 Slim#IPExtractor 配置。

Slim#IPExtractor

type IPExtractor func(*http.Request) string

type Slim struct {
// ...
IPExtractor IPExtractor
// ...
}

IP 提取器函数。如果为 nil,使用默认的回退行为。