Nginx 反向代理解决跨域问题

31,514 阅读6分钟

编写代码两分钟,解决跨域两小时,我吐了。

如果对跨域还不了解的朋友,可以看这篇:【基础】HTTP、TCP/IP 协议的原理及应用

最近一段时间,在搞一个 SDK 的项目,使用的 TS + rolluprollup 相比于 webpack 来说,更利于编写底层库,但是劣势也比较明显,其中之一就是它没有 webpack-dev-server 那样强大的插件来支持本地开发时的服务代理。随之而来的就是一个非常棘手的问题,跨域要怎么解决?

解决跨域的难点

  1. rollup 并没有非常好用的开发时代理插件,能有效解决大部分跨域问题。
  2. 在这个 SDK 项目中,涉及到与多个不同 server 的交互,每个 server 对于跨域的支持各不相同(大部分是根本不支持跨域);对于 request method 的支持也各不相同(有的甚至不支持 OPTIONS 请求)
  3. 不同的 server 不同的接口,对于 cookie 的要求也不相同,有些接口涉及到 cookie 的跨域传输。
  4. 不同的 server 在不同的部门,沟通成本巨大,而且让每个 team 去修改代码,开放跨域协助我本地调试也不现实。

综合上面几点考虑,插件是不能用了,又不想用 node 搭建代理服务。没办法,只好搞一波 Nginx

Nginx 反向代理

我对 nginx 研究不深,目的是只要能帮我解决问题即可,所以就写下我用到的配置,避免下次再遇到同样的问题还要看文档 google。

Nginx 配置文件

Nginx 的安装目录下,有个 conf 文件夹,里面有个 nginx.conf,就是这个文件。

Nginx 常用命令

Nginx 安装目录下打开 bash

  1. ./nginx.exe :用于运行 Nginx ,如果没有改动过配置文件的端口号什么的话,默认代理到 http://localhost:80,浏览器打开输入 http://localhost 能看到 Nginx 的启动页即为成功。
  2. ./nginx.exe -s quit:用于退出 Nginx
  3. ./nginx.exe -s reload:用于重启 Nginx,这个一般用于更改 Nginx 的配置项后,需要重启。
  4. ./nginx.exe -t 用于检查 Nginx 的配置有没有问题,一般在改过 Nginx 配置后,可以调用检查一遍。

如果已经把 Nginx 配置为全局变量,则不需要在 Nginx 的安装目录下开启 bash ,随便哪开都可以访问到,并且直接输入 nginx 即可代表 ./nginx.exe

Nginx 日志

会看 Nginx 日志还是挺重要的,很多问题都可以从日志里排查出来。Nginx 有两种日志:logs/error.loglogs/access.log,分别记录了 Nginx 本身的错误日志,及接口请求的日志。

一般对开发有用的是 access.log ,一条日志结构如下:

127.0.0.1 - - [11/Aug/2021:17:11:15 +0800] "POST /fetch/189 HTTP/1.1" 200 10659 "http://localhost:3000/" "Mozilla/5.0 XXXXX"

日志内容记录了请求源、请求方法、请求接口、Http 版本、status codenavigator 等。

Nginx 反向代理常用配置

打开 conf/nginx.conf 文件,如果只是做反向代理的话,大部分情况只需要配置 http 模块下的 server 即可,一般初始文件,只有一个 server,如果你需要 Nginx 同时开启不同的端口或域名,就需要写多个 server

server

一个 server 模块的配置如下:

server {
        listen       80;   # 端口号
        server_name  localhost;   # server name 默认 localhost

        #access_log  logs/host.access.log  main;

        location / {   # 访问路径匹配规则
            root   html;
            index  index.html index.htm;
        }
        
        error_page   500 502 503 504  /50x.html;  # 错误处理
        location = /50x.html {
            root   html;
        }
}

里面比较重要的是 location 模块,反向代理的主要工作也是配置 location

location

location 配置项定义了一条访问 Nginx 服务某一路径时的匹配规则,location 后面紧跟的是匹配的路径,这个路径可以直接写绝对路径,可以写正则匹配:

location /api1 { # 当访问 http://localhost/api1 时命中
    # ...
}

location ~ ^/(api2/api3) { # 当访问 http://localhost/api2 和 http://localhost/api 3 时命中
    # ...
}

proxy_pass

location 里有多个配置项,其中一个是 proxy_pass ,意思是将当前命中的 Nginx 接口(例如:http://localhost/api )代理到其他 server 的接口,如下例子就是将 http://localhost/api 代理到 https://baidu.com/api

location /api {
    proxy_pass https://baidu.com;
}

需要注意的是,在写 proxy_pass 不能随便在目标地址后加 /,如果你在地址末尾加了 / ,则最终代理是这样的:

location /api {
    proxy_pass https://baidu.com/; # 将会被代理到 https://baidu.com/,后面没有 /api
}

不加 /,则最终代理是这样的,访问 Nginx 命中的 /api ,Nginx 也会自动帮你拼接上去:

location /api {
    proxy_pass https://baidu.com; # 将会代理到 https://baidu.com/api 
}

add_header

location 配置中的 add_header选项,表示 Nginx 将在 response 中添加一些额外的响应头信息给客户端。众所周知,开启跨域支持是需要服务端配置 Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers 这些请求头的,那么既然有了 Nginx 做了中间层代理服务,就算 server 不给我们开启这些,我们完全也能够自给自足:

location /api {
    add_header Access-Control-Allow-Origin * always;
    add_header Access-Control-Allow-Headers *;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, OPTIONS";
    proxy_pass https://baidu.com;
}

其中 * 代表通配符,意思是所有 origin 都允许跨域访问,这个当然是不安全的,但是本地开发就无所谓了,你也可以写自己指定的本地服务。

一般来说,如果请求过程中出现 40X 50X的错误,Nginx 将不会设置 Access-Control-Allow-Origin 继而导致跨域失败,所以需要在后面再加个 always 告诉 Nginx 不管怎样,都给我设置这个响应头。

OPTIONS 请求

其实,编写到这里,大多数的跨域场景,我们应该能很好的解决了,但是我的项目所需要解决的跨域问题,还不是这么简单。

首先,在开发的过程中,我发现对于有些 server ,连我发送的 OPTIONS 请求都过不去,查看 Nginx 日志才发现,Nginx 本身确实帮我将 OPTIONS 请求代理出去了,但是服务端不认这个 Request Method,它只认识 POST,反手给我一个 403。这就麻烦了,server 端不认这个请求,那么后续的 POST 根本没法发,这要咋办?

一番思考后,如果我直接让 Nginx 拦截 OPTIONS 直接响应 200 不就可以了嘛?

location /api {
    add_header Access-Control-Allow-Origin * always;
    add_header Access-Control-Allow-Headers *;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, OPTIONS";
    if ($request_method = 'OPTIONS') {
        return 200;
    }
    proxy_pass https://baidu.com;
}

重启,再试,成功。Nice,第一个问题解决。

proxy_set_header

这个配置项我的项目里并没有怎么用到,但是还是说一下,以便和 add_header 做出区分。

  • add_header 是当请求从 server 端回来时,Nginx 再往这个 response 里添加一些额外的 reponse header 然后发送给 客户端
  • proxy_set_header 是当请求从客户端发出时,Nginx接收到 request 再往请求里添加一些额外的 request header 然后发送给 服务端

常见的一些需要设置 proxy_set_header 的场景,比如说,有些 server 可能需要验证 Host,这个时候,就可以使用 proxy_set_header 伪造一个 Host 来骗过服务端。

proxy_hide_header

这个配置项用于隐藏部分服务端返回的 response header ,这个选项可以和 add_header 配合使用,来达到修改 server response header 的目的。比如有时候,我们做代理转发的时候可能会发现,服务端开启的 Access-Control-Allow-Origin 具体指定的列表里,没有我们发起请求的域,这种情况下跨域同样是无法绕过的;如果再次使用 add_header 重新添加一个新的 Access-Control-Allow-Origin 又会出现重复设置请求头的错误。这种情况下,我们就可以先使用 proxy_hide_header 来将原始 server response header 隐藏,再用 add_header 添加一个新的 response header,即可实现对 server response header 的修改。

location /api {
    # 1. hide the Access-Control-Allow-Origin from the server response
    proxy_hide_header Access-Control-Allow-Origin;
    
    # 2. add a new custom header that allows all * origin instead
    add_header Access-Control-Allow-Origin *;
    proxy_pass https://baidu.com;
}

跨域的 cookie 传输

cookie 这个东西,由于事关安全性,所以是非常敏感的。Domain 不同,Path 不同,浏览器就不会让你访问到这个 Cookie,也不可能存储这个 Cookie ,更不可能让你随随便便地带着 Cookie 跨域名传输,所以要开启跨域情况下的 Cookie 传输,要求也是十分严苛的。

首先,我碰到的一个场景是:server 端需要记录访问 session ,以此来判定当前用户的登陆状态,后面的所有接口,都需要通过 session 来判断。然而当我成功调通登陆接口后,发现后续的所有接口都 Error 并且直接给我 302 到登陆界面,这就说明在服务端那里,我的状态根本就是没登陆,这是怎么回事?

一通排查,我发现是因为 server 端登陆成功后返回的一系列 session 我并没有做处理,所以它们的 Domain 都是 server 的 Domain。浏览器拿到后一看,你当前域是 localhost,你 Cookie 域是其他的域,cookie 不能给你。而后面的请求没有携带这些 session 信息,服务端自然不认得。

一通操作,终于找到了一个叫做 proxy_cookie_domain 的东西

proxy_cookie_domain

这个选项是专门用来修改服务端返回回来的 cookie domain ,可以使用这个配置来将 cookie domain 手动修改后再返回,修改成本地开发的域,这样就能绕过浏览器的限制,成功将 cookie 保存。

location /api {
    add_header Access-Control-Allow-Origin * always;
    add_header Access-Control-Allow-Headers *;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, OPTIONS";
    if ($request_method = 'OPTIONS') {
        return 200;
    }
    proxy_cookie_domain ~\.?baidu.com $host;
    proxy_pass https://baidu.com;
}

再试,可以,这把登陆完成后,打开控制台发现所有来自 server 的 cookie 都被保存下来了。接着请求其他接口,失败。

image.png

image.png

image.png

血压上来了卧槽(此处省略若干优美中国话)。

没有办法,继续解决。

继续找原因,发现是因为就算是本地访问了 Nginx 服务,其实也是跨域的,只不过我将 Nginx 开启了支持普通跨域,可是它不支持 cookie 的跨域传输啊。支持跨域传输 cookie 需要前后端同时支持。

withCredentials

首先在前端这块,我用的是 axios,里面有一项安全认证的配置需要开启:withCredentials: true。将这个配置项打开,代表请求服务端进行跨域的 cookie 传输。

这个 withCredentials 配置项,应该是业界统一命名,命名差别不大,如果不是用的 axios 而且用的别的插件,应该都有这个配置项,写法不同而已,找一下应该能找到。

Access-Control-Allow-Credentials

搞完前端,继续搞 Nginx 里面需要加一个响应头信息:Access-Control-Allow-Credentials: true,代表服务端同意跨域传输凭证。

location /api {
    add_header Access-Control-Allow-Origin * always;
    add_header Access-Control-Allow-Headers *;
    add_header Access-Control-Allow-Methods "GET, POST, PUT, OPTIONS";
    add_header Access-Control-Allow-Credentials true;
    if ($request_method = 'OPTIONS') {
        return 200;
    }
    proxy_cookie_domain ~\.?baidu.com $host;
    proxy_pass https://baidu.com;
}

再试,失败,我*****

再找原因,发现如果开启了 Access-Control-Allow-Credentials 那么 Access-Control-Allow-OriginAccess-Control-Allow-Headers 不能写成 * 这种通配符的形式,必须写明 origin 和 headers。

location /api {
    add_header Access-Control-Allow-Origin http://localhost:3000 always;
    add_header Access-Control-Allow-Headers "Accept,Accept-Encoding,Accept-Language,Connection,Content-Length,Content-Type,Host,Origin,Referer,User-Agent";
    add_header Access-Control-Allow-Methods "GET, POST, PUT, OPTIONS";
    add_header Access-Control-Allow-Credentials true;
    if ($request_method = 'OPTIONS') {
        return 200;
    }
    proxy_cookie_domain ~\.?baidu.com $host;
    proxy_pass https://baidu.com;
}

重启 Nginx,再试,成功了。

proxy_cookie_path

在解决问题的过程中,我发现还有个叫做 proxy_cookie_path 的配置,虽然我没用到,但还是记录一下,这个配置的作用看名字就知道,应该和 proxy_cookie_domain 类似。

一般情况下,server 返回的 cookie 里,可能会写有 Path,如果浏览器的当前 Path 不满足这个 cookie path 的限定条件,cookie 同样是不可操作的。这个 proxy_cookie_path 配置就是为了将 server 返回的 cookie path 改写成自己需要的 cookie path 从而绕过浏览器的限制。

总结

开发两分钟,跨域两小时。

Nginx 的功能很强大,但学习成本也比较大,如果不是专业运维什么的,个人不建议投入大量时间专门学习,相反,如果在后面的工作中碰到问题,带着问题去学,应该会更有帮助。

最后给个 Nginx 配置项官方传送:Nginx 配置

和一篇不错的 nginx 的博客:Nginx 使用与异常处理