nginx获取真实客户端IP

1,564 阅读2分钟

在开发过程中,如果遇到要用nginx做反向代理时,后端获取到的IP是来源于nginx的反向代理IP,而不是真实的客户端IP。如果某些功能(比如访问日志)需要获取客户端IP。可以如下配置。

1. 基本框架

首先,我们需要一个后端服务。这里使用express。
后端运行在9527端口上

const express = require('express');
const app = express();

app.post('/api/', (req, res) => {
    res.send({
        code: 0,
        data: 'success api'
    })
})

const PORT = 9527;

app.listen(PORT, () => {
    console.log(`this server already run at ${PORT}`);
})

然后,要在前端请求后端接口,使用react编写前端页面,使用axios发送请求,直接访问后端服务
前端运行在3000端口上

import axios from "axios";

import React, {Component} from 'react';

class App extends Component {
    constructor(props) {
        super(props);
        this.request = this.request.bind(this);
    }

    request(){
        axios.post('http://localhost:9527/api/', {}).then(res => {
            console.log(res);
        })
    }

    render() {
        return (
            < div >
            <button onClick={this.request}>测试</button>
            < /div>
        );
    }
}

export default App;

这时,因为端口不同,后端要做跨域处理

app.all('*',function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    if (req.method === 'OPTIONS') {
        res.send(200);
    }
    else {
        next();
    }
});

分别运行前端和后端。这是,前端应该是可以直接访问到后端服务的。

2. 使用nginx做反向代理

安装nginx服务器。nginx服务器的配置文件在conf/nginx.conf。
nginx服务跑在8010端口上。
通过nginx代理后端请求:

server {
    listen   8010;
    ...
    location /api/ {
        proxy_pass http://localhost:9527;
    }
    ...
}

之后,前端要去请求nginx服务,由nginx服务器将前端的请求代理到真实的服务:

axios.post('http://localhost:8010/api/', {}).then(res => {
    console.log(res);
})

由于nginx服务器本身和前端在不同端口的,所以也要进行跨域配置:

...
location / {
    ...
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    if ($request_method = 'OPTIONS') {
        return 204;
    }
    ...
}
...

这时,前端会访问nginx服务器,然后由nginx服务器将请求代理到真正的后端服务。

3. 获取真实客户端IP

可以这样设置。

...
location /api/ {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_pass http://localhost:9527;
}
...

给代理的请求添加了2个请求头:x-real-ip 和 x-forwarded-for。
x-real-ip获取到的是上一级代理的IP,而不是真正的客户端IP,需要proxy_add_x_forwarded_for进行合作判断。
x-forwarded-for 存储从客户端到各级代理再到服务端链路上经过的所有IP。proxy_add_x_forwarded_for 的意思是向x-forwared-for上添加一次记录。

4. 完整代码

前端:

import axios from "axios";

import React, {Component} from 'react';

class App extends Component {
    constructor(props) {
        super(props);
        this.request = this.request.bind(this);
    }

    request(){
        axios.post('http://localhost:8010/api/', {}).then(res => {
            console.log(res);
        })
    }

    render() {
        return (
            < div >
            <button onClick={this.request}>测试</button>
            < /div>
        );
    }
}

export default App;

nginx.conf:


#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;

    server {
        listen       8010;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
            add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            if ($request_method = 'OPTIONS') {
                return 204;
            }
            root   D:/nginx/nginx-1.21.0/static;
            index  index.html index.htm;
        }
        location /api/ {
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://localhost:9527;
        }
        


        #error_page  404              /404.html;

        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}

        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }


    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}


    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;

    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;

    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;

    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;

    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}

}

后端:

/**
 * @author:  lanshuai
 * @Date: 2021-06-20
 * @description
 */
const express = require('express');
const app = express();

// 解决跨域问题
app.all('*',function (req, res, next) {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');
    res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
    if (req.method === 'OPTIONS') {
        res.send(200);
    }
    else {
        next();
    }
});

app.post('/api/', (req, res) => {
    console.log(req.headers); // 查看请求头,通过x-real-ip和x-forwarded-for获取客户端真实IP
    res.send({
        code: 0,
        data: 'success api'
    })
})

app.post('/', (req, res) => {
    res.send({
        code: 0,
        data: 'success /'
    })
})

/*
* 集中处理了404请求的中间件
* 注意,该中间件必须放在正常处理流程之后,否则会拦截正常请求
* */

app.use((req, res, next) => {
    next({
        code: -1,
        message: `${req.path}接口不存在`
    }) // 抛给下一个异常处理中间件取处理
})

/*
* 自定义路由异常处理中间件
* 注意两点:
* 1. 方法的参数不能减少
* 2. 方法必须放在路由最后
* */
const errorHandler = function (err, req, res, next) {
    console.log(err);
    // 非token错误
    const msg = (err && err.message) || '系统错误';
    const statusCode = (err.output && err.output.statusCode) || 500;
    const errorMsg = (err.output && err.output.payload && err.output
        .payload.error) || err.message;
    res.status(statusCode).json({
        code: -1,
        msg,
        error: statusCode,
        errorMsg
    })
}


app.use(errorHandler);

const PORT = 9527;

app.listen(PORT, () => {
    console.log(`this server already run at ${PORT}`);
})