前段時間出現了一個請求在測試環境簽名成功,在線上環境簽名失敗的狀況,排查緣由是線上url
中有雙斜槓會被合併成一個傳給後端,在測試環境中不會出現。這個就比較神奇了,Nginx 版本徹底同樣。前端
方式是抓包確認:在線上Nginx
和測試Nginx
抓包,對比 如下例子中nginx
218.218.218.218
是線上服務器Nginx
的ip
121.121.121.121
是本身電腦出口ip
10.0.0.1
是線上Nginx
的局域網ip
10.0.0.2
是 Java 業務機的局域網 ip1. 從本身電腦到線上Nginx的包以下:
17:41:47.110728 IP 121.121.121.121.50935 > 218.218.218.218.80: Flags [P.]
GET /easicar/v1//subCourses/9952078022974031963e5d9a399e9958/text?subCourseId=9952078022974031963e5d9a399e9958 HTTP/1.1
Host: masaike.seewo.com
User-Agent: curl/7.54.0
Accept: */*
2. Nginx到後端的請求以下
17:41:47.113138 IP 10.0.0.1.49610 > 10.0.0.2.40088: Flags [P.]
GET /easicar/v1/subCourses/9952078022974031963e5d9a399e9958/text?subCourseId=9952078022974031963e5d9a399e9958 HTTP/1.1
x-ccloud-pre: 1
X-Forwarded-Url: http://masaike.seewo.com/easicare/v1//subCourses/9952078022974031963e5d9a399e9958/text?subCourseId=9952078022974031963e5d9a399e9958
Host: masaike.seewo.com
X-Real-IP: 121.121.121.121
X-Forwarded-For: 121.121.121.121
X-Forwarded-Proto: http
User-Agent: curl/7.54.0
Accept: */*
複製代碼
能夠看到Nginx
轉發到後端Java
這裏的時候,/easicar/v1//subCourses/
已經沒有兩個斜槓了,可是測試環境轉到後端的時候是有的,這裏就不貼包內容了。後端
本身在本地測試了好久,發現都不會合並多餘的/
,決定debug
一下Nginx
的源碼看看tomcat
環境:Mac+Clion
最終跟進了代碼:src/http/modules/ngx_http_proxy_module.c
的ngx_http_proxy_create_request
函數bash
下面這段代碼是生成轉發給upstream
的http
包服務器
// 拷貝'GET'
b->last = ngx_copy(b->last, method.data, method.len);
*b->last++ = ' ';
u->uri.data = b->last;
// 拷貝uri,核心差異就在這裏
// 若是unparsed_uri=1,url部分就使用unparsed_uri.data,就是沒有合併斜槓的url
// 若是unparsed_uri=0,url部分就使用uri.data,就是合併過斜槓的url
if (plcf->proxy_lengths && ctx->vars.uri.len) {
b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);
} else if (unparsed_uri) {
// 若是unparsed_uri=1,url使用unparsed_uri.data
b->last = ngx_copy(b->last, r->unparsed_uri.data, r->unparsed_uri.len);
} else {
if (r->valid_location) {
b->last = ngx_copy(b->last, ctx->vars.uri.data, ctx->vars.uri.len);
}
if (escape) {
ngx_escape_uri(b->last, r->uri.data + loc_len,
r->uri.len - loc_len, NGX_ESCAPE_URI);
b->last += r->uri.len - loc_len + escape;
} else {
// 若是unparsed_uri=0,url使用uri.data,uri.data是合併過的url
b->last = ngx_copy(b->last, r->uri.data + loc_len,
r->uri.len - loc_len);
}
// 這裏是拼接querystring
if (r->args.len > 0) {
*b->last++ = '?';
b->last = ngx_copy(b->last, r->args.data, r->args.len);
}
}
複製代碼
那麼unparsed_uri
這個標記位怎麼來的? ctx->vars.uri.len == 0
的狀況下會置位1
,vars.uri
的值的含義是Nginx配置文件中proxy_pass server
後面那段 好比proxy_pass http://my-tomcat-server;
那麼vars.uri
值是NULL
好比proxy_pass http://my-tomcat-server/nimei;
那麼vars.uri
值是/nimei
curl
unparsed_uri = 0;
if (plcf->proxy_lengths && ctx->vars.uri.len) {
uri_len = ctx->vars.uri.len;
} else if (ctx->vars.uri.len == 0 && r->valid_unparsed_uri && r == r->main)
{
// ctx->vars.uri.len == 0 的狀況下會置位1
unparsed_uri = 1;
uri_len = r->unparsed_uri.len;
} else {
loc_len = (r->valid_location && ctx->vars.uri.len) ?
plcf->location.len : 0;
if (r->quoted_uri || r->space_in_uri || r->internal) {
escape = 2 * ngx_escape_uri(NULL, r->uri.data + loc_len,
r->uri.len - loc_len, NGX_ESCAPE_URI);
}
uri_len = ctx->vars.uri.len + r->uri.len - loc_len + escape
+ sizeof("?") - 1 + r->args.len;
}
複製代碼
回過來看這個問題,就很簡單了函數
線上環境配置
location /easicar {
proxy_pass http://easicar/easicar;
測試環境配置
location / {
proxy_pass http://nginx-ingress;
複製代碼
線上配置server
後面多了/easicar
,會走unparsed_uri=0
的邏輯,會使用合併過/的url,測試環境server
後面是空的,會走unparsed_uri=1
的邏輯,會不合並url測試
還有一個問題,merge_slashes
這個指令有什麼用?merge_slashes這個指令默認是開的,會決定會不會自動合併uri中的/
,決定了uri這個基礎,會不會有第一步合併這一步編碼
merge_slashes | proxy_pass後有無url | 最終轉發url是否合併/ |
---|---|---|
on | 有 | 合併/ |
on | 無 | 不合並/ |
off | 有 | 不合並/ |
off | 無 | 不合並/ |
這個問題看起來表面上隻影響了雙斜槓的問題,實際上不少地方都有影響,好比剛恰好線上又出現了一塊兒問題 請求是: /easicar/v1/subCourses/{subCourseId}/comments/create 由於前端問題{subCourseId},沒有用值覆蓋它,在線上不正常,HTTP 狀態碼返回 400,在測試環境正常 。仍是由於那個問題致使的。
實驗結果以下
一、server 後面有內容的時候 (模擬線上狀況)
proxy_pass http://my-tomcat-server/nimei
客戶端請求到 Nginx
19:03:01.396763 IP 127.0.0.1.61759 > 127.0.0.1.8080: Flags [P.]
POST /apm-demo-server/%7Bfoo%7D//bar HTTP/1.1
Content-Type: text/plain; charset=utf-8
Nginx請求到upstream
19:03:01.398280 IP6 ::1.61760 > ::1.8111: Flags [P.]
POST /nimei/{foo}/bar HTTP/1.1
X-Forwarded-Url: http://ya-dev.test.xiwo.com/apm-demo-server/%7Bfoo%7D//bar
複製代碼
能夠看到轉發到後端服務器那裏的時候已是解碼過的{foo}
二、server 後面沒有內容的時候 (模擬測試環境)
proxy_pass http://my-tomcat-server;
客戶端請求到 Nginx
19:16:37.949701 IP 127.0.0.1.62054 > 127.0.0.1.8080: Flags [P.]
POST /apm-demo-server/%7Bfoo%7D//bar HTTP/1.1
Nginx請求到upstream
19:16:37.953191 IP6 ::1.62055 > ::1.8111: Flags [P.]
POST /apm-demo-server/%7Bfoo%7D//bar HTTP/1.1
X-Forwarded-Url: http://ya-dev.test.xiwo.com/apm-demo-server/%7Bfoo%7D//bar
複製代碼
能夠看到轉發到後端服務器那裏的時候已是未解碼過的 %7Bfoo%7D Nginx 在 server 後面 uri 不爲空的時候,會把 url 解碼、合併斜杆後的 url 傳給 upstream 服務器,可是 tomcat 會拒絕掉部分未經編碼的他以爲不合法的字符。不管發起方編碼的多麼好,都會有問題
解決辦法有兩種:
那麼哪些是http協議認爲的合法的字符呢? 寫了一段代碼,通過一層Nginx
,轉發到tomcat
,遍歷了0-127的全部字符,如下字符是必定不被tomcat
容許的
" 0x22 < 0x3c > 0x3e ^ 0x5e ` 0x60 { 0x7b | 0x7c } 0x7d 複製代碼
看tomcat
源碼也能夠知道
if (IS_CONTROL[i] || i > 127 ||
i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
i == '^' || i == '`' || i == '{' || i == '|' || i == '}') {
if (!REQUEST_TARGET_ALLOW[i]) {
IS_NOT_REQUEST_TARGET[i] = true;
}
}
複製代碼
具體能夠參考:RFC 7230 和 RFC 3986