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 请求和静态网页资源转发到不同的目标服务器。
- 将静态资源请求 proxy_pass 给本地 web 服务器(8000)。
- 将 cgi 请求直接 proxy_pass 给目标服务器,从而解决跨域问题。
修改代码中的 cgi 域名
代码中根据运行模式,设置了不同的域名:
- debug: 正式prod域名;
- profile:测试testing域名;
- release:线上智能网关域名;
首先,修改涉及跨域问题的 cgi 请求,将三个环境的绝对 url 地址改为相对地址。
release 模式下的 web socket url 域名改为 coding.net 正式环境域名,同 debug 模式。
在 shelf_proxy 方案中,flutter run 通过 --dart-define
传入参数,替换占位变量 apiBaseUrl
和 authToken
,如下图分支 ①。
在 nginx 反向代理方案中,给 flutter run 传递一个 --dart-define=NGINX_REVERSE_PROXY=true
参数,Dart 代码解析以确定是走 nginx 反向代理。
由于访问的url和port即是nginx代理服务器,协议是直接同域转发,故将 baseUrl 的 host 部分移除改为相对path,如下图分支 ②。
启动 flutter web server
修改好 cig 域名路径后,启动 flutter web 服务器。
- 方案一:执行
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
- 方案二:执行
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
- 在 nginx.conf 中增加反向代理转发配置:
- 请自行根据本地 nginx 和配置所在目录,修改调整 error_log、access_log 等路径。
- 需要为 nginx 设置访问coding cgi的身份认证信息(Cookie或Token),否则请求会返回403。
- 检测配置文件:nginx -t -c `pwd`/nginx.conf
- 启动nginx服务:nginx -p `pwd`/ -c nginx.conf
- 可借助 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/"