从http.Request获取客户端IP地址的正确方法

117

如何从http.Request中获取所有客户端的IP地址?在PHP中,有很多需要检查的变量。在Go语言中也是这样吗?

我找到的一个方法是:

req.RemoteAddr

请求是否区分大小写?例如x-forwarded-forX-Forwarded-For以及X-FORWARDED-FOR是相同的吗?(来自req.Header.Get("X-FORWARDED-FOR")

8个回答

124

查看http.Request, 你可以找到以下成员变量:

// HTTP defines that header names are case-insensitive.
// The request parser implements this by canonicalizing the
// name, making the first character and any characters
// following a hyphen uppercase and the rest lowercase.
//
// For client requests certain headers are automatically
// added and may override values in Header.
//
// See the documentation for the Request.Write method.
Header Header

// RemoteAddr allows HTTP servers and other software to record
// the network address that sent the request, usually for
// logging. This field is not filled in by ReadRequest and
// has no defined format. The HTTP server in this package
// sets RemoteAddr to an "IP:port" address before invoking a
// handler.
// This field is ignored by the HTTP client.
RemoteAddr string

您可以使用RemoteAddr来获取远程客户端的IP地址和端口(格式为“IP:port”),这是原始请求者或最后一个代理(例如在您的服务器前面的负载均衡器)的地址。

这是你唯一确定的信息。

然后,您可以调查标题,它们是不区分大小写的(根据上面的文档),这意味着您所有的示例都将起作用并产生相同的结果:

req.Header.Get("X-Forwarded-For") // capitalisation
req.Header.Get("x-forwarded-for") // doesn't
req.Header.Get("X-FORWARDED-FOR") // matter

这是因为内部的http.Header.Get会为您规范化键名。(如果您想直接访问header map,而不是通过Get访问,则需要先使用http.CanonicalHeaderKey。)
最后,"X-Forwarded-For"可能是您想查看以获取有关客户端IP更多信息的字段。但这在很大程度上取决于远程端使用的HTTP软件,因为客户端可以在其中放置任何内容。此外,请注意,此字段的预期格式是逗号+空格分隔的IP地址列表。您需要对其进行一些解析,以获取所需的单个IP(可能是列表中的第一个IP),例如:
// Assuming format is as expected
ips := strings.Split("10.0.0.1, 10.0.0.2, 10.0.0.3", ", ")
for _, ip := range ips {
    fmt.Println(ip)
}

将产生:

10.0.0.1
10.0.0.2
10.0.0.3

1
从我对 req.Header 的文档阅读中得出结论,你只需要执行 req.Header.Get("X-Forwarded-For") 就可以了,因为其他情况都已经被解析器规范化到此处了。 - Lars Haugseth
3
当然,我的回答是说所有变体都可以作为标题,因为标题不区分大小写。我只提到这些是因为OP在问有关大小写敏感性的问题,但我可以看出措辞可能会让人困惑。我会进行更新。 - tomasz
是的,HTTP头区分大小写,但你可以使用http.CanonicalHeaderKey而不是尝试所有可能的大写和小写组合。 - bithavoc
1
你应该警惕客户端的“X-Forwarded-For”欺骗。你的服务的第一个外部网关可能应该剥离客户端提供的任何这样的头部。 - Falco
我也想在iOS中获取连接的接入点的IP地址。有人能帮我吗? - Akshada-Systematix

41

这是我构想IP的方式

func ReadUserIP(r *http.Request) string {
    IPAddress := r.Header.Get("X-Real-Ip")
    if IPAddress == "" {
        IPAddress = r.Header.Get("X-Forwarded-For")
    }
    if IPAddress == "" {
        IPAddress = r.RemoteAddr
    }
    return IPAddress
}
  • X-Real-Ip - 获取第一个真实 IP(如果请求位于多个 NAT 源/负载均衡器的后面)

  • X-Forwarded-For - 如果由于某些原因 X-Real-Ip 为空并且没有返回响应,则从 X-Forwarded-For 获取

  • Remote Address - 最后一招(通常不可靠,因为这可能是最后一个 IP,或者如果它是向服务器发出的裸 HTTP 请求,即没有负载均衡器)

11
警告:请检查此答案的评论 - https://dev59.com/O3A75IYBdhLWcg3w-OVA#55790 - "客户端可以设置 X-Forwarded-ForX-Real-IP 标头为任意值。除非您有可信赖的反向代理,否则不应使用这些值。" - hewiefreeman

35

这里是一个完全工作的例子

package main

import (  
    // Standard library packages
    "fmt"
    "strconv"
    "log"
    "net"
    "net/http"

    // Third party packages
    "github.com/julienschmidt/httprouter"
    "github.com/skratchdot/open-golang/open"
)



// https://blog.golang.org/context/userip/userip.go
func getIP(w http.ResponseWriter, req *http.Request, _ httprouter.Params){
    fmt.Fprintf(w, "<h1>static file server</h1><p><a href='./static'>folder</p></a>")

    ip, port, err := net.SplitHostPort(req.RemoteAddr)
    if err != nil {
        //return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)

        fmt.Fprintf(w, "userip: %q is not IP:port", req.RemoteAddr)
    }

    userIP := net.ParseIP(ip)
    if userIP == nil {
        //return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
        fmt.Fprintf(w, "userip: %q is not IP:port", req.RemoteAddr)
        return
    }

    // This will only be defined when site is accessed via non-anonymous proxy
    // and takes precedence over RemoteAddr
    // Header.Get is case-insensitive
    forward := req.Header.Get("X-Forwarded-For")

    fmt.Fprintf(w, "<p>IP: %s</p>", ip)
    fmt.Fprintf(w, "<p>Port: %s</p>", port)
    fmt.Fprintf(w, "<p>Forwarded for: %s</p>", forward)
}


func main() {  
    myport := strconv.Itoa(10002);


    // Instantiate a new router
    r := httprouter.New()

    r.GET("/ip", getIP)

    // Add a handler on /test
    r.GET("/test", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
        // Simply write some test data for now
        fmt.Fprint(w, "Welcome!\n")
    })  


    l, err := net.Listen("tcp", "localhost:" + myport)
    if err != nil {
        log.Fatal(err)
    }
    // The browser can connect now because the listening socket is open.


    //err = open.Start("http://localhost:"+ myport + "/test")
    err = open.Start("http://localhost:"+ myport + "/ip")
    if err != nil {
         log.Println(err)
    }

    // Start the blocking server loop.
    log.Fatal(http.Serve(l, r)) 
}

为什么不在“X-Forwarded-For”上运行ParseIP? - user776942
3
@kristen:实际上,X-Forwarded-For 是(潜在地)一个 IP 地址列表(代理链)。所以你首先需要按“,”进行分割。 - Stefan Steiger

6

我认为我有比当前发布的方法更好的方法。

package main

import (
    "fmt"
    "log"
    "net"
    "net/http"
    "strings"
)

func main() {
    http.HandleFunc("/", getUserIP)
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

// Get the IP address of the server's connected user.
func getUserIP(httpWriter http.ResponseWriter, httpServer *http.Request) {
    var userIP string
    if len(httpServer.Header.Get("CF-Connecting-IP")) > 1 {
        userIP = httpServer.Header.Get("CF-Connecting-IP")
        fmt.Println(net.ParseIP(userIP))
    } else if len(httpServer.Header.Get("X-Forwarded-For")) > 1 {
        userIP = httpServer.Header.Get("X-Forwarded-For")
        fmt.Println(net.ParseIP(userIP))
    } else if len(httpServer.Header.Get("X-Real-IP")) > 1 {
        userIP = httpServer.Header.Get("X-Real-IP")
        fmt.Println(net.ParseIP(userIP))
    } else {
        userIP = httpServer.RemoteAddr
        if strings.Contains(userIP, ":") {
            fmt.Println(net.ParseIP(strings.Split(userIP, ":")[0]))
        } else {
            fmt.Println(net.ParseIP(userIP))
        }
    }
}

5
请解释为什么这样做更好,您解释得越多,答案就越好。 - Pranav Asthana

5
在PHP中有许多变量需要检查。在Go语言中也是如此吗?
这与Go(或PHP)无关。它取决于客户端、代理、负载均衡器或服务器发送的内容。根据您的环境获取所需内容。
http.Request.RemoteAddr包含远程IP地址。它可能是您实际的客户端,也可能不是。
请求是否区分大小写?例如,x-forwarded-for与X-Forwarded-For和X-FORWARDED-FOR相同吗?(从req.Header.Get("X-FORWARDED-FOR"))
不,为什么不自己试试?http://play.golang.org/p/YMf_UBvDsH

1
在设置/获取之前,标头始终会转换为相同的MIME格式,因此req.Header.Get参数不区分大小写。来源https://golang.org/pkg/net/textproto/#CanonicalMIMEHeaderKey - k3a

4
根据Mozilla MDN的说法: "X-Forwarded-For (XFF)标头是一种事实上的标准标头,用于确定客户端的源IP地址。"
他们在他们的X-Forwarded-For文章中发布了清晰的信息。

3
客户端可以设置X-Forwarded-For头部的任意值。在没有检查可信代理的情况下使用X-Forwarded-For可能会导致IP欺骗。
头部示例:X-Forwarded-For: <client>, <proxy1>, <proxy2> 例如,有人可能会这样调用:
curl -H "X-Forwarded-For: 136.226.254.1" -H "X-Real-Ip: 136.226.254.2" "http://super.com"

如果您的L7负载均衡器不检查和清理这些头,则会在代码中出现IP欺骗(136.226.254.1)。如果您基于客户端IP地址有一些逻辑,它将无法正常工作。例如,基于IP进行限流。

例如,nginx模块http://nginx.org/ru/docs/http/ngx_http_realip_module.html使用基于获取链式X-Forwarded-For地址中最后一个不受信任的IP地址的逻辑。我没有找到合适的中间件来使用相同的逻辑,并编写了它:https://github.com/thrownew/go-middlewares/tree/main/clientip


1

在使用Cloudfront时,客户端IP地址位于x-original-forwarded-for中。以下是Javascript示例。

function getIp(request) {
  const { headers, connection, socket } = request
  const connectionSocket = connection && connection.socket

  return (
    (headers && headers['x-original-forwarded-for']) ||
    (connection && connection.remoteAddress) ||
    (socket && socket.remoteAddress) ||
    (connectionSocket && connectionSocket.remoteAddress) ||
    null
  )
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接