Flutter Web CORS解决方案3-反向代理

本文介绍第三种解决FlutterWeb CORS问题的方案:基于 Nginx 的反向代理服务。


在 2-协议代理转发方案 中,local-cors-proxy 主要是修改代码中涉及跨域请求的cgi域名,由本地代理服务器代理请求cgi。

假设 flutter web server 端口号为 8080,local-cors-proxy 端口号为 8000,则本地访问 localhost:8080,内部cgi协议请求同域的 localhost:8000,从而解决跨域问题。

在 local-cors-proxy 方案中,执行 flutter run -d web-server 启动 local flutter web server,在本机浏览器中输入 http://localhost:8080 可以成功跨域访问。但是,在局域网其他机器上通过 LAN IP(例如 http://192.168.0.100:8080)访问,请求跨域的cgi还是失败。在 shelf_proxy 方案中,serveAddress 已修改为 anyIP,可以支持 localhost 或基于 LAN IP 的局域网访问。

这里继续介绍第三种方案——基于 Nginx 的反向代理服务。所谓反向代理(Reverse Proxy),是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

flutter web server 服务监听端口号为 8000,nginx 反向代理服务器监听端口号为 8080,本机访问链接为 localhost(或127.0.0.1):8080,局域网内的访问链接为 192.168.0.106:8080。配置 nginx 反向代理,将 cgi 请求和静态网页资源转发到不同的目标服务器。

  1. 将静态资源请求 proxy_pass 给本地 web 服务器(8000)。
  2. 将 cgi 请求直接 proxy_pass 给目标服务器,从而解决跨域问题。

修改代码中的 cgi 域名

代码中根据运行模式,设置了不同的域名:

  • debug: 正式prod域名;
  • profile:测试testing域名;
  • release:线上智能网关域名;

首先,修改涉及跨域问题的 cgi 请求,将三个环境的绝对 url 地址改为相对地址。

release 模式下的 web socket url 域名改为 coding.net 正式环境域名,同 debug 模式。

在 shelf_proxy 方案中,flutter run 通过 --dart-define 传入参数,替换占位变量 apiBaseUrlauthToken,如下图分支 ①。
在 nginx 反向代理方案中,给 flutter run 传递一个 --dart-define=NGINX_REVERSE_PROXY=true 参数,Dart 代码解析以确定是走 nginx 反向代理。
由于访问的url和port即是nginx代理服务器,协议是直接同域转发,故将 baseUrl 的 host 部分移除改为相对path,如下图分支 ②。

启动 flutter web server

修改好 cig 域名路径后,启动 flutter web 服务器。

  1. 方案一:执行 flutter run -d web-server 构建并启动 web server:

详情查看帮助:flutter run -h
该方案支持按键 r/R 热重载,按键 q 退出服务
也可不指定 --profile,默认是 --debug,也可指定 --release,方便调试各种模式
如果服务不正常或访问不可达,可尝试移除 --web-hostname 选项,可能存在版本兼容问题

# 1. 进入flutter web项目根目录
$ cd ~/Projects/tgit/coding-oa/app
# 2. 执行 flutter run 构建启动服务
$ flutter run -d web-server --profile --web-renderer html --web-port 8000 --web-hostname 0.0.0.0
  1. 方案二:执行 flutter build 编译,然后启动 python http.server 服务:

一个 zsh 常驻窗口,实时查看 http.server 运行日志;
另开一个 zsh 窗口,改动代码需重新执行 build 构建;

# 1. 进入flutter web项目根目录
$ cd ~/Projects/tgit/coding-oa/app
# 2. 执行 flutter build web 命令编译打包
## 详情查看帮助:flutter build -h,flutter build web -h
$ flutter build web --profile --source-maps --web-renderer html
# 3. 启动 python3 内置的简易测试 Http Server
## 详情参考帮助 python3 -m http.server -h
$ python3 -m http.server 8000 -b 0.0.0.0 -d build/web

启动 nginx 反向代理服务

接下来,启动运行 nginx 反向代理服务。

$ cd ~/workspace
$ mkdir nginx && cd nginx
# touch nginx.conf
$ vim nginx.conf
  1. 在 nginx.conf 中增加反向代理转发配置:
    • 请自行根据本地 nginx 和配置所在目录,修改调整 error_log、access_log 等路径。
    • 需要为 nginx 设置访问coding cgi的身份认证信息(Cookie或Token),否则请求会返回403。
  2. 检测配置文件:nginx -t -c `pwd`/nginx.conf
  3. 启动nginx服务:nginx -p `pwd`/ -c nginx.conf
  4. 可借助 tail 命令滚动查看实时日志:tail -f nginx-access.log,以便排查问题
# workspace/nginx/nginx.conf

error_log /tmp/nginx-error.log info;

events {
    # worker 进程并发连接数
    worker_connections 256;
}

http {
    # for Linux : include /etc/nginx/mime.types;
    include /usr/local/etc/nginx/mime.types; # for macOS
    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 /tmp/nginx-access.log main;

    sendfile on;
    # tcp_nopush on;

    keepalive_timeout 65;

    server {
        server_name 127.0.0.1;
        listen 8080;

        # 根据不同的环境,设置不同的域名和认证cookie
        # 对应 flutter build、flutter run 指定的模式
        set $run_mode debug; # profile, release
        set $xHost host;
        set $xCookie cookie;
        set $xToken token;
        if ($run_mode = debug) {
            # 测试环境域名:xxx.testing.coding.net
            set $xHost xxx.testing.coding.net;
            # 包含认证信息的cookie请在桌面 Chrome 访问 coding 抓包获取
            ## 包含 host_key、uid、TCOA_TICKET、ERP_USERNAME、RIO_TCOA_TICKET、XSRF-TOKEN、_gid 等信息
            # set $xCookie "";
            # 或在CODING工作台-个人账号设置申请个人令牌,用作身份认证
            set $xToken "Token 69c1********************************ccb4";
        }

        if ($run_mode = profile) {
            # 正式环境域名:xxx.coding.net
            set $xHost xxx.coding.net;
            # 包含认证信息的cookie请在桌面 Chrome 访问 coding 抓包获取
            ## 包含 host_key、uid、TCOA_TICKET、ERP_USERNAME、RIO_TCOA_TICKET、XSRF-TOKEN、_gid 等信息
            # set $xCookie "";
            # 或在CODING工作台-个人账号设置里申请个人令牌,用作身份认证
            set $xToken "Token 2b57********************************fe89";
        }

        # cgi协议请求直接转发到 coding.woa.com
        location ^~ /api/ {
            proxy_set_header Host $xHost;
            # 设置 Cookie 或 Token 进行身份认证
            # proxy_set_header Cookie $xCookie;
            proxy_set_header Authorization $xToken;
            # 对于变量拼接的 proxy_pass 域名,需要指定 resolver
            # DNS服务器居家网络一般填写路由网关
            # resolver 192.168.0.1;
            # DNS服务器公司内请根据实际情况填写
            resolver 10.20.89.65 10.20.89.66;
            # 否则下面将报错 no resolver defined to resolve
            proxy_pass http://$xHost;
        }

        # 静态资源转发给本地 flutter web server
        location / {
            proxy_pass http://127.0.0.1:8010;
        }
    }
}

Shell 封装启动代理脚本

编写辅助启动 nginx reverse proxy 和 web app 的 Shell 脚本 launch_nginx.sh,主要功能是执行 nginx 命令启动反向代理、执行 flutter run 启动 web-server。

    # 从配置文件中读取配置
    get_env_config $mode $need_token

    # 兜底代理和服务监听端口
    get_local_port

    # 实例化nginx配置文件
    inst_nginx_conf

    # 启动 nginx 反向代理
    echo "nginx prefix = $nginx_prefix"
    nginx -p $nginx_prefix -c nginx_proxy.conf

    if [ $role = server ]; then
        echo -e "✅  server listening on \033[4mhttp://$lan_ip:$proxy_port\033[0m"
    elif [ $role = proxy ]; then
        echo -e "✅  proxy listening on \033[4mhttp://$lan_ip:$proxy_port\033[0m"
    fi

    echo "------------------------------------------------------------"

    if [ $role != proxy ]; then
        set -x
        # 移除 http.dart 中 baseUrl 的 /api/ 前缀
        flutter run --$mode -d web-server --web-renderer=html --web-port=$web_port --web-hostname=0.0.0.0 --dart-define=NGINX_REVERSE_PROXY=true
        set +x

        # 尝试杀死后台挂起的进程
        kill_run
    fi

help & usage

执行 launch_nginx.sh -h 可查看脚本帮助:

脚本启动角色(role)没有 -C 客户端模式,也支持 -S 启动纯代理模式,其他参数和 launch_shelf.sh 差不多。

$ ./scripts/proxy/launch_nginx.sh -h
launch_nginx.sh version: 1.0.0
Usage: launch_nginx.sh [-?hvSPdpr] [-n nginx-port] [-w web-port]

Options:
    -?,-h,--help            : show help and exit
    -v, --version           : show version and exit
    -S, --server            : start as server, default
    -P, --proxy             : start as proxy daemon
    -d, --debug             : run in debug mode, default
    -p, --profile           : run in profile mode
    -r, --release           : run in release mode
    -n, --nginx-port port   : config nginx proxy port, default 8080
    -w, --web-port port     : config flutter web port, default 8010

mode & conf

这一部分和 2-协议代理转发方案 中 launch_shelf.sh 的相关流程差不多,可对照参考,相比而言,多了一步 inst_nginx_conf

nginx 配置文件(nginx.conf)一般是静态写就,而我们希望启动nginx时可指定以下参数:

  • PROXY_PORT: 指定 nginx 反向代理监听端口;
  • CGI_HOST: 通过 proxy_set_header Host 设置CGI目标域名;
  • AUTH_TOKEN: 通过 proxy_set_header Authorization 设置认证token;
  • DNS_RESOLVER: 设置 resolver;
  • WEB_PORT: 静态资源转发 proxy_pass 拼接链接。

故抽象出 nginx 配置模板 nginx_proxy_tmpl.conf,将以上参数作为占位变量。

    # nginx_proxy_tmpl.conf
    server {
        server_name 127.0.0.1;
        listen PROXY_PORT;

        # 测试环境域名:xxx.testing.coding.net
        set $xHost CGI_HOST;
        # 包含认证信息的cookie请在桌面 Chrome 访问 coding 抓包获取
        ## 包含 host_key、uid、TCOA_TICKET、ERP_USERNAME、RIO_TCOA_TICKET、XSRF-TOKEN、_gid 等信息
        # set $xCookie "";
        # 或在CODING工作台-个人账号设置申请个人令牌,用作身份认证
        set $xToken "Token AUTH_TOKEN";

        # cgi协议请求直接转发到 coding.woa.com
        location ^~ /api/ {
            proxy_set_header Host $xHost;
            # 设置 Cookie 或 Token 进行身份认证
            # proxy_set_header Cookie $xCookie;
            proxy_set_header Authorization $xToken;
            # 对于变量拼接的 proxy_pass 域名,需要指定 resolver
            # prefs - network - Wi-Fi Advanced - DNS
            resolver DNS_RESOLVER;
            # 否则下面将报错 no resolver defined to resolve
            proxy_pass http://$xHost;
        }

        # 静态资源转发给本地 flutter web server
        location / {
            proxy_pass http://127.0.0.1:WEB_PORT;
        }
    }

在 sh 脚本的 inst_nginx_conf 函数中,复制一份模板以便实例化(如果目标文件已存在会覆盖掉)。
然后,通过 sed 命令替换模板文本文件中的相关占位变量完成nginx配置实例化。

# 替换nginx配置模板,实例化 /tmp/nginx/nginx_proxy.conf
inst_nginx_conf() {
    # nginx -p工作目录
    nginx_prefix=/tmp/nginx
    # 如果目录不存在先创建目录
    if ! [ -d $nginx_prefix ]; then
        mkdir $nginx_prefix
        echo "mkdir $nginx_prefix $?"
    fi
    local nginx_conf_inst=/tmp/nginx/nginx_proxy.conf
    # 复制一份模板以便实例化,如果目标文件已存在会覆盖掉
    cp scripts/proxy/nginx_proxy_tmpl.conf $nginx_conf_inst
    # prefs - network - Wi-Fi Advanced - DNS
    local dns_resolver=''
    dns_resolver=$(awk 'BEGIN {ORS=" "} /nameserver/{print $2}' /etc/resolv.conf)
    echo "dns_resolver = $dns_resolver" # 末尾多了一个空格
    # 替换模板中的宏变量,也可考虑基于 awk sub 实现
    sed -i '' "s#NGINX_PREFIX#$nginx_prefix#g" $nginx_conf_inst
    sed -i '' "s/PROXY_PORT/$proxy_port/g" $nginx_conf_inst
    sed -i '' "s/CGI_HOST/${api_base_url:?unset or null}/g" $nginx_conf_inst
    sed -i '' "s/AUTH_TOKEN/${auth_token:?unset or null}/g" $nginx_conf_inst
    sed -i '' "s/DNS_RESOLVER/$dns_resolver/g" $nginx_conf_inst
    sed -i '' "s/WEB_PORT/$web_port/g" $nginx_conf_inst
}

然后,执行 nginx -p $nginx_prefix -c nginx_proxy.conf 启动 nginx 反向代理。

-p 指定工作目录,nginx_prefix=/tmp/nginx,实例化的配置文件为 /tmp/nginx/nginx_proxy.conf。

run & debug

在项目根目录执行 sh 脚本,点击提示中的 server listening 局域网链接打开浏览器即可访问web服务。

局域网中的其他机器,也可以输入该url访问服务。

$ ./scripts/proxy/launch_nginx.sh

✅  server listening on http://10.20.89.64:8080

说明

反向代理启动的实际上是入口服务,其内部将静态资源访问代理到web服务,需要 flutter run -d web-server 启动 web 服务。
这与 vscode/Android Studio 启动的 client 模式(flutter run -d chrome)是两种不同的模式,故无法结合使用。

停止、重启 nginx 服务

Controlling NGINX Processes at Runtime

nginx can be controlled with signals.

To reload your configuration, you can stop or restart NGINX, or send signals to the master process. A signal can be sent by running the nginx command (invoking the NGINX executable) with the -s argument.

nginx -s <SIGNAL>

nginx -s 支持以下命令:

# Shut down gracefully (the `SIGQUIT` signal)
nginx -s quit
# Shut down immediately (or fast shutdown, the `SIGTERM` singal)
nginx -s stop
# Reload the configuration file (the `SIGHUP` signal)
nginx -s reload
# Reopen log files (the `SIGUSR1` signal)  
nginx -s reopen

除了 nginx -s 命令,还可以通过 kill 直接发送信号操控nginx进程。

The kill utility can also be used to send a signal directly to the master process.

kill -<SIG> <pid>

关于信号量,可以 man signal 查看说明文档。

The pid(process ID) of the master process is written, by default, to the nginx.pid file, which is located in the /usr/local/nginx/logs or /var/run directory.

关于 pid 的获取

The process ID of the master process is written to the file /usr/local/nginx/logs/nginx.pid by default. This name may be changed at configuration time, or in nginx.conf using the pid directive.

macOS 下通过 brew 安装的nginx 默认pid存储路径是 /usr/local/var/run/nginx.pid

若 nginx.conf 配置了主进程pid(master_pid)的存储路径,则可以从文件读取pid:

# 查看pid文件中存储的 master_pid
$ cat /usr/local/var/run/nginx.pid
7524

然后调用 kill 命令发送信号操控nginx进程:

# 从容停止主进程
kill -QUIT `cat /usr/local/var/run/nginx.pid`
# 快速停止主进程
kill -TERM `cat /usr/local/var/run/nginx.pid`
kill -INT `cat /usr/local/var/run/nginx.pid`
# 平滑重启
kill -HUP `cat /usr/local/var/run/nginx.pid`

除此之外,还可以通过 ps 等命令查找提取 nginx 进程id:

# 查询nginx进程
$ ps -lef | grep -i nginx:
  501  7524     1        4   0  31  0  4337568    692 -      Ss                  0 ??         0:00.00 nginx: master pr  7:32AM
  501  7525  7524        4   0  31  0  4337628    812 -      S                   0 ??         0:00.00 nginx: worker pr  7:32AM

# 查询提取nginx进程号
$ ps -lef | grep -i nginx: | awk '{ print $2}'
7524
7525
# 提取主进程 master_pid
$ ps -lef | grep -i nginx: | awk '{ print $2}' | sed -n '1p'
7524
# 提取主进程 master_pid
$ ps -lef | grep 'nginx:.*master' | awk '{ print $2}'
7524

然后调用 kill 命令发送信号操控nginx进程:

# 强制停止名称包含nginx的进程
pkill -KILL nginx
# 强制停止所有nginx进程
ps -lef | grep -i nginx: | awk '{ print $2}' | xargs kill -KILL

参考:nginx启动、重启、关闭How to stop nginx on Mac OS X

proxy_temp 目录权限问题

手机端局域网连接调试,js、image、font等加载很慢,甚至加载不出来。

tail -f nginx-error.log 滚动日志看到报错,提示用户没有对 proxy_temp 目录的写操作权限。

2022/01/27 14:03:36 [crit] 33301#0: *8743 open() "/usr/local/var/run/nginx/proxy_temp/0/02/0000000020" failed (13: Permission denied) while reading upstream, client: 127.0.0.1, server: 127.0.0.1, request: "GET /dart_sdk.js HTTP/1.1", upstream: "http://127.0.0.1:8000/dart_sdk.js", host: "127.0.0.1:8080", referrer: "http://127.0.0.1:8080/"

nginx 反向代理默认开启了 proxy_buffering 功能:当加载的文件超过 proxy_temp_file_write_size 所设置的值时,nginx worker process 会将文件写入 proxy_temp 文件夹中。

方案1:在 nginx.conf 中禁用掉 http 的 proxy_buffering 功能:

    # 禁用对被代理的服务器的应答缓冲
    proxy_buffering off;

方案2:改变工作进程用户(组)对 proxy_temp 目录的权限或所属关系,使得有权写入buffer。

当 nginx.conf 中没有指定 user 时,工作进程默认的用户组和用户是 nobody nobody。

ls -l 查看 proxy_temp 父目录权限,可知 proxy_temp 目录的属主为 nobody:admin :

$ ls -l /usr/local/var/run/nginx/           
total 0
drwx------   2 nobody  admin   64 Jan 25 23:07 client_body_temp
drwx------   2 nobody  admin   64 Jan 25 23:07 fastcgi_temp
drwx------  12 nobody  admin  384 Jan 27 07:18 proxy_temp
drwx------   2 nobody  admin   64 Jan 25 23:07 scgi_temp
drwx------   2 nobody  admin   64 Jan 25 23:07 uwsgi_temp

通过 ps 查询工作进程所属的用户为当前用户(whoami):

$ ps aux | grep "nginx: worker process" | grep -v grep | awk '{ print $1}'
faner

而当前用户没有权限查看 proxy_temp 目录的:

$ ls -l /usr/local/var/run/nginx/proxy_temp/
ls: : Permission denied

进一步查看当前用户的用户(组)信息:

$ id `whoami` | awk 'BEGIN {RS=" ";} NR<=2 {print}'
uid=501(faner)
gid=20(staff)

执行以下脚本,使工作线程的用户组接管 proxy_temp 目录:

$ worker_user_id=`ps aux | grep "nginx: worker process" | grep -v grep | awk '{ print $1}'`
$ worker_user_group=`groups $current_user_id | awk '{print $1}'`
$ sudo chown -R $worker_user_id:$worker_user_group /usr/local/var/run/nginx/proxy_temp/
$ sudo nginx -s reload

平滑重启nginx后,局域网网络请求畅通,nginx-error.log 中日志显示 proxy buffer 运行正常:

2022/01/27 16:28:21 [warn] 63716#0: *9924 an upstream response is buffered to a temporary file /usr/local/var/run/nginx/proxy_temp/0/04/0000000040 while reading upstream, client: 192.168.0.113, server: 127.0.0.1, request: "GET /dart_sdk.js HTTP/1.1", upstream: "http://127.0.0.1:8000/dart_sdk.js", host: "192.168.0.106:8080", referrer: "http://192.168.0.106:8080/"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值