GET和POST


 

w3school給出的比較

下面的表格比較了兩種 HTTP 方法:GET 和 POST。

  GET POST
后退按鈕/刷新 無害 數據會被重新提交(瀏覽器應該告知用戶數據會被重新提交)。
書簽 可收藏為書簽 不可收藏為書簽
緩存 能被緩存 不能緩存
編碼類型 application/x-www-form-urlencoded application/x-www-form-urlencoded 或 multipart/form-data。為二進制數據使用多重編碼。
歷史 參數保留在瀏覽器歷史中。 參數不會保存在瀏覽器歷史中。
對數據長度的限制 是的。當發送數據時,GET 方法向 URL 添加數據;URL 的長度是受限制的(URL 的最大長度是 2048 個字符)。 無限制。
對數據類型的限制 只允許 ASCII 字符。 沒有限制。也允許二進制數據。
安全性 與 POST 相比,GET 的安全性較差,因為所發送的數據是 URL 的一部分。在發送密碼或其他敏感信息時絕不要使用 GET ! POST 比 GET 更安全,因為參數不會被保存在瀏覽器歷史或 web 服務器日志中。
可見性 數據在 URL 中對所有人都是可見的。 數據不會顯示在 URL 中。

 

 

從HTTP本身來講GET與POST的區別

征求意見稿(英語:Request For Comments,縮寫為RFC),是由互聯網工程任務組(IETF)發布的一系列備忘錄。

RFC7231里定義了HTTP方法的幾個性質:

  1. Safe - 安全 這里的「安全」和通常理解的「安全」意義不同,如果一個方法的語義在本質上是「只讀」的,那么這個方法就是安全的。客戶端向服務端的資源發起的請求如果使用了是安全的方法,就不應該引起服務端任何的狀態變化,因此也是無害的。 此RFC定義,GET, HEAD, OPTIONS 和 TRACE 這幾個方法是安全的。 但是這個定義只是規范,並不能保證方法的實現也是安全的,服務端的實現可能會不符合方法語義,正如上文說過的使用GET修改用戶信息的情況。 引入安全這個概念的目的是為了方便網絡爬蟲和緩存,以免調用或者緩存某些不安全方法時引起某些意外的后果。User Agent(瀏覽器)應該在執行安全和不安全方法時做出區分對待,並給用戶以提示。

  2. Idempotent - 冪等 冪等的概念是指同一個請求方法執行多次和僅執行一次的效果完全相同。按照RFC規范,PUT,DELETE和安全方法都是冪等的。同樣,這也僅僅是規范,服務端實現是否冪等是無法確保的。 引入冪等主要是為了處理同一個請求重復發送的情況,比如在請求響應前失去連接,如果方法是冪等的,就可以放心地重發一次請求。這也是瀏覽器在后退/刷新時遇到POST會給用戶提示的原因:POST語義不是冪等的,重復請求可能會帶來意想不到的后果。

  3. Cacheable - 可緩存性 顧名思義就是一個方法是否可以被緩存,此RFC里GET,HEAD和某些情況下的POST都是可緩存的,但是絕大多數的瀏覽器的實現里僅僅支持GET和HEAD。關於緩存的更多內容可以去看RFC7234。

 

語法與語義

對語法和語義的理解。語法是和文法結構有關,然而語義是和按照這個結構所組合的單詞符號的意義有關。合理的語法結構並不表明語義是合法的。例如我們常說:我上大學,這個句子是符合語法規則的,也符合語義規則。但是大學上我,雖然符合語法規則,但沒有什么意義,所以說是不符合語義的。

HTTP請求報文的格式:

從語法上來說,只要符合該格式的請求就是符合語法的。語義上來說,GET的語義就是「獲取資源」,POST的語義是「處理資源」,那么在具體實現這兩個方法時,就必須考慮其語義,做出符合其語義的行為,如果使用GET方法創建用戶信息,POST獲取資源列表,這樣就只能說這個請求是「合法」的,但不是「符合語義」的。

 

瀏覽器的GET和POST

這里特指瀏覽器中Ajax的HTTP請求,即從HTML和瀏覽器誕生就一直使用的HTTP協議中的GET/POST。瀏覽器用GET請求來獲取一個html頁面/圖片/css/js等資源;用POST來提交一個<form>表單,並得到一個結果的網頁。

GET

“讀取“一個資源。比如Get到一個html文件。反復讀取不應該對訪問的數據有副作用。比如”GET一下,用戶就下單了,返回訂單已受理“,這是不可接受的。沒有副作用被稱為“冪等“。

POST

在頁面里<form> 標簽會定義一個表單。點擊其中的submit元素會發出一個POST請求讓服務器做一件事。這件事往往是有副作用的,不冪等的。不冪等也就意味着不能隨意多次執行。因此也就不能緩存,不能保存書簽等。

 

GET和POST攜帶數據的格式也有區別。當瀏覽器發出一個GET請求時,就意味着要么是用戶自己在瀏覽器的地址欄輸入,要不就是點擊了html里a標簽的href中的url。所以其實並不是GET只能用url,而是瀏覽器直接發出的GET只能由一個url觸發。所以沒辦法,GET上要在url之外帶一些參數就只能依靠url上附帶querystring。但是HTTP協議本身並沒有這個限制。

瀏覽器的POST請求都來自表單提交。每次提交,表單的數據被瀏覽器用編碼到HTTP請求的body里。瀏覽器發出的POST請求的body主要有有兩種格式,一種是application/x-www-form-urlencoded用來傳輸簡單的數據,大概就是"key1=value1&key2=value2"這樣的格式。另外一種是傳文件,會采用multipart/form-data格式。采用后者是因為application/x-www-form-urlencoded的編碼方式對於文件這種二進制的數據非常低效。

 

接口中的GET和POST

這里是指通過瀏覽器的Ajax api,或者iOS/Android的App的http client,java的commons-httpclient/okhttp或者是curl,postman之類的工具發出來的GET和POST請求。

當用HTTP實現接口發送請求時,就沒有瀏覽器中那么多限制了,只要是符合HTTP格式的就可以發。其中的「請求方法」可以是GET也可以是POST,或者其他的HTTP Method,如PUT、DELETE、OPTION……。從協議本身看,並沒有什么限制說GET一定不能沒有body,POST就一定不能把參放到<URL>的querystring上。因此其實可以更加自由的去利用格式。比如Elastic Search的_search api就用了帶body的GET;也可以自己開發接口讓POST一半的參數放在url的querystring里,另外一半放body里;你甚至還可以讓所有的參數都放Header里——可以做各種各樣的定制,只要請求的客戶端和服務器端能夠約定好。

當然,太自由也帶來了另一種麻煩,於是就有了一些列接口規范/風格。其中名氣最大的當屬REST。REST充分運用GET、POST、PUT和DELETE,約定了這4個接口分別獲取、創建、替換和刪除“資源”,REST最佳實踐還推薦在請求體使用json格式。這樣僅僅通過看HTTP的method就可以明白接口是什么意思,並且解析格式也得到了統一。

json相對於x-www-form-urlencoded的優勢在於1)可以有嵌套結構;以及 2)可以支持更豐富的數據類型。通過一些框架,json可以直接被服務器代碼映射為業務實體。用起來十分方便。但是如果是寫一個接口支持上傳文件,那么還是multipart/form-data格式更合適。

 

REST接口規范

在REST中, 【GET】 + 【資源定位符】被專用於獲取資源或者資源列表,如下。與瀏覽器的場景類似,REST GET也不應該有副作用,於是可以被反復無腦調用。

 GET http://foo.com/books          獲取書籍列表
 GET http://foo.com/books/:bookId 根據bookId獲取一本具體的書

REST 【POST】+ 【資源定位符】則用於“創建一個資源”,如下。這里就能留意到瀏覽器中用來實現表單提交的POST,和REST里實現創建資源的POST語義上的不同。

 POST http://foo.com/books
 {
  "title": "西游記",
  "author": "施耐庵",
  ...
 }

REST POST和REST PUT的區別有些api是使用PUT作為創建資源的Method。PUT與POST的區別在於,PUT的實際語義是“replace”replace。REST規范里提到PUT的請求體應該是完整的資源,包括id在內。比如上面的創建一本書的api也可以定義為:

 PUT http://foo.com/books
 {
 "id": "BOOK:affe001bbe0556a",
 "title": "西游記",
 "author": "施耐庵",
 ...
 }

服務器應該先根據請求提供的id進行查找,如果存在一個對應id的元素,就用請求中的數據整體替換已經存在的資源;如果沒有,就用“把這個id對應的資源從【空】替換為【請求數據】“。直觀看起來就是“創建”了。與PUT相比,POST更像是一個“factory”,通過一組必要的數據創建出完整的資源。

 

 

 

關於安全性

我們常聽到GET不如POST安全,因為POST用body傳輸數據,而GET用url傳輸,更加容易看到。但是從攻擊的角度,無論是GET還是POST都不夠安全,因為HTTP本身是明文協議每個HTTP請求和返回的每個byte都會在網絡上明文傳播,不管是url,header還是body。這完全不是一個“是否容易在瀏覽器地址欄上看到“的問題。

為了避免傳輸中數據被竊取,必須做從客戶端到服務器的端端加密。業界的通行做法就是https——即用SSL協議協商出的密鑰加密明文的http數據。這個加密的協議和HTTP協議本身相互獨立。如果是利用HTTP開發公網的站點/App,要保證安全,https是最最基本的要求。

當然,端端加密並不一定非得用https。比如國內金融領域都會用私有網絡,也有GB的加密協議SM系列。但除了軍隊,金融等特殊機構之外,似乎並沒有必要自己發明一套類似於ssl的協議。

回到HTTP本身,的確GET請求的參數更傾向於放在url上,因此有更多機會被泄漏。比如攜帶私密信息的url會展示在地址欄上,還可以分享給第三方,就非常不安全了。此外,從客戶端到服務器端,有大量的中間節點,包括網關,代理等。他們的access log通常會輸出完整的url,比如nginx的默認access log就是如此。如果url上攜帶敏感數據,就會被記錄下來。但請注意,就算私密數據在body里,也是可以被記錄下來的,因此如果請求要經過不信任的公網,避免泄密的唯一手段就是https。這里說的“避免access log泄漏“僅僅是指避免可信區域中的http代理的默認行為帶來的安全隱患。比如你是不太希望讓自己公司的運維同學從公司主網關的log里看到用戶的密碼吧。

另外,上面講過,如果是用作接口,GET實際上也可以帶body,POST也可以在url上攜帶數據。所以實際上到底怎么傳輸私密數據,要看具體場景具體分析。當然,絕大多數場景,用POST + body里寫私密數據是合理的選擇。一個典型的例子就是“登錄”:

 POST http://foo.com/user/login
 {
  "username": "dakuankuan",
  "passowrd": "12345678"
 }

安全是一個巨大的主題,有由很多細節組成的一個完備體系,比如返回私密數據的mask,XSS,CSRF,跨域安全,前端加密,釣魚,salt,…… POST和GET在安全這件事上僅僅是個小角色。因此單獨討論POST和GET本身哪個更安全意義並不是太大。只要記得一般情況下,私密數據傳輸用POST + body就好。

關於編碼

常見的說法有,比如GET的參數只能支持ASCII,而POST能支持任意binary,包括中文。但其實從上面可以看到,GET和POST實際上都能用url和body。因此所謂編碼確切地說應該是http中url用什么編碼,body用什么編碼。

先說下url。url只能支持ASCII的說法源自於RFC1738

Thus, only alphanumerics, the special characters "$-_.+!*'(),", and reserved characters used for their reserved purposes may be used unencoded within a URL.

實際上這里規定的僅僅是一個ASCII的子集[a-zA-Z0-9$-_.+!*'(),]。它們是可以“不經編碼”在url中使用。比如盡管空格也是ASCII字符,但是不能直接用在url里。

那這個“編碼”是什么呢?如果有了特殊符號和中文怎么辦呢?一種叫做percent encoding的編碼方法就是干這個用的:https://en.wikipedia.org/wiki/Percent-encodingen.wikipedia.org

這也就是為啥我們偶爾看到url里有一坨%和16位數字組成的序列。

使用Percent Encoding,即使是binary data,也是可以通過編碼后放在URL上的。

但要特別注意,這個編碼方式只管把字符轉換成URL可用字符,但是卻不管字符集編碼(比如中文到底是用UTF8還是GBK)這塊早期一直都相當亂,也沒有什么統一規范。比如有時跟網頁編碼一樣,有的是操作系統的編碼一樣。最要命的是瀏覽器的地址欄是不受開發者控制的。這樣,對於同樣一個帶中文的url,如果有的瀏覽器一定要用GBK(比如老的IE8),有的一定要用UTF8(比如chrome)。后端就可能認不出來。對此常用的辦法是避免讓用戶輸入這種帶中文的url。如果有這種形式的請求,都改成用戶界面上輸入,然后通過Ajax發出的辦法。Ajax發出的編碼形式開發者是可以100%控制的。

不過目前基本上utf8已經大一統了。現在的開發者除非是被國家規定要求一定要用GB系列編碼的場景,基本上不會再遇到這類問題了。

關於url的編碼,阮一峰的一篇文章有比較詳細的解釋:http://www.ruanyifeng.com/blog/2010/02/url_encoding.html

順便說一句,盡管在瀏覽器地址欄可以看到中文。但這種url在發送請求過程中,瀏覽器會把中文用字符編碼+Percent Encode翻譯為真正的url,再發給服務器。瀏覽器地址欄里的中文只是想讓用戶體驗好些而已。

再討論下Body。HTTP Body相對好些,因為有個Content-Type來比較明確的定義。比如:

 POST xxxxxx HTTP/1.1
 ...
 Content-Type: application/x-www-form-urlencoded ; charset=UTF-8

這里Content-Type會同時定義請求body的格式(application/x-www-form-urlencoded)和字符編碼(UTF-8)。

所以body和url都可以提交中文數據給后端,但是POST的規范好一些,相對不容易出錯,容易讓開發者安心。對於GET+url的情況,只要不涉及到在老舊瀏覽器的地址欄輸入url,也不會有什么太大的問題。

回到POST,瀏覽器直接發出的POST請求就是表單提交,而表單提交只有application/x-www-form-urlencoded針對簡單的key-value場景;和multipart/form-data,針對只有文件提交,或者同時有文件和key-value的混合提交表單的場景。

如果是Ajax或者其他HTTP Client發出去的POST請求,其body格式就非常自由了,常用的有json,xml,文本,csv……甚至是你自己發明的格式。只要前后端能約定好即可。

瀏覽器的POST需要發兩個請求嗎?

上文中的"HTTP 格式“清楚的顯示了HTTP請求可以被大致分為“請求頭”和“請求體”兩個部分。使用HTTP時大家會有一個約定,即所有的“控制類”信息應該放在請求頭中,具體的數據放在請求體里“。於是服務器端在解析時,總是會先完全解析全部的請求頭部。這樣,服務器端總是希望能夠了解請求的控制信息后,就能決定這個請求怎么進一步處理,是拒絕,還是根據content-type去調用相應的解析器處理數據,或者直接用zero copy轉發。

比如在用Java寫服務時,請求處理代碼總是能從HttpSerlvetRequest里getParameter/Header/url。這些信息都是請求頭里的,框架直接就解析了。而對於請求體,只提供了一個inputstream,如果開發人員覺得應該進一步處理,就自己去讀取和解析請求體。這就能體現出服務器端對請求頭和請求體的不同處理方式。

舉個實際的例子,比如寫一個上傳文件的服務,請求url中包含了文件名稱,請求體中是個尺寸為幾百兆的壓縮二進制流。服務器端接收到請求后,就可以先拿到請求頭部,查看用戶是不是有權限上傳,文件名是不是符合規范等。如果不符合,就不再處理請求體的數據了,直接丟棄。而不用等到整個請求都處理完了再拒絕。

為了進一步優化,客戶端可以利用HTTP的Continued協議來這樣做:客戶端總是先發送所有請求頭給服務器,讓服務器校驗。如果通過了,服務器回復“100 - Continue”,客戶端再把剩下的數據發給服務器。如果請求被拒了,服務器就回復個400之類的錯誤,這個交互就終止了。這樣,就可以避免浪費帶寬傳請求體。但是代價就是會多一次Round Trip。如果剛好請求體的數據也不多,那么一次性全部發給服務器可能反而更好。

基於此,客戶端就能做一些優化,比如內部設定一次POST的數據超過1KB就先只發“請求頭”,否則就一次性全發。客戶端甚至還可以做一些Adaptive的策略,統計發送成功率,如果成功率很高,就總是全部發等等。不同瀏覽器,不同的客戶端(curl,postman)可以有各自的不同的方案。不管怎樣做,優化目的總是在提高數據吞吐和降低帶寬浪費上做一個折衷。

因此到底是發一次還是發N次,客戶端可以很靈活的決定。因為不管怎么發都是符合HTTP協議的,因此我們應該視為這種優化是一種實現細節,而不用扯到GET和POST本身的區別上。更不要當個什么世紀大發現。

到底什么算請求體

看完了上面的內容后,讀者也許會對“什么是請求體”感到困惑不已,比如x-www-form-endocded編碼的body算不算“請求體”呢?

從HTTP協議的角度,“請求頭”就是Method + URL(含querystring)+ Headers;再后邊的都是請求體。

但是從業務角度,如果你把一次請求立即為一個調用的話。比如上面的

 POST http://foo.com/books
 {
  "title": "西游記",
  "author": "施耐庵",
  ...
 }

那么這一行函數名和兩個參數都可以看作是一個請求,不區分頭和體。即便用HTTP協議實現,title和author編碼到了HTTP請求體中。Java的HttpServletRequest支持用getParameter方法獲取x-www-url-form-encoded中的數據,表達的意思就是“請求“的”參數“。

對於HTTP,需要區分【頭】和【體】,Http Request和Http Response都這么區分。Http這么干主要用作

  • 對於HTTP代理

    • 支持轉發規則,比如nginx先要解析請求頭,拿到URL和Header才能決定怎么做(轉發proxy_pass,重定向redirect,rewrite后重新判斷……)

    • 需要用請求頭的信息記錄log。盡管請求體里的數據也可以記錄,但一般只記錄請求頭的部分數據。

    • 如果代理規則不涉及到請求體,那么請求體就可以不用從內核態的page cache復制一份到用戶態了,可以直接zero copy轉發。這對於上傳文件的場景極為有效。

    • ……

  • 對於HTTP服務器

    • 可以通過請求頭進行ACL控制,比如看看Athorization頭里的數據是否能讓認證通過

    • 可以做一些攔截,比如看到Content-Length里的數太大,或者Content-Type自己不支持,或者Accept要求的格式自己無法處理,就直接返回失敗了。

    • 如果body的數據很大,利用Stream API,可以方便支持一塊一塊的處理數據,而不是一次性全部讀取出來再操作,以至於占用大量內存。

    • ……

但從高一級的業務角度,我們在意的其實是【請求】和【返回】。當我們在說“請求頭”這三個字時,也許實際的意思是【請求】。而用HTTP實現【請求】時,可能僅僅用到【HTTP的請求頭】(比如大部分GET請求),也可能是【HTTP請求頭】+【HTTP請求體】(比如用POST實現一次下單)。

總之,這里有兩層,不要混哦。

關於URL的長度

因為上面提到了不論是GET和POST都可以使用URL傳遞數據,所以我們常說的“GET數據有長度限制“其實是指”URL的長度限制“。

HTTP協議本身對URL長度並沒有做任何規定。實際的限制是由客戶端/瀏覽器以及服務器端決定的。

先說瀏覽器。不同瀏覽器不太一樣。比如我們常說的2048個字符的限制,其實是IE8的限制。並且原始文檔的說的其實是“URL的最大長度是2083個字符,path的部分最長是2048個字符“。見https://support.microsoft.com/en-us/help/208427/maximum-url-length-is-2-083-characters-in-internet-explorer。IE8之后的IE URL限制我沒有查到明確的文檔,但有些資料稱IE 11的地址欄只能輸入法2047個字符,但是允許用戶點擊html里的超長URL。我沒實驗,哪位有興趣可以試試。

Chrome的URL限制是2MB,見https://chromium.googlesource.com/chromium/src/+/master/docs/security/url_display_guidelines/url_display_guidelines.md

Safari,Firefox等瀏覽器也有自己的限制,但都比IE大的多,這里就不挨個列出了。

然而新的IE已經開始使用Chrome的內核了,也就意味着“瀏覽器端URL的長度限制為2048字符”這種說法會慢慢成為歷史。

其他的客戶端,比如Java的,js的http client大多數也並沒有限制URL最大有多長。

除了瀏覽器,服務器這邊也有限制,比如apache的LimieRequestLine指令。

apache實際上限制的是HTTP請求第一行“Request Line“的長度,即<METHOD><URL> <VERSION>那一行。

再比如nginx用large_client_header_buffers 指令來分配請求頭中的很長數據的buffer。這個buffer可以用來處理url,header value等。

Tomcat的限制是web.xml里maxHttpHeaderSize來設置的,控制的是整個“請求頭”的總長度。

為啥要限制呢?如果寫過解析一段字符串的代碼就能明白,解析的時候要分配內存。對於一個字節流的解析,必須分配buffer來保存所有要存儲的數據。而URL這種東西必須當作一個整體看待,無法一塊一塊處理,於是就處理一個請求時必須分配一整塊足夠大的內存。如果URL太長,而並發又很高,就容易擠爆服務器的內存;同時,超長URL的好處並不多,我也只有處理老系統的URL時因為不敢碰原來的邏輯,又得追加更多數據,才會使用超長URL。

對於開發者來說,使用超長的URL完全是給自己埋坑,需要同時要考慮前后端,以及中間代理每一個環節的配置。此外,超長URL會影響搜索引擎的爬蟲,有些爬蟲甚至無法處理超過2000個字節的URL。這也就意味着這些URL無法被搜到,坑爹啊。

其實並沒有太大必要弄清楚精確的URL最大長度限制。我個人的經驗是,只要某個要開發的資源/api的URL長度有可能達到2000個bytes以上,就必須使用body來傳輸數據,除非有特殊情況。至於到底是GET + body還是POST + body可以看情況決定。

留意,1個漢字字符經過UTF8編碼 + percent encoding后會變成9個字節,別算錯哦。

 

 

參考鏈接:

https://www.zhihu.com/question/28586791/answer/145424285

https://www.zhihu.com/question/28586791/answer/767316172

https://www.w3school.com.cn/tags/html_ref_httpmethods.asp

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM