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

Posted

技术标签:

【中文标题】从 http.Request 获取客户端 IP 地址的正确方法【英文标题】:Correct way of getting Client's IP Addresses from http.Request 【发布时间】:2015-01-29 20:09:59 【问题描述】:

http.Request 获取所有客户端 IP 地址的正确方法是什么?在php 中有很多variables 我应该检查一下。在 Go 上也一样吗?

我找到的一个是:

req.RemoteAddr

请求是否区分大小写?例如x-forwarded-for 是否与X-Forwarded-ForX-FORWARDED-FOR 相同? (来自req.Header.Get("X-FORWARDED-FOR")

【问题讨论】:

【参考方案1】:

在 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

【讨论】:

在设置/获取之前,标头总是转换为相同的 MIME 格式,因此 req.Header.Get 参数不区分大小写。来源golang.org/pkg/net/textproto/#CanonicalMIMEHeaderKey【参考方案2】:

查看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 软件,因为客户端可以根据需要在其中放置任何内容。另外,请注意此字段的expected format 是逗号+空格分隔的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

【讨论】:

我阅读req.Header文档的方式,你只需要做req.Header.Get("X-Forwarded-For"),因为其他情况由解析器规范化。 是的,当然,我的回答是 all 变体将起作用,因为标题不区分大小写。我提到这些只是因为 OP 询问是否区分大小写,但可以看到措辞可能令人困惑。会更新。 是的,标题区分大小写,但您应该使用http.CanonicalHeaderKey 而不是尝试所有可能的大小写组合。 您应该小心客户端的“X-Forwarded-For”欺骗。您的服务的第一个外部网关可能应该剥离客户端提供的任何此类标头。 我也在寻找在 ios 中获取已连接接入点的 IP 地址。谁能帮我解决这个问题?【参考方案3】:

这是一个完整的工作示例

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? @kristen:实际上,X-Forwarded-For (可能)是一个 IP 地址列表(代理链)。所以你需要先用“,”分割。【参考方案4】:

这就是我想出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 获取

远程地址 - 最后的手段(通常不可靠,因为这可能是最后一个 ip,或者如果它是对服务器的裸 http 请求,即没有负载平衡器)

【讨论】:

警告:检查这个答案上的 cmets - ***.com/a/55790/1584308 - “客户端可以将 X-Forwarded-ForX-Real-IP 标头设置为它想要的任何值。除非你有一个受信任的反向代理,你不应该使用任何这些值。”【参考方案5】:

根据 Mozilla MDN:“X-Forwarded-For (XFF) 标头是一个事实上的标准标头,用于识别客户端的原始 IP 地址。” 他们在X-Forwarded-For 文章中发布了明确的信息。

【讨论】:

【参考方案6】:

使用 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
  )

【讨论】:

【参考方案7】:

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

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))
        
    

【讨论】:

请解释为什么这样更好,你解释得越多答案就越好。

以上是关于从 http.Request 获取客户端 IP 地址的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

获取客户端IP和归属地

Vue实战041:获取当前客户端IP地址详解(内网和外网)

Go Web 编程之 请求

触动精灵 获取外网IP

在 RPC 调用中确定请求者的 IP 地址

获取真实ip