1、“Content-Security-Policy”頭缺失
在網上查了關于這個響應頭的說明,CSP相當于前台的白名單,用來限制網站内部一些資源擷取的來源,如限制CSS、JS、圖檔或者第三方連結等。
CSP的設定可以在一定程度上限制XSS攻擊,有2種方式可以設定。第一種通過設定HTTP響應頭,另一種通過HTML的<meta>标簽。
具體的設定和說明請參考: http://www.ruanyifeng.com/blog/2016/09/csp.html
Content Security Policy 入門教程
跨域腳本攻擊 XSS 是最常見、危害最大的網頁安全漏洞
為了防止它們,要采取很多程式設計措施,非常麻煩。很多人提出,能不能根本上解決問題,浏覽器自動禁止外部注入惡意腳本?
這就是"網頁安全政策"(Content Security Policy,縮寫 CSP)的來曆。本文詳細介紹如何使用 CSP 防止 XSS 攻擊。
一、簡介
CSP 的實質就是白名單制度,開發者明确告訴用戶端,哪些外部資源可以加載和執行,等同于提供白名單。它的實作和執行全部由浏覽器完成,開發者隻需提供配置。
CSP 大大增強了網頁的安全性。攻擊者即使發現了漏洞,也沒法注入腳本,除非還控制了一台列入了白名單的可信主機。
兩種方法可以啟用 CSP。一種是通過 HTTP 頭資訊的
Content-Security-Policy
的字段。
Content-Security-Policy: script-src 'self'; object-src 'none';
style-src cdn.example.org third-party.org; child-src https:
另一種是通過網頁的
<meta>
标簽。
<meta http-equiv="Content-Security-Policy" content="script-src 'self'; object-src 'none'; style-src cdn.example.org third-party.org; child-src https:">
上面代碼中,CSP 做了如下配置。
- 腳本:隻信任目前域名
- <object>标簽:不信任任何URL,即不加載任何資源
- 樣式表:隻信任cdn.example.org和third-party.org
- 架構(frame):必須使用HTTPS協定加載
- 其他資源:沒有限制
啟用後,不符合 CSP 的外部資源就會被阻止加載。
Chrome 的報錯資訊。
Firefox 的報錯資訊。
二、限制選項
CSP 提供了很多限制選項,涉及安全的各個方面。
2.1 資源加載限制
以下選項限制各類資源的加載。
- script-src:外部腳本
- style-src:樣式表
- img-src:圖像
- media-src:媒體檔案(音頻和視訊)
- font-src:字型檔案
- object-src:插件(比如 Flash)
- child-src:架構
- frame-ancestors:嵌入的外部資源(比如<frame>、<iframe>、<embed>和<applet>)
- connect-src:HTTP 連接配接(通過 XHR、WebSockets、EventSource等)
- worker-src:worker腳本
- manifest-src:manifest 檔案
2.2 default-src
default-src
用來設定上面各個選項的預設值。
Content-Security-Policy: default-src 'self'
上面代碼限制所有的外部資源,都隻能從目前域名加載。
如果同時設定某個單項限制(比如
font-src
)和
default-src
,前者會覆寫後者,即字型檔案會采用
font-src
的值,其他資源依然采用
default-src
的值。
2.3 URL 限制
有時,網頁會跟其他 URL 發生聯系,這時也可以加以限制。
- frame-ancestors:限制嵌入架構的網頁
- base-uri:限制<base#href>
- form-action:限制<form#action>
2.4 其他限制
其他一些安全相關的功能,也放在了 CSP 裡面。
-
:HTTPS 網頁不得加載 HTTP 資源(浏覽器已經預設開啟)block-all-mixed-content
-
:自動将網頁上所有加載外部資源的 HTTP 連結換成 HTTPS 協定upgrade-insecure-requests
-
:限制可以使用的插件格式plugin-types
-
:浏覽器行為的限制,比如不能有彈出視窗等。sandbox
2.5 report-uri
有時,我們不僅希望防止 XSS,還希望記錄此類行為。
report-uri
就用來告訴浏覽器,應該把注入行為報告給哪個網址。
Content-Security-Policy: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;
上面代碼指定,将注入行為報告給
/my_amazing_csp_report_parser
這個 URL。
浏覽器會使用
POST
方法,發送一個JSON對象,下面是一個例子。
{
"csp-report": {
"document-uri": "http://example.org/page.html",
"referrer": "http://evil.example.com/",
"blocked-uri": "http://evil.example.com/evil.js",
"violated-directive": "script-src 'self' https://apis.google.com",
"original-policy": "script-src 'self' https://apis.google.com; report-uri http://example.org/my_amazing_csp_report_parser"
}
}
三、Content-Security-Policy-Report-Only
除了
Content-Security-Policy
,還有一個
Content-Security-Policy-Report-Only
字段,表示不執行限制選項,隻是記錄違反限制的行為。
它必須與
report-uri
選項配合使用。
Content-Security-Policy-Report-Only: default-src 'self'; ...; report-uri /my_amazing_csp_report_parser;
四、選項值
每個限制選項可以設定以下幾種值,這些值就構成了白名單。
- 主機名:
,
example.org
https://example.com:443
- 路徑名:
example.org/resources/js/
- 通配符:
,
*.example.org
(表示任意協定、任意子域名、任意端口)
*://*.example.com:*
- 協定名:
、
https:
data:
- 關鍵字
:目前域名,需要加引号
'self'
- 關鍵字
:禁止加載任何外部資源,需要加引号
'none'
多個值也可以并列,用空格分隔。
Content-Security-Policy: script-src 'self' https://apis.google.com
如果同一個限制選項使用多次,隻有第一次會生效。
# 錯誤的寫法
script-src https://host1.com; script-src https://host2.com
# 正确的寫法
script-src https://host1.com https://host2.com
如果不設定某個限制選項,就是預設允許任何值。
五、script-src 的特殊值
除了正常值,
script-src
還可以設定一些特殊值。注意,下面這些值都必須放在單引号裡面。
-
:允許執行頁面内嵌的
'unsafe-inline'
标簽和事件監聽函數
<script>
-
:允許将字元串當作代碼執行,比如使用
unsafe-eval
、
eval
、
setTimeout
和
setInterval
等函數。
Function
- nonce值:每次HTTP回應給出一個授權token,頁面内嵌腳本必須有這個token,才會執行
- hash值:列出允許執行的腳本代碼的Hash值,頁面内嵌腳本的哈希值隻有吻合的情況下,才能執行。
nonce值的例子如下,伺服器發送網頁的時候,告訴浏覽器一個随機生成的token。
Content-Security-Policy: script-src 'nonce-EDNnf03nceIOfn39fn3e9h3sdfa'
頁面内嵌腳本,必須有這個token才能執行。
<script nonce=EDNnf03nceIOfn39fn3e9h3sdfa>
</script>
hash值的例子如下,伺服器給出一個允許執行的代碼的hash值。
Content-Security-Policy: script-src 'sha256-qznLcsROx4GACP2dm0UCKCzCG-HiZ1guq6ZZDob_Tng='
下面的代碼就會允許執行,因為hash值相符。
<script>alert('Hello, world.');</script>
注意,計算hash值的時候,<script>标簽不算在内。
除了
script-src
選項,nonce值和hash值還可以用在
style-src
選項,控制頁面内嵌的樣式表。
六、注意點
(1)
script-src
和
object-src
是必設的,除非設定了
default-src
。
因為攻擊者隻要能注入腳本,其他限制都可以規避。而
object-src
必設是因為 Flash 裡面可以執行外部腳本。
(2)
script-src
不能使用
unsafe-inline
關鍵字(除非伴随一個nonce值),也不能允許設定
data:
URL。
下面是兩個惡意攻擊的例子。
<img src="x" onerror="evil()">
<script src="data:text/javascript,evil()"></script>
(3)必須特别注意 JSONP 的回調函數。
<script
src="/path/jsonp?callback=alert(document.domain)//">
</script>
上面的代碼中,雖然加載的腳本來自目前域名,但是通過改寫回調函數,攻擊者依然可以執行惡意代碼。
七、參考連結
- CSP Is Dead, Long Live CSP! , by Lukas Weichselbaum
- An Introduction to Content Security Policy, by Mike West
Spring 代碼修改内容
我是在自定義的攔截器中加了CSP設定,後面2個關于響應頭的設定也是在這裡加的。
public class YourInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//your code
//……
//設定CSP
response.addHeader("Content-Security-Policy","default-src 'self' 'unsafe-inline' 'unsafe-eval';");
response.addHeader("X-Content-Type-Options","nosniff");
response.addHeader("X-XSS-Protection","1");
//your code
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//your code
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//your code
}
}
2、“X-Content-Type-Options”頭缺失或不安全
響應頭缺失,按照掃描要求設定,具體設定請參照第一點。如果不知道具體的屬性值,可以參考官方的API或者去網上找一找别人的說明。
這是别人寫的,大家有需要的話可以參考一下:https://www.cnblogs.com/vekair/p/11233649.html
銷售“安全記分卡”的公司正在崛起,并已開始成為企業銷售的一個因素。這些公司組合使用 HTTP 安全報頭和 IP 信譽來進行評級。不過,在很大程度上,公司的得分取決于對外開放網站上設定的安全響應報頭。本文介紹了常用的安全響應報頭及對應的推薦安全值,并給出了示例。
銷售“安全記分卡”的公司正在崛起,并已開始成為企業銷售的一個因素。我從客戶那裡了解到,他們對從評級低的供應商那裡的采購很不放心,至少有案例表明,他們依據最初的評級改變了采購決策。
我調查了這些評級公司是如何計算公司安全性得分的,結果發現他們組合使用了 HTTP 安全報頭和 IP 信譽。
IP 信譽基于的是黑名單和垃圾郵件清單,再加上公共 IP 所有權資料。隻要你的公司沒有垃圾郵件,并且能夠快速檢測和阻止惡意軟體感染,那麼通常這些軟體應該就是幹淨的。HTTP 安全報頭使用的計算方式與Mozilla Observatory的工作方式類似。
是以,對于大多數公司來說,在很大程度上,他們的得分取決于對外開放的網站上設定的安全響應報頭。
設定正确的響應報頭可以快速實作(通常不需要進行大量測試),并能提高網站的安全性,現在還可以幫助我們赢得具有安全意識的客戶。
我對這種測試方法的價值以及這些公司提出的過高的定價方案持懷疑态度。我不認為它與真正的産品安全性有那麼大的關聯。然而,這無疑增加了設定響應報頭并維護其正确性的重要性,值得為此投入時間。
在本文中,我将介紹常用的評估響應報頭,及每個報頭的推薦安全值,并給出一個響應報頭設定的示例。在本文的最後,還将給出常見的應用程式和 Web 伺服器的設定示例。
重要的安全響應報頭
Content-Security-Policy(CSP)
CSP 通過指定允許加載哪些資源的形式,來防止跨站腳本注入。在本文所列的安全響應報頭中,正确地設定和維護 CSP,可能是最耗時的,也是最容易出現風險的。在開發 CSP 的過程中,要謹慎充分地測試它——以“合法”的方式阻塞站點使用的内容源會破壞站點的功能。
建立 CSP 初稿的一個很好的工具是Mozilla 實驗室的 CSP 浏覽器擴充。在浏覽器中安裝此擴充程式,首先充分地浏覽要為其設定 CSP 的站點,然後在站點中使用生成的 CSP。理想情況下,還可以重構 JavaScript,使其沒有殘留的任何内聯腳本,進而使我們可以删除“unsafe inline”指令設定。
CSP 的指令設定可能比較複雜,也很混亂,是以,如果你想更深入的了解 CSP,請通路其官方網站。
一個好的 CSP 開始可能是如下這樣的(在真正的站點上使用時,可能需要進行大量的修改)。在包含該站點的每個部分中都添加域名。
複制代碼
# 預設情況下,僅允許通路目前站點的内容 |
# 允許通路目前站點和 imgur.com 的圖檔資源 |
# 不允許通路 Flash、Java 等對象 |
# 僅允許通路目前站點的腳本 |
# 僅允許通路目前站點的樣式 |
# 僅允許嵌入目前站點的内嵌 |
# 将 <base> 标記中的 URL 限制在目前站點 |
# 表單僅允許送出到目前站點 |
Content-Security-Policy: default-src 'self'; img-src 'self' https://i.imgur.com; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; |
Strict-Transport-Security(HSTS)
該響應報頭告訴浏覽器,隻能通過 HTTPS 通路網站——如果網站啟用過 HTTPS,它将會一直生效。如果使用子域名,還建議在任何使用過的子域名對此加以強制。
複制代碼
Strict-Transport-Security: max-age=3600; includeSubDomains |
X-Content-Type-Options
該響應報頭確定浏覽器遵守應用程式設定的 MIME 類型。這有助于防止某些類型的跨站腳本注入攻擊。
它還能減少浏覽器“猜測”某些内容不正确時的意外應用程式行為,例如,當開發人員将某個頁面标記為“HTML”,但浏覽器認為它更像 JavaScript,并試圖将其渲染為 JavaScript 時。該響應報頭能確定浏覽器始終遵守服務端設定的 MIME 類型。
複制代碼
X-Content-Type-Options: nosniff |
Cache-Control(緩存控制)
這個響應報頭比其他的要稍微複雜一些,因為我們可能需要根據内容類型的不同而使用不同的緩存政策。
任何具有敏感資料的頁面,如使用者頁面或客戶結算頁面,都應該設定成無緩存。其中一個原因是,防止共享計算機上的某個人按回退按鈕或浏覽曆史記錄又能檢視到個人資訊。
但是,對于像靜态資産(圖像、CSS 檔案和 JS 檔案)等很少變更的頁面,很适合使用緩存。既可以通過逐頁設定的方式來實作,也可以通過在服務端配置使用正規表達式的方式來實作。
複制代碼
# 預設情況不使用緩存 |
Header set Cache-Control no-cache |
# 靜态資産設定成緩存 1 天 |
<filesMatch ".(css|jpg|jpeg|png|gif|js|ico)$"> |
Header set Cache-Control "max-age=86400, public" |
</filesMatch> |
Expires(過期時間)
該響應報頭能設定目前請求緩存的過期時間。如果設定了 Cache-Control 的 max-age 響應報頭,它将會被忽略,是以,在不考慮使用 Cache-Control 而進行本地緩存測試時,才設定它。
為了安全起見,我們假定浏覽器不應該緩存任何内容,是以,我們可以把過期時間設定為一個總表示過期的數值。
複制代碼
Expires: 0 |
X-Frame-Options
該響應報頭用來表明站點是否允許在 iFrame 中展示。
如果惡意站點将我們的網站嵌套在 iFrame 中,那麼惡意站點就可以通過運作一些 JavaScript 來執行點選劫持攻擊,這些 JavaScript 能夠捕獲 iFrame 上的滑鼠點選事件,然後代表使用者與該站點進行互動(不必單擊需要單擊它們的地方!)。
應該始終将它設定為 deny(拒絕),除非特别需要使用内嵌,在這種情況下,應将其設定為 same-origin(同源)。如果需要在頁面中内嵌其他的站點,也可以在此處以白名單的形式列舉其他的域名。
還應該注意的是,這個響應報頭已經被 CSP 的 frame-ancestors 指令所取代。目前,我仍然建議設定該響應報頭來相容不同的工具,但将來它可能會被逐漸淘汰。
複制代碼
X-Frame-Options: deny |
Access-Control-Allow-Origin
通過該響應報頭可以告訴浏覽器,允許哪些其他站點的前端 JavaScript 代碼對頁面送出請求。除非需要設定此響應報頭,否則通常預設值就是正确的設定。
例如,如果站點 A 使用了一些 JavaScript,該 JavaScript 想要向站點 B 送出請求,那麼站點 B 必須使用指定了允許站點 A 發出此請求的報頭來提供響應。如果需要設定多個源,請參見MDN 上的詳情介紹頁面。
這可能有點難以了解,是以,我畫了一個圖表來說明這個響應報頭是如何工作的:
Access-Control-Allow-Origin 對應的資料流
``` Access-Control-Allow-Origin: http://www.one.site.com ```
Set-Cookie
確定 cookie 僅能通過 HTTPS(加密)傳送,并且不能通過 JavaScript 通路。如果站點也支援 HTTPS(站點應該支援 HTTPS),那麼就隻能發送 HTTPS cookie。我們通常需要設定如下标志:
- Secure
- HttpOnly
一個定義 Cookie 的示例:
複制代碼
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly |
請參閱Mozilla 文檔中的 cookies 部分以了解更多相關資訊。
X-XSS-Protection
該響應報頭用來訓示浏覽器停止執行跨站腳本攻擊檢測。一般來說,設定它的風險很低,但在投入生産前仍需要進行測試。
複制代碼
X-XSS-Protection: 1; mode=block |
Web 伺服器的配置示例
通常,最好在伺服器配置中添加站點範圍内的響應報頭。在此,cookie 是一個例外,因為它們通常是在應用程式内定義的。
在将任何響應報頭添加到站點之前,我建議首先檢查 Observatory 或手動檢視響應報頭,以檢視已經設定了哪些響應報頭。有些架構和伺服器會自動設定其中一些響應報頭,是以,我們隻需設定我們需要的或想要變更的響應報頭即可。
Apache 配置
.htaccess 中的 Apache 設定示例:
複制代碼
<IfModule mod_headers.c> |
## CSP |
Header set Content-Security-Policy: default-src 'self'; img-src 'self' https://i.imgur.com; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; |
## 通用的安全響應報頭 |
Header set X-XSS-Protection: 1; mode=block |
Header set Access-Control-Allow-Origin: http://www.one.site.com |
Header set X-Frame-Options: deny |
Header set X-Content-Type-Options: nosniff |
Header set Strict-Transport-Security: max-age=3600; includeSubDomains |
## 緩存政策 |
# 預設情況下不使用緩存 |
Header set Cache-Control no-cache |
Header set Expires: 0 |
# 設定靜态資産緩存 1 天 |
<filesMatch ".(ico|css|js|gif|jpeg|jpg|png|svg|woff|ttf|eot)$"> |
Header set Cache-Control "max-age=86400, public" |
</filesMatch> |
</IfModule> |
Nginx 設定
複制代碼
## CSP |
add_header Content-Security-Policy: default-src 'self'; img-src 'self' https://i.imgur.com; object-src 'none'; script-src 'self'; style-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; |
## 通用的安全響應報頭 |
add_header X-XSS-Protection: 1; mode=block; |
add_header Access-Control-Allow-Origin: http://www.one.site.com; |
add_header X-Frame-Options: deny; |
add_header X-Content-Type-Options: nosniff; |
add_header Strict-Transport-Security: max-age=3600; includeSubDomains; |
## 緩存政策 |
** 預設不使用緩存 ** |
add_header Cache-Control no-cache; |
add_header Expires: 0; |
** 設定靜态資産緩存 1 天 ** |
location ~* \.(?:ico|css|js|gif|jpe?g|png|svg|woff|ttf|eot)$ { |
try_files $uri @rewriteapp; |
add_header Cache-Control "max-age=86400, public"; |
} |
應用程式級的響應報頭設定
如果我們沒有通路 Web 伺服器的權限,或者需要設定複雜的響應報頭,那麼我們就可能需要在應用程式内設定這些響應報頭了。這通常可以在整個站點的架構中間件中實作,也可以在每次響應的基礎上進行一次性的報頭設定。
為了簡便起見,在示例中,隻包含了一個響應報頭。所需的全部響應報頭都是以相同的方式通過該方法來添加的。
Node 及 express:
添加一個全局挂載路徑:
複制代碼
app.use(function(req, res, next) { |
res.header('X-XSS-Protection', 1; mode=block); |
next(); |
}); |
Java 及 Spring:
我沒有太多的 Spring 實踐經驗,但Baeldung對在 Spring 中如何設定響應報頭提供了很好的指導。
PHP:
我對各種 PHP 架構不是很熟悉。查找了能夠處理請求的中間件。對于單個響應,它的設定非常簡單。
複制代碼
header("X-XSS-Protection: 1; mode=block"); |
Python 及 Django
Django 包含可配置的安全中間件,通過該中間件來處理所有響應報頭的設定。首先啟用它們。
對于特定頁面,可以将響應視為字典。Django 有一個處理緩存的特殊方法,如果試圖以這種方式設定緩存響應報頭,那麼就應該調研後再使用。
複制代碼
response = HttpResponse() |
response["X-XSS-Protection"] = "1; mode=block" |
總結
設定響應報頭相對來說比較簡單快捷。在資料保護、跨站腳本注入和點選劫持方面,站點安全性将會有相當大的提高。
還可以確定我們不會因為依賴此資訊的公司安全評級而失去未來的業務交易。這種做法似乎越來越多,我希望在未來幾年,它能繼續在企業銷售中發揮作用。
如果以上有所遺漏,你認為還應該包含其他的安全響應報頭,請留言回複。
英文原文:https://nullsweep.com/http-security-headers-a-complete-guide
3、“X-XSS-Protection”頭缺失或不安全
3、“X-XSS-Protection”頭缺失或不安全
響應頭缺失,按照掃描要求設定,具體設定請參照前2點。
4、查詢中接受的主體參數
4、查詢中接受的主體參數
這個就是禁用GET請求,但是由于系統内部需要用到,是以隻需要對攜帶資料傳遞的請求設定為POST方式。
5、啟用了不安全的“OPTIONS”HTTP 方法
5、啟用了不安全的“OPTIONS”HTTP 方法
這個在網上有很多方法可以參考,如果伺服器有Nginx,可以直接在Nginx中配置,如果沒有可以配置在Tomcat上。我這裡是配置在Tomcat上的。
Tomcat的web.xml檔案進行修改(修改前請先記得備份哦)
<!-- 限制HTTP請求方法配置,開始 -->
<!-- 先注釋掉原來的引入檔案 -->
<!-- <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"> -->
<!-- 添加新的引入檔案 -->
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:web="http://java.sun.com/xml/ns/j2ee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<!-- 過濾掉DELETE、HEAD、PUT等請求方式 -->
<security-constraint>
<web-resource-collection>
<url-pattern>/*</url-pattern>
<http-method>PUT</http-method>
<http-method>DELETE</http-method>
<http-method>HEAD</http-method>
<http-method>OPTIONS</http-method>
<http-method>TRACE</http-method>
<http-method>PATCH</http-method>
</web-resource-collection>
<auth-constraint></auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
<!--限制HTTP請求方法配置,結束 -->
<!-- 限制HTTP請求方法配置,開始 -->
<!-- 先注釋掉原來的引入檔案 -->
<!-- <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"> -->
<!-- 添加新的引入檔案 -->
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:web="http://java.sun.com/xml/ns/j2ee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<!-- 過濾掉DELETE、HEAD、PUT等請求方式 -->
<security-constraint>
<web-resource-collection>
<url-pattern>/*</url-pattern>
<http-method>PUT</http-method>
<http-method>DELETE</http-method>
<http-method>HEAD</http-method>
<http-method>OPTIONS</http-method>
<http-method>TRACE</http-method>
<http-method>PATCH</http-method>
</web-resource-collection>
<auth-constraint></auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
</login-config>
<!--限制HTTP請求方法配置,結束 -->
6、發現資料庫錯誤模式
這個就是說不允許使用者在前台看到有關資料庫的報錯,就是不能在前台看到下圖這樣類型的報錯。
是以這裡就對資料庫背景的查詢進行攔截,做一個統一的異常處理,使前台看不到這種報錯,類似下圖這種,具體的樣式可以自己設計。
後端需要自定義一個異常處理類,繼承HandlerExceptionResolver,重寫resolveException()方法,在方法裡面對異常資訊進行處理即可。我這裡直接簡單寫了幾句話,傳回給前台頁面,加上了時間是便于排查問題。要記得把這個類注入進去。
@Component
public class WebExceptionResolver implements HandlerExceptionResolver {
private static transient Logger logger = LoggerFactory.getLogger(WebExceptionResolver.class);
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
logger.error("WebExceptionResolver:{}", ex);
// if json
boolean isJson = false;
HandlerMethod method = (HandlerMethod) handler;
ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class);
if (responseBody != null) {
isJson = true;
}
// error result
Map<String, String> map = new HashMap<>();
map.put("errorCode", "500");
map.put("errorMsg", "出錯了,請聯系管理者");
map.put("errorDate", DateUtil.getToday19());
String errorResult = JSONObject.toJSONString(map);
// response
ModelAndView mv = new ModelAndView();
if (isJson) {
try {
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(errorResult);
} catch (IOException e) {
logger.error(e.getMessage(), e);
}
return mv;
} else {
mv.addObject("exceptionMsg",map.get("errorMsg"));
mv.addObject("date", DateUtil.getToday19());
mv.setViewName("sys/errorPage");
return mv;
}
}
}
6、具有不安全、不正确或缺少 SameSite 屬性的 Cookie
基于 Shiro 架構作為 Cookie管理器模式
這個就是要求對Cookie設定SameSite屬性,防止跨域和XSS攻擊的。因為我這裡使用了Shiro架構作為Cookie管理器,要從Shiro的角度處理。
從Shiro的配置檔案可以看到,預設使用的是DefaultSessionManager。
<!--Session叢集配置 -->
<bean id="sessionManager"
class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="globalSessionTimeout" value="${globalSessionTimeout}" />
<property name="sessionDAO" ref="shiroSessionDAO" />
<property name="sessionValidationScheduler" ref="sessionValidationScheduler" />
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionIdCookie" ref="wapsession" />
</bean>
因為Shiro版本問題(1.2.3),去查官網文檔發現這個版本的cookie是沒有SameSite屬性的。1.7.1及以上版本才添加了這個屬性,是以現在要麼就把Shiro更新版本,要麼就重寫這個類。由于考慮到Shiro與Spring的相容問題,是以我這裡直接重寫DefaultWebSessionManager。
看源碼我們可以看到,在DefaultWebSessionManager這個類有2個屬性,其中有一個就是Cookie,它的構造方法可以去改變cookie的值,進而給cookie加上SameSite屬性。
但是仔細觀察構造方法,可以看到這個Cookie是一個SimpleCookie,也就是說我們想要給cookie加上一個新的屬性,還需要自定義SimpleCookie。
除此之外,還有一點很重要,除了構造方法以外,還要關注cookie是什麼時候生成的。這個onStart()方法很重要,是存儲session的核心方法。
觀察完畢之後,可以動手寫我們自己的WebSessionManager和SimpleCookie了。
(1)自定義WebSessionManager:MyDefaultWebSessionManager
public class MyDefaultWebSessionManager extends DefaultWebSessionManager {
private static final Logger log = LoggerFactory.getLogger(MyDefaultWebSessionManager.class);
@Override
protected void onStart(Session session, SessionContext context) {
super.onStart(session, context);
if (!WebUtils.isHttp(context)) {
log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response pair. No session ID cookie will be set.");
} else {
HttpServletRequest request = WebUtils.getHttpRequest(context);
HttpServletResponse response = WebUtils.getHttpResponse(context);
if (this.isSessionIdCookieEnabled()) {
Serializable sessionId = session.getId();
//重寫父類方法,使用我們自己定義的cookie
this.storeSessionId(sessionId, request, response);
} else {
log.debug("Session ID cookie is disabled. No cookie has been set for new session with id {}", session.getId());
}
request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
}
}
private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) {
if (currentId == null) {
String msg = "sessionId cannot be null when persisting for subsequent requests.";
throw new IllegalArgumentException(msg);
} else {
Cookie cookie = this.getSessionIdCookie();
//Cookie cookie = new MySimpleCookie(template);
String idString = currentId.toString();
cookie.setValue(idString);
//這一行決定自定義的cookie中的屬性
cookie.saveTo(request, response);
log.trace("Set session ID cookie for session with id {}", idString);
}
}
}
(2)自定義SimpleCookie:MySimpleCookie
public class MySimpleCookie extends SimpleCookie {
private static final transient Logger log = LoggerFactory.getLogger(MySimpleCookie.class);
//自定義的屬性sameSite
private String sameSite;
//重寫該方法,添加SameSite屬性
@Override
public void saveTo(HttpServletRequest request, HttpServletResponse response) {
String name = this.getName();
String value = this.getValue();
String comment = this.getComment();
String domain = this.getDomain();
String path = this.calculatePath(request);
int maxAge = this.getMaxAge();
int version = this.getVersion();
boolean secure = this.isSecure();
boolean httpOnly = this.isHttpOnly();
String s = this.addCookieHeader(name, value, comment, domain, path, maxAge,
version, secure, httpOnly);
//在原來的基礎上添加SameSite屬性
String headerValue = appendtSameSite(s, sameSite);
response.addHeader("Set-Cookie", headerValue);
}
private String addCookieHeader(String name,
String value, String comment, String domain,
String path, int maxAge, int version, boolean secure,
boolean httpOnly) {
String headerValue = this.buildHeaderValue(name, value, comment, domain, path, maxAge, version, secure, httpOnly);
if (log.isDebugEnabled()) {
log.debug("Added HttpServletResponse Cookie [{}]", headerValue);
}
return headerValue;
}
private String calculatePath(HttpServletRequest request) {
String path = StringUtils.clean(this.getPath());
if (!StringUtils.hasText(path)) {
path = StringUtils.clean(request.getContextPath());
}
if (path == null) {
path = "/";
}
log.trace("calculated path: {}", path);
return path;
}
//這裡隻拼接一個字元串,沒用StringBuilder,不考慮效率問題
private String appendtSameSite(String s, String sameSite) {
if (org.apache.commons.lang3.StringUtils.isNotBlank(sameSite)) {
s += ("; ");
s += ("SameSite=") + sameSite;
}
return s;
}
public String getSameSite() {
return sameSite;
}
public void setSameSite(String sameSite) {
this.sameSite = sameSite;
}
public MySimpleCookie(String name, String sameSite) {
super(name);
this.sameSite = sameSite;
}
}
(3)調整自己的配置檔案,使WebSessionManager指向自定義的類。
<!--Session叢集配置 -->
<bean id="sessionManager"
class="com.mytest.common.sys.service.realm.MyDefaultWebSessionManager">
<property name="globalSessionTimeout" value="${globalSessionTimeout}" />
<property name="sessionDAO" ref="shiroSessionDAO" />
<property name="sessionValidationScheduler" ref="sessionValidationScheduler" />
<property name="sessionValidationSchedulerEnabled" value="true" />
<property name="sessionIdCookie" ref="wapsession" />
</bean>
<!-- 指定本系統SESSIONID, 預設為: JSESSIONID 問題: 與SERVLET容器名沖突, 如JETTY, TOMCAT 等預設JSESSIONID-->
<bean id="wapsession" class="com.mytest.common.sys.service.realm.MySimpleCookie">
<property name="secure" value="${cookieIsSecure}"/>
<constructor-arg name="name" value="${cookieName}"/>
<constructor-arg name="sameSite" value="${sameSite}"/>
</bean>
(4)最後呈現的效果
基于Spring 正常的 一般管理模式
import com.google.common.net.HttpHeaders;
import java.io.IOException;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.UrlPathHelper;
public class XssFilter implements Filter {
public XssFilter() {
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//方法體裡增加代碼:
//20220616 處理:“具有不安全、不正确或缺少 SameSite 屬性的 Cookie”
httpServletResponse.setHeader("Set-Cookie", ((HttpServletRequest) request).getHeader("Set-Cookie")+",SameSite=strict");
ResponseCookie cookie = ResponseCookie.from("myCookie", "myCookieValue") // key & value
.httpOnly(true) // 禁止js讀取
.secure(false) // 在http下也傳輸
.domain("localhost")// 域名
.path("/") // path
.maxAge(Duration.ofHours(1)) // 1個小時候過期
.sameSite("Lax") // 大多數情況也是不發送第三方 Cookie,但是導航到目标網址的 Get 請求除外 .Samesite 有兩個屬性值,分别是 Strict 、Lax和None。
.build()
;
// 設定Cookie Header
httpServletResponse.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());
} }
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import java.time.Duration;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* An {@code HttpCookie} subclass with the additional attributes allowed in
* the "Set-Cookie" response header. To build an instance use the {@link #from}
* static method.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @since 5.0
* @see <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public final class ResponseCookie extends HttpCookie {
private final Duration maxAge;
@Nullable
private final String domain;
@Nullable
private final String path;
private final boolean secure;
private final boolean httpOnly;
@Nullable
private final String sameSite;
/**
* Private constructor. See {@link #from(String, String)}.
*/
private ResponseCookie(String name, String value, Duration maxAge, @Nullable String domain,
@Nullable String path, boolean secure, boolean httpOnly, @Nullable String sameSite) {
super(name, value);
Assert.notNull(maxAge, "Max age must not be null");
this.maxAge = maxAge;
this.domain = domain;
this.path = path;
this.secure = secure;
this.httpOnly = httpOnly;
this.sameSite = sameSite;
Rfc6265Utils.validateCookieName(name);
Rfc6265Utils.validateCookieValue(value);
Rfc6265Utils.validateDomain(domain);
Rfc6265Utils.validatePath(path);
}
/**
* Return the cookie "Max-Age" attribute in seconds.
* <p>A positive value indicates when the cookie expires relative to the
* current time. A value of 0 means the cookie should expire immediately.
* A negative value means no "Max-Age" attribute in which case the cookie
* is removed when the browser is closed.
*/
public Duration getMaxAge() {
return this.maxAge;
}
/**
* Return the cookie "Domain" attribute, or {@code null} if not set.
*/
@Nullable
public String getDomain() {
return this.domain;
}
/**
* Return the cookie "Path" attribute, or {@code null} if not set.
*/
@Nullable
public String getPath() {
return this.path;
}
/**
* Return {@code true} if the cookie has the "Secure" attribute.
*/
public boolean isSecure() {
return this.secure;
}
/**
* Return {@code true} if the cookie has the "HttpOnly" attribute.
* @see <a href="https://www.owasp.org/index.php/HTTPOnly">https://www.owasp.org/index.php/HTTPOnly</a>
*/
public boolean isHttpOnly() {
return this.httpOnly;
}
/**
* Return the cookie "SameSite" attribute, or {@code null} if not set.
* <p>This limits the scope of the cookie such that it will only be attached to
* same site requests if {@code "Strict"} or cross-site requests if {@code "Lax"}.
* @since 5.1
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
*/
@Nullable
public String getSameSite() {
return this.sameSite;
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof ResponseCookie)) {
return false;
}
ResponseCookie otherCookie = (ResponseCookie) other;
return (getName().equalsIgnoreCase(otherCookie.getName()) &&
ObjectUtils.nullSafeEquals(this.path, otherCookie.getPath()) &&
ObjectUtils.nullSafeEquals(this.domain, otherCookie.getDomain()));
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + ObjectUtils.nullSafeHashCode(this.domain);
result = 31 * result + ObjectUtils.nullSafeHashCode(this.path);
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getName()).append('=').append(getValue());
if (StringUtils.hasText(getPath())) {
sb.append("; Path=").append(getPath());
}
if (StringUtils.hasText(this.domain)) {
sb.append("; Domain=").append(this.domain);
}
if (!this.maxAge.isNegative()) {
sb.append("; Max-Age=").append(this.maxAge.getSeconds());
sb.append("; Expires=");
long millis = this.maxAge.getSeconds() > 0 ? System.currentTimeMillis() + this.maxAge.toMillis() : 0;
sb.append(HttpHeaders.formatDate(millis));
}
if (this.secure) {
sb.append("; Secure");
}
if (this.httpOnly) {
sb.append("; HttpOnly");
}
if (StringUtils.hasText(this.sameSite)) {
sb.append("; SameSite=").append(this.sameSite);
}
return sb.toString();
}
/**
* Factory method to obtain a builder for a server-defined cookie that starts
* with a name-value pair and may also include attributes.
* @param name the cookie name
* @param value the cookie value
* @return a builder to create the cookie with
*/
public static ResponseCookieBuilder from(final String name, final String value) {
return from(name, value, false);
}
/**
* Factory method to obtain a builder for a server-defined cookie. Unlike
* {@link #from(String, String)} this option assumes input from a remote
* server, which can be handled more leniently, e.g. ignoring a empty domain
* name with double quotes.
* @param name the cookie name
* @param value the cookie value
* @return a builder to create the cookie with
* @since 5.2.5
*/
public static ResponseCookieBuilder fromClientResponse(final String name, final String value) {
return from(name, value, true);
}
private static ResponseCookieBuilder from(final String name, final String value, boolean lenient) {
return new ResponseCookieBuilder() {
private Duration maxAge = Duration.ofSeconds(-1);
@Nullable
private String domain;
@Nullable
private String path;
private boolean secure;
private boolean httpOnly;
@Nullable
private String sameSite;
@Override
public ResponseCookieBuilder maxAge(Duration maxAge) {
this.maxAge = maxAge;
return this;
}
@Override
public ResponseCookieBuilder maxAge(long maxAgeSeconds) {
this.maxAge = maxAgeSeconds >= 0 ? Duration.ofSeconds(maxAgeSeconds) : Duration.ofSeconds(-1);
return this;
}
@Override
public ResponseCookieBuilder domain(String domain) {
this.domain = initDomain(domain);
return this;
}
@Nullable
private String initDomain(String domain) {
if (lenient && StringUtils.hasLength(domain)) {
String str = domain.trim();
if (str.startsWith("\"") && str.endsWith("\"")) {
if (str.substring(1, str.length() - 1).trim().isEmpty()) {
return null;
}
}
}
return domain;
}
@Override
public ResponseCookieBuilder path(String path) {
this.path = path;
return this;
}
@Override
public ResponseCookieBuilder secure(boolean secure) {
this.secure = secure;
return this;
}
@Override
public ResponseCookieBuilder httpOnly(boolean httpOnly) {
this.httpOnly = httpOnly;
return this;
}
@Override
public ResponseCookieBuilder sameSite(@Nullable String sameSite) {
this.sameSite = sameSite;
return this;
}
@Override
public ResponseCookie build() {
return new ResponseCookie(name, value, this.maxAge, this.domain, this.path,
this.secure, this.httpOnly, this.sameSite);
}
};
}
/**
* A builder for a server-defined HttpCookie with attributes.
*/
public interface ResponseCookieBuilder {
/**
* Set the cookie "Max-Age" attribute.
*
* <p>A positive value indicates when the cookie should expire relative
* to the current time. A value of 0 means the cookie should expire
* immediately. A negative value results in no "Max-Age" attribute in
* which case the cookie is removed when the browser is closed.
*/
ResponseCookieBuilder maxAge(Duration maxAge);
/**
* Variant of {@link #maxAge(Duration)} accepting a value in seconds.
*/
ResponseCookieBuilder maxAge(long maxAgeSeconds);
/**
* Set the cookie "Path" attribute.
*/
ResponseCookieBuilder path(String path);
/**
* Set the cookie "Domain" attribute.
*/
ResponseCookieBuilder domain(String domain);
/**
* Add the "Secure" attribute to the cookie.
*/
ResponseCookieBuilder secure(boolean secure);
/**
* Add the "HttpOnly" attribute to the cookie.
* @see <a href="https://www.owasp.org/index.php/HTTPOnly">https://www.owasp.org/index.php/HTTPOnly</a>
*/
ResponseCookieBuilder httpOnly(boolean httpOnly);
/**
* Add the "SameSite" attribute to the cookie.
* <p>This limits the scope of the cookie such that it will only be
* attached to same site requests if {@code "Strict"} or cross-site
* requests if {@code "Lax"}.
* @since 5.1
* @see <a href="https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis#section-4.1.2.7">RFC6265 bis</a>
*/
ResponseCookieBuilder sameSite(@Nullable String sameSite);
/**
* Create the HttpCookie.
*/
ResponseCookie build();
}
private static class Rfc6265Utils {
private static final String SEPARATOR_CHARS = new String(new char[] {
'(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '=', '{', '}', ' '
});
private static final String DOMAIN_CHARS =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-";
public static void validateCookieName(String name) {
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
// CTL = <US-ASCII control chars (octets 0 - 31) and DEL (127)>
if (c <= 0x1F || c == 0x7F) {
throw new IllegalArgumentException(
name + ": RFC2616 token cannot have control chars");
}
if (SEPARATOR_CHARS.indexOf(c) >= 0) {
throw new IllegalArgumentException(
name + ": RFC2616 token cannot have separator chars such as '" + c + "'");
}
if (c >= 0x80) {
throw new IllegalArgumentException(
name + ": RFC2616 token can only have US-ASCII: 0x" + Integer.toHexString(c));
}
}
}
public static void validateCookieValue(@Nullable String value) {
if (value == null) {
return;
}
int start = 0;
int end = value.length();
if (end > 1 && value.charAt(0) == '"' && value.charAt(end - 1) == '"') {
start = 1;
end--;
}
for (int i = start; i < end; i++) {
char c = value.charAt(i);
if (c < 0x21 || c == 0x22 || c == 0x2c || c == 0x3b || c == 0x5c || c == 0x7f) {
throw new IllegalArgumentException(
"RFC2616 cookie value cannot have '" + c + "'");
}
if (c >= 0x80) {
throw new IllegalArgumentException(
"RFC2616 cookie value can only have US-ASCII chars: 0x" + Integer.toHexString(c));
}
}
}
public static void validateDomain(@Nullable String domain) {
if (!StringUtils.hasLength(domain)) {
return;
}
int char1 = domain.charAt(0);
int charN = domain.charAt(domain.length() - 1);
if (char1 == '-' || charN == '.' || charN == '-') {
throw new IllegalArgumentException("Invalid first/last char in cookie domain: " + domain);
}
for (int i = 0, c = -1; i < domain.length(); i++) {
int p = c;
c = domain.charAt(i);
if (DOMAIN_CHARS.indexOf(c) == -1 || (p == '.' && (c == '.' || c == '-')) || (p == '-' && c == '.')) {
throw new IllegalArgumentException(domain + ": invalid cookie domain char '" + c + "'");
}
}
}
public static void validatePath(@Nullable String path) {
if (path == null) {
return;
}
for (int i = 0; i < path.length(); i++) {
char c = path.charAt(i);
if (c < 0x20 || c > 0x7E || c == ';') {
throw new IllegalArgumentException(path + ": Invalid cookie path char '" + c + "'");
}
}
}
}
}
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import com.sun.xml.internal.messaging.saaj.packaging.mime.internet.ContentDisposition;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* A data structure representing HTTP request or response headers, mapping String header names
* to a list of String values, also offering accessors for common application-level data types.
*
* <p>In addition to the regular methods defined by {@link Map}, this class offers many common
* convenience methods, for example:
* <ul>
* <li>{@link #getFirst(String)} returns the first value associated with a given header name</li>
* <li>{@link #add(String, String)} adds a header value to the list of values for a header name</li>
* <li>{@link #set(String, String)} sets the header value to a single string value</li>
* </ul>
*
* <p>Note that {@code HttpHeaders} generally treats header names in a case-insensitive manner.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Brian Clozel
* @author Juergen Hoeller
* @author Josh Long
* @author Sam Brannen
* @since 3.0
*/
public class HttpHeaders implements MultiValueMap<String, String>, Serializable {
private static final long serialVersionUID = -8578554704772377436L;
/**
* The HTTP {@code Accept} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">Section 5.3.2 of RFC 7231</a>
*/
public static final String ACCEPT = "Accept";
/**
* The HTTP {@code Accept-Charset} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.3">Section 5.3.3 of RFC 7231</a>
*/
public static final String ACCEPT_CHARSET = "Accept-Charset";
/**
* The HTTP {@code Accept-Encoding} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.4">Section 5.3.4 of RFC 7231</a>
*/
public static final String ACCEPT_ENCODING = "Accept-Encoding";
/**
* The HTTP {@code Accept-Language} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.5">Section 5.3.5 of RFC 7231</a>
*/
public static final String ACCEPT_LANGUAGE = "Accept-Language";
/**
* The HTTP {@code Accept-Patch} header field name.
* @since 5.3.6
* @see <a href="https://tools.ietf.org/html/rfc5789#section-3.1">Section 3.1 of RFC 5789</a>
*/
public static final String ACCEPT_PATCH = "Accept-Patch";
/**
* The HTTP {@code Accept-Ranges} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7233#section-2.3">Section 5.3.5 of RFC 7233</a>
*/
public static final String ACCEPT_RANGES = "Accept-Ranges";
/**
* The CORS {@code Access-Control-Allow-Credentials} response header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
/**
* The CORS {@code Access-Control-Allow-Headers} response header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
/**
* The CORS {@code Access-Control-Allow-Methods} response header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
/**
* The CORS {@code Access-Control-Allow-Origin} response header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
/**
* The CORS {@code Access-Control-Expose-Headers} response header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
/**
* The CORS {@code Access-Control-Max-Age} response header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
/**
* The CORS {@code Access-Control-Request-Headers} request header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
/**
* The CORS {@code Access-Control-Request-Method} request header field name.
* @see <a href="https://www.w3.org/TR/cors/">CORS W3C recommendation</a>
*/
public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
/**
* The HTTP {@code Age} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.1">Section 5.1 of RFC 7234</a>
*/
public static final String AGE = "Age";
/**
* The HTTP {@code Allow} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.4.1">Section 7.4.1 of RFC 7231</a>
*/
public static final String ALLOW = "Allow";
/**
* The HTTP {@code Authorization} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.2">Section 4.2 of RFC 7235</a>
*/
public static final String AUTHORIZATION = "Authorization";
/**
* The HTTP {@code Cache-Control} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2">Section 5.2 of RFC 7234</a>
*/
public static final String CACHE_CONTROL = "Cache-Control";
/**
* The HTTP {@code Connection} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-6.1">Section 6.1 of RFC 7230</a>
*/
public static final String CONNECTION = "Connection";
/**
* The HTTP {@code Content-Encoding} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.2.2">Section 3.1.2.2 of RFC 7231</a>
*/
public static final String CONTENT_ENCODING = "Content-Encoding";
/**
* The HTTP {@code Content-Disposition} header field name.
* @see <a href="https://tools.ietf.org/html/rfc6266">RFC 6266</a>
*/
public static final String CONTENT_DISPOSITION = "Content-Disposition";
/**
* The HTTP {@code Content-Language} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.3.2">Section 3.1.3.2 of RFC 7231</a>
*/
public static final String CONTENT_LANGUAGE = "Content-Language";
/**
* The HTTP {@code Content-Length} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3.2">Section 3.3.2 of RFC 7230</a>
*/
public static final String CONTENT_LENGTH = "Content-Length";
/**
* The HTTP {@code Content-Location} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.4.2">Section 3.1.4.2 of RFC 7231</a>
*/
public static final String CONTENT_LOCATION = "Content-Location";
/**
* The HTTP {@code Content-Range} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7233#section-4.2">Section 4.2 of RFC 7233</a>
*/
public static final String CONTENT_RANGE = "Content-Range";
/**
* The HTTP {@code Content-Type} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-3.1.1.5">Section 3.1.1.5 of RFC 7231</a>
*/
public static final String CONTENT_TYPE = "Content-Type";
/**
* The HTTP {@code Cookie} header field name.
* @see <a href="https://tools.ietf.org/html/rfc2109#section-4.3.4">Section 4.3.4 of RFC 2109</a>
*/
public static final String COOKIE = "Cookie";
/**
* The HTTP {@code Date} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.2">Section 7.1.1.2 of RFC 7231</a>
*/
public static final String DATE = "Date";
/**
* The HTTP {@code ETag} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
*/
public static final String ETAG = "ETag";
/**
* The HTTP {@code Expect} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.1.1">Section 5.1.1 of RFC 7231</a>
*/
public static final String EXPECT = "Expect";
/**
* The HTTP {@code Expires} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.3">Section 5.3 of RFC 7234</a>
*/
public static final String EXPIRES = "Expires";
/**
* The HTTP {@code From} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.5.1">Section 5.5.1 of RFC 7231</a>
*/
public static final String FROM = "From";
/**
* The HTTP {@code Host} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-5.4">Section 5.4 of RFC 7230</a>
*/
public static final String HOST = "Host";
/**
* The HTTP {@code If-Match} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.1">Section 3.1 of RFC 7232</a>
*/
public static final String IF_MATCH = "If-Match";
/**
* The HTTP {@code If-Modified-Since} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.3">Section 3.3 of RFC 7232</a>
*/
public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
/**
* The HTTP {@code If-None-Match} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.2">Section 3.2 of RFC 7232</a>
*/
public static final String IF_NONE_MATCH = "If-None-Match";
/**
* The HTTP {@code If-Range} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7233#section-3.2">Section 3.2 of RFC 7233</a>
*/
public static final String IF_RANGE = "If-Range";
/**
* The HTTP {@code If-Unmodified-Since} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7232#section-3.4">Section 3.4 of RFC 7232</a>
*/
public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
/**
* The HTTP {@code Last-Modified} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.2">Section 2.2 of RFC 7232</a>
*/
public static final String LAST_MODIFIED = "Last-Modified";
/**
* The HTTP {@code Link} header field name.
* @see <a href="https://tools.ietf.org/html/rfc5988">RFC 5988</a>
*/
public static final String LINK = "Link";
/**
* The HTTP {@code Location} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.2">Section 7.1.2 of RFC 7231</a>
*/
public static final String LOCATION = "Location";
/**
* The HTTP {@code Max-Forwards} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.1.2">Section 5.1.2 of RFC 7231</a>
*/
public static final String MAX_FORWARDS = "Max-Forwards";
/**
* The HTTP {@code Origin} header field name.
* @see <a href="https://tools.ietf.org/html/rfc6454">RFC 6454</a>
*/
public static final String ORIGIN = "Origin";
/**
* The HTTP {@code Pragma} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.4">Section 5.4 of RFC 7234</a>
*/
public static final String PRAGMA = "Pragma";
/**
* The HTTP {@code Proxy-Authenticate} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.3">Section 4.3 of RFC 7235</a>
*/
public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
/**
* The HTTP {@code Proxy-Authorization} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.4">Section 4.4 of RFC 7235</a>
*/
public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
/**
* The HTTP {@code Range} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7233#section-3.1">Section 3.1 of RFC 7233</a>
*/
public static final String RANGE = "Range";
/**
* The HTTP {@code Referer} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.5.2">Section 5.5.2 of RFC 7231</a>
*/
public static final String REFERER = "Referer";
/**
* The HTTP {@code Retry-After} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.3">Section 7.1.3 of RFC 7231</a>
*/
public static final String RETRY_AFTER = "Retry-After";
/**
* The HTTP {@code Server} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.4.2">Section 7.4.2 of RFC 7231</a>
*/
public static final String SERVER = "Server";
/**
* The HTTP {@code Set-Cookie} header field name.
* @see <a href="https://tools.ietf.org/html/rfc2109#section-4.2.2">Section 4.2.2 of RFC 2109</a>
*/
public static final String SET_COOKIE = "Set-Cookie";
/**
* The HTTP {@code Set-Cookie2} header field name.
* @see <a href="https://tools.ietf.org/html/rfc2965">RFC 2965</a>
*/
public static final String SET_COOKIE2 = "Set-Cookie2";
/**
* The HTTP {@code TE} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-4.3">Section 4.3 of RFC 7230</a>
*/
public static final String TE = "TE";
/**
* The HTTP {@code Trailer} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-4.4">Section 4.4 of RFC 7230</a>
*/
public static final String TRAILER = "Trailer";
/**
* The HTTP {@code Transfer-Encoding} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3.1">Section 3.3.1 of RFC 7230</a>
*/
public static final String TRANSFER_ENCODING = "Transfer-Encoding";
/**
* The HTTP {@code Upgrade} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-6.7">Section 6.7 of RFC 7230</a>
*/
public static final String UPGRADE = "Upgrade";
/**
* The HTTP {@code User-Agent} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.5.3">Section 5.5.3 of RFC 7231</a>
*/
public static final String USER_AGENT = "User-Agent";
/**
* The HTTP {@code Vary} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.4">Section 7.1.4 of RFC 7231</a>
*/
public static final String VARY = "Vary";
/**
* The HTTP {@code Via} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7230#section-5.7.1">Section 5.7.1 of RFC 7230</a>
*/
public static final String VIA = "Via";
/**
* The HTTP {@code Warning} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.5">Section 5.5 of RFC 7234</a>
*/
public static final String WARNING = "Warning";
/**
* The HTTP {@code WWW-Authenticate} header field name.
* @see <a href="https://tools.ietf.org/html/rfc7235#section-4.1">Section 4.1 of RFC 7235</a>
*/
public static final String WWW_AUTHENTICATE = "WWW-Authenticate";
/**
* An empty {@code HttpHeaders} instance (immutable).
* @since 5.0
*/
// public static final HttpHeaders EMPTY = new ReadOnlyHttpHeaders(new LinkedMultiValueMap<>());
/**
* Pattern matching ETag multiple field values in headers such as "If-Match", "If-None-Match".
* @see <a href="https://tools.ietf.org/html/rfc7232#section-2.3">Section 2.3 of RFC 7232</a>
*/
private static final Pattern ETAG_HEADER_VALUE_PATTERN = Pattern.compile("\\*|\\s*((W\\/)?(\"[^\"]*\"))\\s*,?");
private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = new DecimalFormatSymbols(Locale.ENGLISH);
private static final ZoneId GMT = ZoneId.of("GMT");
/**
* Date formats with time zone as specified in the HTTP RFC to use for formatting.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
*/
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US).withZone(GMT);
/**
* Date formats with time zone as specified in the HTTP RFC to use for parsing.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
*/
private static final DateTimeFormatter[] DATE_PARSERS = new DateTimeFormatter[] {
DateTimeFormatter.RFC_1123_DATE_TIME,
DateTimeFormatter.ofPattern("EEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(GMT)
};
final MultiValueMap<String, String> headers;
/**
* Construct a new, empty instance of the {@code HttpHeaders} object.
* <p>This is the common constructor, using a case-insensitive map structure.
*/
public HttpHeaders() {
this(CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)));
}
/**
* Construct a new {@code HttpHeaders} instance backed by an existing map.
* <p>This constructor is available as an optimization for adapting to existing
* headers map structures, primarily for internal use within the framework.
* @param headers the headers map (expected to operate with case-insensitive keys)
* @since 5.1
*/
public HttpHeaders(MultiValueMap<String, String> headers) {
Assert.notNull(headers, "MultiValueMap must not be null");
this.headers = headers;
}
/**
* Get the list of header values for the given header name, if any.
* @param headerName the header name
* @return the list of header values, or an empty list
* @since 5.2
*/
public List<String> getOrEmpty(Object headerName) {
List<String> values = get(headerName);
return (values != null ? values : Collections.emptyList());
}
/**
* Set the list of acceptable {@linkplain MediaType media types},
* as specified by the {@code Accept} header.
*/
public void setAccept(List<MediaType> acceptableMediaTypes) {
set(ACCEPT, MediaType.toString(acceptableMediaTypes));
}
/**
* Return the list of acceptable {@linkplain MediaType media types},
* as specified by the {@code Accept} header.
* <p>Returns an empty list when the acceptable media types are unspecified.
*/
/* public List<MediaType> getAccept() {
return MediaType.parseMediaTypes(get(ACCEPT));
}*/
/**
* Set the acceptable language ranges, as specified by the
* {@literal Accept-Language} header.
* @since 5.0
*/
public void setAcceptLanguage(List<Locale.LanguageRange> languages) {
Assert.notNull(languages, "LanguageRange List must not be null");
DecimalFormat decimal = new DecimalFormat("0.0", DECIMAL_FORMAT_SYMBOLS);
List<String> values = languages.stream()
.map(range ->
range.getWeight() == Locale.LanguageRange.MAX_WEIGHT ?
range.getRange() :
range.getRange() + ";q=" + decimal.format(range.getWeight()))
.collect(Collectors.toList());
set(ACCEPT_LANGUAGE, toCommaDelimitedString(values));
}
/**
* Return the language ranges from the {@literal "Accept-Language"} header.
* <p>If you only need sorted, preferred locales only use
* {@link #getAcceptLanguageAsLocales()} or if you need to filter based on
* a list of supported locales you can pass the returned list to
* {@link Locale#filter(List, Collection)}.
* @throws IllegalArgumentException if the value cannot be converted to a language range
* @since 5.0
*/
public List<Locale.LanguageRange> getAcceptLanguage() {
String value = getFirst(ACCEPT_LANGUAGE);
return (StringUtils.hasText(value) ? Locale.LanguageRange.parse(value) : Collections.emptyList());
}
/**
* Variant of {@link #setAcceptLanguage(List)} using {@link Locale}'s.
* @since 5.0
*/
public void setAcceptLanguageAsLocales(List<Locale> locales) {
setAcceptLanguage(locales.stream()
.map(locale -> new Locale.LanguageRange(locale.toLanguageTag()))
.collect(Collectors.toList()));
}
/**
* A variant of {@link #getAcceptLanguage()} that converts each
* {@link java.util.Locale.LanguageRange} to a {@link Locale}.
* @return the locales or an empty list
* @throws IllegalArgumentException if the value cannot be converted to a locale
* @since 5.0
*/
public List<Locale> getAcceptLanguageAsLocales() {
List<Locale.LanguageRange> ranges = getAcceptLanguage();
if (ranges.isEmpty()) {
return Collections.emptyList();
}
return ranges.stream()
.map(range -> Locale.forLanguageTag(range.getRange()))
.filter(locale -> StringUtils.hasText(locale.getDisplayName()))
.collect(Collectors.toList());
}
/**
* Set the list of acceptable {@linkplain MediaType media types} for
* {@code PATCH} methods, as specified by the {@code Accept-Patch} header.
* @since 5.3.6
*/
public void setAcceptPatch(List<MediaType> mediaTypes) {
set(ACCEPT_PATCH, MediaType.toString(mediaTypes));
}
/**
* Return the list of acceptable {@linkplain MediaType media types} for
* {@code PATCH} methods, as specified by the {@code Accept-Patch} header.
* <p>Returns an empty list when the acceptable media types are unspecified.
* @since 5.3.6
*/
/*public List<MediaType> getAcceptPatch() {
return MediaType.parseMediaTypes(get(ACCEPT_PATCH));
}*/
/**
* Set the (new) value of the {@code Access-Control-Allow-Credentials} response header.
*/
public void setAccessControlAllowCredentials(boolean allowCredentials) {
set(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(allowCredentials));
}
/**
* Return the value of the {@code Access-Control-Allow-Credentials} response header.
*/
public boolean getAccessControlAllowCredentials() {
return Boolean.parseBoolean(getFirst(ACCESS_CONTROL_ALLOW_CREDENTIALS));
}
/**
* Set the (new) value of the {@code Access-Control-Allow-Headers} response header.
*/
public void setAccessControlAllowHeaders(List<String> allowedHeaders) {
set(ACCESS_CONTROL_ALLOW_HEADERS, toCommaDelimitedString(allowedHeaders));
}
/**
* Return the value of the {@code Access-Control-Allow-Headers} response header.
*/
public List<String> getAccessControlAllowHeaders() {
return getValuesAsList(ACCESS_CONTROL_ALLOW_HEADERS);
}
/**
* Set the (new) value of the {@code Access-Control-Allow-Methods} response header.
*/
public void setAccessControlAllowMethods(List<HttpMethod> allowedMethods) {
set(ACCESS_CONTROL_ALLOW_METHODS, StringUtils.collectionToCommaDelimitedString(allowedMethods));
}
/**
* Return the value of the {@code Access-Control-Allow-Methods} response header.
*/
/* public List<HttpMethod> getAccessControlAllowMethods() {
List<HttpMethod> result = new ArrayList<>();
String value = getFirst(ACCESS_CONTROL_ALLOW_METHODS);
if (value != null) {
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
for (String token : tokens) {
HttpMethod resolved = HttpMethod.resolve(token);
if (resolved != null) {
result.add(resolved);
}
}
}
return result;
}*/
/**
* Set the (new) value of the {@code Access-Control-Allow-Origin} response header.
*/
public void setAccessControlAllowOrigin(@Nullable String allowedOrigin) {
setOrRemove(ACCESS_CONTROL_ALLOW_ORIGIN, allowedOrigin);
}
/**
* Return the value of the {@code Access-Control-Allow-Origin} response header.
*/
@Nullable
public String getAccessControlAllowOrigin() {
return getFieldValues(ACCESS_CONTROL_ALLOW_ORIGIN);
}
/**
* Set the (new) value of the {@code Access-Control-Expose-Headers} response header.
*/
public void setAccessControlExposeHeaders(List<String> exposedHeaders) {
set(ACCESS_CONTROL_EXPOSE_HEADERS, toCommaDelimitedString(exposedHeaders));
}
/**
* Return the value of the {@code Access-Control-Expose-Headers} response header.
*/
public List<String> getAccessControlExposeHeaders() {
return getValuesAsList(ACCESS_CONTROL_EXPOSE_HEADERS);
}
/**
* Set the (new) value of the {@code Access-Control-Max-Age} response header.
* @since 5.2
*/
public void setAccessControlMaxAge(Duration maxAge) {
set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge.getSeconds()));
}
/**
* Set the (new) value of the {@code Access-Control-Max-Age} response header.
*/
public void setAccessControlMaxAge(long maxAge) {
set(ACCESS_CONTROL_MAX_AGE, Long.toString(maxAge));
}
/**
* Return the value of the {@code Access-Control-Max-Age} response header.
* <p>Returns -1 when the max age is unknown.
*/
public long getAccessControlMaxAge() {
String value = getFirst(ACCESS_CONTROL_MAX_AGE);
return (value != null ? Long.parseLong(value) : -1);
}
/**
* Set the (new) value of the {@code Access-Control-Request-Headers} request header.
*/
public void setAccessControlRequestHeaders(List<String> requestHeaders) {
set(ACCESS_CONTROL_REQUEST_HEADERS, toCommaDelimitedString(requestHeaders));
}
/**
* Return the value of the {@code Access-Control-Request-Headers} request header.
*/
public List<String> getAccessControlRequestHeaders() {
return getValuesAsList(ACCESS_CONTROL_REQUEST_HEADERS);
}
/**
* Set the (new) value of the {@code Access-Control-Request-Method} request header.
*/
public void setAccessControlRequestMethod(@Nullable HttpMethod requestMethod) {
setOrRemove(ACCESS_CONTROL_REQUEST_METHOD, (requestMethod != null ? requestMethod.name() : null));
}
/**
* Return the value of the {@code Access-Control-Request-Method} request header.
*/
/* @Nullable
public HttpMethod getAccessControlRequestMethod() {
return HttpMethod.resolve(getFirst(ACCESS_CONTROL_REQUEST_METHOD));
}*/
/**
* Set the list of acceptable {@linkplain Charset charsets},
* as specified by the {@code Accept-Charset} header.
*/
public void setAcceptCharset(List<Charset> acceptableCharsets) {
StringJoiner joiner = new StringJoiner(", ");
for (Charset charset : acceptableCharsets) {
joiner.add(charset.name().toLowerCase(Locale.ENGLISH));
}
set(ACCEPT_CHARSET, joiner.toString());
}
/**
* Return the list of acceptable {@linkplain Charset charsets},
* as specified by the {@code Accept-Charset} header.
*/
public List<Charset> getAcceptCharset() {
String value = getFirst(ACCEPT_CHARSET);
if (value != null) {
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
List<Charset> result = new ArrayList<>(tokens.length);
for (String token : tokens) {
int paramIdx = token.indexOf(';');
String charsetName;
if (paramIdx == -1) {
charsetName = token;
}
else {
charsetName = token.substring(0, paramIdx);
}
if (!charsetName.equals("*")) {
result.add(Charset.forName(charsetName));
}
}
return result;
}
else {
return Collections.emptyList();
}
}
/**
* Set the set of allowed {@link HttpMethod HTTP methods},
* as specified by the {@code Allow} header.
*/
public void setAllow(Set<HttpMethod> allowedMethods) {
set(ALLOW, StringUtils.collectionToCommaDelimitedString(allowedMethods));
}
/**
* Return the set of allowed {@link HttpMethod HTTP methods},
* as specified by the {@code Allow} header.
* <p>Returns an empty set when the allowed methods are unspecified.
*/
/*public Set<HttpMethod> getAllow() {
String value = getFirst(ALLOW);
if (StringUtils.hasLength(value)) {
String[] tokens = StringUtils.tokenizeToStringArray(value, ",");
List<HttpMethod> result = new ArrayList<>(tokens.length);
for (String token : tokens) {
HttpMethod resolved = HttpMethod.resolve(token);
if (resolved != null) {
result.add(resolved);
}
}
return EnumSet.copyOf(result);
}
else {
return EnumSet.noneOf(HttpMethod.class);
}
}*/
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* Basic Authentication based on the given username and password.
* <p>Note that this method only supports characters in the
* {@link StandardCharsets#ISO_8859_1 ISO-8859-1} character set.
* @param username the username
* @param password the password
* @throws IllegalArgumentException if either {@code user} or
* {@code password} contain characters that cannot be encoded to ISO-8859-1
* @since 5.1
* @see #setBasicAuth(String)
* @see #setBasicAuth(String, String, Charset)
* @see #encodeBasicAuth(String, String, Charset)
* @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
*/
public void setBasicAuth(String username, String password) {
setBasicAuth(username, password, null);
}
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* Basic Authentication based on the given username and password.
* @param username the username
* @param password the password
* @param charset the charset to use to convert the credentials into an octet
* sequence. Defaults to {@linkplain StandardCharsets#ISO_8859_1 ISO-8859-1}.
* @throws IllegalArgumentException if {@code username} or {@code password}
* contains characters that cannot be encoded to the given charset
* @since 5.1
* @see #setBasicAuth(String)
* @see #setBasicAuth(String, String)
* @see #encodeBasicAuth(String, String, Charset)
* @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
*/
public void setBasicAuth(String username, String password, @Nullable Charset charset) {
setBasicAuth(encodeBasicAuth(username, password, charset));
}
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* Basic Authentication based on the given {@linkplain #encodeBasicAuth
* encoded credentials}.
* <p>Favor this method over {@link #setBasicAuth(String, String)} and
* {@link #setBasicAuth(String, String, Charset)} if you wish to cache the
* encoded credentials.
* @param encodedCredentials the encoded credentials
* @throws IllegalArgumentException if supplied credentials string is
* {@code null} or blank
* @since 5.2
* @see #setBasicAuth(String, String)
* @see #setBasicAuth(String, String, Charset)
* @see #encodeBasicAuth(String, String, Charset)
* @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
*/
public void setBasicAuth(String encodedCredentials) {
Assert.hasText(encodedCredentials, "'encodedCredentials' must not be null or blank");
set(AUTHORIZATION, "Basic " + encodedCredentials);
}
/**
* Set the value of the {@linkplain #AUTHORIZATION Authorization} header to
* the given Bearer token.
* @param token the Base64 encoded token
* @since 5.1
* @see <a href="https://tools.ietf.org/html/rfc6750">RFC 6750</a>
*/
public void setBearerAuth(String token) {
set(AUTHORIZATION, "Bearer " + token);
}
/**
* Set a configured {@link CacheControl} instance as the
* new value of the {@code Cache-Control} header.
* @since 5.0.5
*/
/*public void setCacheControl(CacheControl cacheControl) {
setOrRemove(CACHE_CONTROL, cacheControl.getHeaderValue());
}*/
/**
* Set the (new) value of the {@code Cache-Control} header.
*/
public void setCacheControl(@Nullable String cacheControl) {
setOrRemove(CACHE_CONTROL, cacheControl);
}
/**
* Return the value of the {@code Cache-Control} header.
*/
@Nullable
public String getCacheControl() {
return getFieldValues(CACHE_CONTROL);
}
/**
* Set the (new) value of the {@code Connection} header.
*/
public void setConnection(String connection) {
set(CONNECTION, connection);
}
/**
* Set the (new) value of the {@code Connection} header.
*/
public void setConnection(List<String> connection) {
set(CONNECTION, toCommaDelimitedString(connection));
}
/**
* Return the value of the {@code Connection} header.
*/
public List<String> getConnection() {
return getValuesAsList(CONNECTION);
}
/**
* Set the {@code Content-Disposition} header when creating a
* {@code "multipart/form-data"} request.
* <p>Applications typically would not set this header directly but
* rather prepare a {@code MultiValueMap<String, Object>}, containing an
* Object or a {@link org.springframework.core.io.Resource} for each part,
* and then pass that to the {@code RestTemplate} or {@code WebClient}.
* @param name the control name
* @param filename the filename (may be {@code null})
* @see #getContentDisposition()
*/
/* public void setContentDispositionFormData(String name, @Nullable String filename) {
Assert.notNull(name, "Name must not be null");
ContentDisposition.Builder disposition = ContentDisposition.formData().name(name);
if (StringUtils.hasText(filename)) {
disposition.filename(filename);
}
setContentDisposition(disposition.build());
}*/
/**
* Set the {@literal Content-Disposition} header.
* <p>This could be used on a response to indicate if the content is
* expected to be displayed inline in the browser or as an attachment to be
* saved locally.
* <p>It can also be used for a {@code "multipart/form-data"} request.
* For more details see notes on {@link #set ContentDispositionFormData}.
* @since 5.0
* @see #get ContentDisposition()
*/
public void setContentDisposition(ContentDisposition contentDisposition) {
set(CONTENT_DISPOSITION, contentDisposition.toString());
}
/**
* Return a parsed representation of the {@literal Content-Disposition} header.
* @since 5.0
* @see #setContentDisposition(C ontentDisposition)
*/
/* public ContentDisposition getContentDisposition() {
String contentDisposition = getFirst(CONTENT_DISPOSITION);
if (StringUtils.hasText(contentDisposition)) {
return ContentDisposition.parse(contentDisposition);
}
return ContentDisposition.empty();
}*/
/**
* Set the {@link Locale} of the content language,
* as specified by the {@literal Content-Language} header.
* <p>Use {@code put(CONTENT_LANGUAGE, list)} if you need
* to set multiple content languages.</p>
* @since 5.0
*/
public void setContentLanguage(@Nullable Locale locale) {
setOrRemove(CONTENT_LANGUAGE, (locale != null ? locale.toLanguageTag() : null));
}
/**
* Get the first {@link Locale} of the content languages, as specified by the
* {@code Content-Language} header.
* <p>Use {@link #getValuesAsList(String)} if you need to get multiple content
* languages.
* @return the first {@code Locale} of the content languages, or {@code null}
* if unknown
* @since 5.0
*/
@Nullable
public Locale getContentLanguage() {
return getValuesAsList(CONTENT_LANGUAGE)
.stream()
.findFirst()
.map(Locale::forLanguageTag)
.orElse(null);
}
/**
* Set the length of the body in bytes, as specified by the
* {@code Content-Length} header.
*/
public void setContentLength(long contentLength) {
set(CONTENT_LENGTH, Long.toString(contentLength));
}
/**
* Return the length of the body in bytes, as specified by the
* {@code Content-Length} header.
* <p>Returns -1 when the content-length is unknown.
*/
public long getContentLength() {
String value = getFirst(CONTENT_LENGTH);
return (value != null ? Long.parseLong(value) : -1);
}
/**
* Set the {@linkplain MediaType media type} of the body,
* as specified by the {@code Content-Type} header.
*/
public void setContentType(@Nullable MediaType mediaType) {
if (mediaType != null) {
Assert.isTrue(!mediaType.isWildcardType(), "Content-Type cannot contain wildcard type '*'");
Assert.isTrue(!mediaType.isWildcardSubtype(), "Content-Type cannot contain wildcard subtype '*'");
set(CONTENT_TYPE, mediaType.toString());
}
else {
remove(CONTENT_TYPE);
}
}
/**
* Return the {@linkplain MediaType media type} of the body, as specified
* by the {@code Content-Type} header.
* <p>Returns {@code null} when the content-type is unknown.
*/
@Nullable
public MediaType getContentType() {
String value = getFirst(CONTENT_TYPE);
return (StringUtils.hasLength(value) ? MediaType.parseMediaType(value) : null);
}
/**
* Set the date and time at which the message was created, as specified
* by the {@code Date} header.
* @since 5.2
*/
public void setDate(ZonedDateTime date) {
setZonedDateTime(DATE, date);
}
/**
* Set the date and time at which the message was created, as specified
* by the {@code Date} header.
* @since 5.2
*/
public void setDate(Instant date) {
setInstant(DATE, date);
}
/**
* Set the date and time at which the message was created, as specified
* by the {@code Date} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
*/
public void setDate(long date) {
setDate(DATE, date);
}
/**
* Return the date and time at which the message was created, as specified
* by the {@code Date} header.
* <p>The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @throws IllegalArgumentException if the value cannot be converted to a date
*/
public long getDate() {
return getFirstDate(DATE);
}
/**
* Set the (new) entity tag of the body, as specified by the {@code ETag} header.
*/
public void setETag(@Nullable String etag) {
if (etag != null) {
Assert.isTrue(etag.startsWith("\"") || etag.startsWith("W/"),
"Invalid ETag: does not start with W/ or \"");
Assert.isTrue(etag.endsWith("\""), "Invalid ETag: does not end with \"");
set(ETAG, etag);
}
else {
remove(ETAG);
}
}
/**
* Return the entity tag of the body, as specified by the {@code ETag} header.
*/
@Nullable
public String getETag() {
return getFirst(ETAG);
}
/**
* Set the duration after which the message is no longer valid,
* as specified by the {@code Expires} header.
* @since 5.0.5
*/
public void setExpires(ZonedDateTime expires) {
setZonedDateTime(EXPIRES, expires);
}
/**
* Set the date and time at which the message is no longer valid,
* as specified by the {@code Expires} header.
* @since 5.2
*/
public void setExpires(Instant expires) {
setInstant(EXPIRES, expires);
}
/**
* Set the date and time at which the message is no longer valid,
* as specified by the {@code Expires} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
*/
public void setExpires(long expires) {
setDate(EXPIRES, expires);
}
/**
* Return the date and time at which the message is no longer valid,
* as specified by the {@code Expires} header.
* <p>The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @see #getFirstZonedDateTime(String)
*/
public long getExpires() {
return getFirstDate(EXPIRES, false);
}
/**
* Set the (new) value of the {@code Host} header.
* <p>If the given {@linkplain InetSocketAddress#getPort() port} is {@code 0},
* the host header will only contain the
* {@linkplain InetSocketAddress#getHostString() host name}.
* @since 5.0
*/
public void setHost(@Nullable InetSocketAddress host) {
if (host != null) {
String value = host.getHostString();
int port = host.getPort();
if (port != 0) {
value = value + ":" + port;
}
set(HOST, value);
}
else {
remove(HOST, null);
}
}
/**
* Return the value of the {@code Host} header, if available.
* <p>If the header value does not contain a port, the
* {@linkplain InetSocketAddress#getPort() port} in the returned address will
* be {@code 0}.
* @since 5.0
*/
@Nullable
public InetSocketAddress getHost() {
String value = getFirst(HOST);
if (value == null) {
return null;
}
String host = null;
int port = 0;
int separator = (value.startsWith("[") ? value.indexOf(':', value.indexOf(']')) : value.lastIndexOf(':'));
if (separator != -1) {
host = value.substring(0, separator);
String portString = value.substring(separator + 1);
try {
port = Integer.parseInt(portString);
}
catch (NumberFormatException ex) {
// ignore
}
}
if (host == null) {
host = value;
}
return InetSocketAddress.createUnresolved(host, port);
}
/**
* Set the (new) value of the {@code If-Match} header.
* @since 4.3
*/
public void setIfMatch(String ifMatch) {
set(IF_MATCH, ifMatch);
}
/**
* Set the (new) value of the {@code If-Match} header.
* @since 4.3
*/
public void setIfMatch(List<String> ifMatchList) {
set(IF_MATCH, toCommaDelimitedString(ifMatchList));
}
/**
* Return the value of the {@code If-Match} header.
* @throws IllegalArgumentException if parsing fails
* @since 4.3
*/
public List<String> getIfMatch() {
return getETagValuesAsList(IF_MATCH);
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfModifiedSince(ZonedDateTime ifModifiedSince) {
setZonedDateTime(IF_MODIFIED_SINCE, ifModifiedSince.withZoneSameInstant(GMT));
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfModifiedSince(Instant ifModifiedSince) {
setInstant(IF_MODIFIED_SINCE, ifModifiedSince);
}
/**
* Set the (new) value of the {@code If-Modified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
*/
public void setIfModifiedSince(long ifModifiedSince) {
setDate(IF_MODIFIED_SINCE, ifModifiedSince);
}
/**
* Return the value of the {@code If-Modified-Since} header.
* <p>The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @see #getFirstZonedDateTime(String)
*/
public long getIfModifiedSince() {
return getFirstDate(IF_MODIFIED_SINCE, false);
}
/**
* Set the (new) value of the {@code If-None-Match} header.
*/
public void setIfNoneMatch(String ifNoneMatch) {
set(IF_NONE_MATCH, ifNoneMatch);
}
/**
* Set the (new) values of the {@code If-None-Match} header.
*/
public void setIfNoneMatch(List<String> ifNoneMatchList) {
set(IF_NONE_MATCH, toCommaDelimitedString(ifNoneMatchList));
}
/**
* Return the value of the {@code If-None-Match} header.
* @throws IllegalArgumentException if parsing fails
*/
public List<String> getIfNoneMatch() {
return getETagValuesAsList(IF_NONE_MATCH);
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfUnmodifiedSince(ZonedDateTime ifUnmodifiedSince) {
setZonedDateTime(IF_UNMODIFIED_SINCE, ifUnmodifiedSince.withZoneSameInstant(GMT));
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setIfUnmodifiedSince(Instant ifUnmodifiedSince) {
setInstant(IF_UNMODIFIED_SINCE, ifUnmodifiedSince);
}
/**
* Set the (new) value of the {@code If-Unmodified-Since} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
* @since 4.3
*/
public void setIfUnmodifiedSince(long ifUnmodifiedSince) {
setDate(IF_UNMODIFIED_SINCE, ifUnmodifiedSince);
}
/**
* Return the value of the {@code If-Unmodified-Since} header.
* <p>The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @since 4.3
* @see #getFirstZonedDateTime(String)
*/
public long getIfUnmodifiedSince() {
return getFirstDate(IF_UNMODIFIED_SINCE, false);
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setLastModified(ZonedDateTime lastModified) {
setZonedDateTime(LAST_MODIFIED, lastModified.withZoneSameInstant(GMT));
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* @since 5.1.4
*/
public void setLastModified(Instant lastModified) {
setInstant(LAST_MODIFIED, lastModified);
}
/**
* Set the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* <p>The date should be specified as the number of milliseconds since
* January 1, 1970 GMT.
*/
public void setLastModified(long lastModified) {
setDate(LAST_MODIFIED, lastModified);
}
/**
* Return the time the resource was last changed, as specified by the
* {@code Last-Modified} header.
* <p>The date is returned as the number of milliseconds since
* January 1, 1970 GMT. Returns -1 when the date is unknown.
* @see #getFirstZonedDateTime(String)
*/
public long getLastModified() {
return getFirstDate(LAST_MODIFIED, false);
}
/**
* Set the (new) location of a resource,
* as specified by the {@code Location} header.
*/
public void setLocation(@Nullable URI location) {
setOrRemove(LOCATION, (location != null ? location.toASCIIString() : null));
}
/**
* Return the (new) location of a resource
* as specified by the {@code Location} header.
* <p>Returns {@code null} when the location is unknown.
*/
@Nullable
public URI getLocation() {
String value = getFirst(LOCATION);
return (value != null ? URI.create(value) : null);
}
/**
* Set the (new) value of the {@code Origin} header.
*/
public void setOrigin(@Nullable String origin) {
setOrRemove(ORIGIN, origin);
}
/**
* Return the value of the {@code Origin} header.
*/
@Nullable
public String getOrigin() {
return getFirst(ORIGIN);
}
/**
* Set the (new) value of the {@code Pragma} header.
*/
public void setPragma(@Nullable String pragma) {
setOrRemove(PRAGMA, pragma);
}
/**
* Return the value of the {@code Pragma} header.
*/
@Nullable
public String getPragma() {
return getFirst(PRAGMA);
}
/**
* Sets the (new) value of the {@code Range} header.
*/
/*public void setRange(List<HttpRange> ranges) {
String value = HttpRange.toString(ranges);
set(RANGE, value);
}*/
/**
* Return the value of the {@code Range} header.
* <p>Returns an empty list when the range is unknown.
*/
/* public List<HttpRange> getRange() {
String value = getFirst(RANGE);
return HttpRange.parseRanges(value);
}*/
/**
* Set the (new) value of the {@code Upgrade} header.
*/
public void setUpgrade(@Nullable String upgrade) {
setOrRemove(UPGRADE, upgrade);
}
/**
* Return the value of the {@code Upgrade} header.
*/
@Nullable
public String getUpgrade() {
return getFirst(UPGRADE);
}
/**
* Set the request header names (e.g. "Accept-Language") for which the
* response is subject to content negotiation and variances based on the
* value of those request headers.
* @param requestHeaders the request header names
* @since 4.3
*/
public void setVary(List<String> requestHeaders) {
set(VARY, toCommaDelimitedString(requestHeaders));
}
/**
* Return the request header names subject to content negotiation.
* @since 4.3
*/
public List<String> getVary() {
return getValuesAsList(VARY);
}
/**
* Set the given date under the given header name after formatting it as a string
* using the RFC-1123 date-time formatter. The equivalent of
* {@link #set(String, String)} but for date headers.
* @since 5.0
*/
public void setZonedDateTime(String headerName, ZonedDateTime date) {
set(headerName, DATE_FORMATTER.format(date));
}
/**
* Set the given date under the given header name after formatting it as a string
* using the RFC-1123 date-time formatter. The equivalent of
* {@link #set(String, String)} but for date headers.
* @since 5.1.4
*/
public void setInstant(String headerName, Instant date) {
setZonedDateTime(headerName, ZonedDateTime.ofInstant(date, GMT));
}
/**
* Set the given date under the given header name after formatting it as a string
* using the RFC-1123 date-time formatter. The equivalent of
* {@link #set(String, String)} but for date headers.
* @since 3.2.4
* @see #setZonedDateTime(String, ZonedDateTime)
*/
public void setDate(String headerName, long date) {
setInstant(headerName, Instant.ofEpochMilli(date));
}
/**
* Parse the first header value for the given header name as a date,
* return -1 if there is no value, or raise {@link IllegalArgumentException}
* if the value cannot be parsed as a date.
* @param headerName the header name
* @return the parsed date header, or -1 if none
* @since 3.2.4
* @see #getFirstZonedDateTime(String)
*/
public long getFirstDate(String headerName) {
return getFirstDate(headerName, true);
}
/**
* Parse the first header value for the given header name as a date,
* return -1 if there is no value or also in case of an invalid value
* (if {@code rejectInvalid=false}), or raise {@link IllegalArgumentException}
* if the value cannot be parsed as a date.
* @param headerName the header name
* @param rejectInvalid whether to reject invalid values with an
* {@link IllegalArgumentException} ({@code true}) or rather return -1
* in that case ({@code false})
* @return the parsed date header, or -1 if none (or invalid)
* @see #getFirstZonedDateTime(String, boolean)
*/
private long getFirstDate(String headerName, boolean rejectInvalid) {
ZonedDateTime zonedDateTime = getFirstZonedDateTime(headerName, rejectInvalid);
return (zonedDateTime != null ? zonedDateTime.toInstant().toEpochMilli() : -1);
}
/**
* Parse the first header value for the given header name as a date,
* return {@code null} if there is no value, or raise {@link IllegalArgumentException}
* if the value cannot be parsed as a date.
* @param headerName the header name
* @return the parsed date header, or {@code null} if none
* @since 5.0
*/
@Nullable
public ZonedDateTime getFirstZonedDateTime(String headerName) {
return getFirstZonedDateTime(headerName, true);
}
/**
* Parse the first header value for the given header name as a date,
* return {@code null} if there is no value or also in case of an invalid value
* (if {@code rejectInvalid=false}), or raise {@link IllegalArgumentException}
* if the value cannot be parsed as a date.
* @param headerName the header name
* @param rejectInvalid whether to reject invalid values with an
* {@link IllegalArgumentException} ({@code true}) or rather return {@code null}
* in that case ({@code false})
* @return the parsed date header, or {@code null} if none (or invalid)
*/
@Nullable
private ZonedDateTime getFirstZonedDateTime(String headerName, boolean rejectInvalid) {
String headerValue = getFirst(headerName);
if (headerValue == null) {
// No header value sent at all
return null;
}
if (headerValue.length() >= 3) {
// Short "0" or "-1" like values are never valid HTTP date headers...
// Let's only bother with DateTimeFormatter parsing for long enough values.
// See https://stackoverflow.com/questions/12626699/if-modified-since-http-header-passed-by-ie9-includes-length
int parametersIndex = headerValue.indexOf(';');
if (parametersIndex != -1) {
headerValue = headerValue.substring(0, parametersIndex);
}
for (DateTimeFormatter dateFormatter : DATE_PARSERS) {
try {
return ZonedDateTime.parse(headerValue, dateFormatter);
}
catch (DateTimeParseException ex) {
// ignore
}
}
}
if (rejectInvalid) {
throw new IllegalArgumentException("Cannot parse date value \"" + headerValue +
"\" for \"" + headerName + "\" header");
}
return null;
}
/**
* Return all values of a given header name,
* even if this header is set multiple times.
* @param headerName the header name
* @return all associated values
* @since 4.3
*/
public List<String> getValuesAsList(String headerName) {
List<String> values = get(headerName);
if (values != null) {
List<String> result = new ArrayList<>();
for (String value : values) {
if (value != null) {
Collections.addAll(result, StringUtils.tokenizeToStringArray(value, ","));
}
}
return result;
}
return Collections.emptyList();
}
/**
* Remove the well-known {@code "Content-*"} HTTP headers.
* <p>Such headers should be cleared from the response if the intended
* body can't be written due to errors.
* @since 5.2.3
*/
public void clearContentHeaders() {
this.headers.remove(HttpHeaders.CONTENT_DISPOSITION);
this.headers.remove(HttpHeaders.CONTENT_ENCODING);
this.headers.remove(HttpHeaders.CONTENT_LANGUAGE);
this.headers.remove(HttpHeaders.CONTENT_LENGTH);
this.headers.remove(HttpHeaders.CONTENT_LOCATION);
this.headers.remove(HttpHeaders.CONTENT_RANGE);
this.headers.remove(HttpHeaders.CONTENT_TYPE);
}
/**
* Retrieve a combined result from the field values of the ETag header.
* @param headerName the header name
* @return the combined result
* @throws IllegalArgumentException if parsing fails
* @since 4.3
*/
protected List<String> getETagValuesAsList(String headerName) {
List<String> values = get(headerName);
if (values != null) {
List<String> result = new ArrayList<>();
for (String value : values) {
if (value != null) {
Matcher matcher = ETAG_HEADER_VALUE_PATTERN.matcher(value);
while (matcher.find()) {
if ("*".equals(matcher.group())) {
result.add(matcher.group());
}
else {
result.add(matcher.group(1));
}
}
if (result.isEmpty()) {
throw new IllegalArgumentException(
"Could not parse header '" + headerName + "' with value '" + value + "'");
}
}
}
return result;
}
return Collections.emptyList();
}
/**
* Retrieve a combined result from the field values of multi-valued headers.
* @param headerName the header name
* @return the combined result
* @since 4.3
*/
@Nullable
protected String getFieldValues(String headerName) {
List<String> headerValues = get(headerName);
return (headerValues != null ? toCommaDelimitedString(headerValues) : null);
}
/**
* Turn the given list of header values into a comma-delimited result.
* @param headerValues the list of header values
* @return a combined result with comma delimitation
*/
protected String toCommaDelimitedString(List<String> headerValues) {
StringJoiner joiner = new StringJoiner(", ");
for (String val : headerValues) {
if (val != null) {
joiner.add(val);
}
}
return joiner.toString();
}
/**
* Set the given header value, or remove the header if {@code null}.
* @param headerName the header name
* @param headerValue the header value, or {@code null} for none
*/
private void setOrRemove(String headerName, @Nullable String headerValue) {
if (headerValue != null) {
set(headerName, headerValue);
}
else {
remove(headerName);
}
}
// MultiValueMap implementation
/**
* Return the first header value for the given header name, if any.
* @param headerName the header name
* @return the first header value, or {@code null} if none
*/
@Override
@Nullable
public String getFirst(String headerName) {
return this.headers.getFirst(headerName);
}
/**
* Add the given, single header value under the given name.
* @param headerName the header name
* @param headerValue the header value
* @throws UnsupportedOperationException if adding headers is not supported
* @see #put(String, List)
* @see #set(String, String)
*/
@Override
public void add(String headerName, @Nullable String headerValue) {
this.headers.add(headerName, headerValue);
}
/* @Override
public void addAll(String key, List<? extends String> values) {
this.headers.addAll(key, values);
}
@Override
public void addAll(MultiValueMap<String, String> values) {
this.headers.addAll(values);
}*/
/**
* Set the given, single header value under the given name.
* @param headerName the header name
* @param headerValue the header value
* @throws UnsupportedOperationException if adding headers is not supported
* @see #put(String, List)
* @see #add(String, String)
*/
@Override
public void set(String headerName, @Nullable String headerValue) {
this.headers.set(headerName, headerValue);
}
@Override
public void setAll(Map<String, String> values) {
this.headers.setAll(values);
}
@Override
public Map<String, String> toSingleValueMap() {
return this.headers.toSingleValueMap();
}
// Map implementation
@Override
public int size() {
return this.headers.size();
}
@Override
public boolean isEmpty() {
return this.headers.isEmpty();
}
@Override
public boolean containsKey(Object key) {
return this.headers.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return this.headers.containsValue(value);
}
@Override
@Nullable
public List<String> get(Object key) {
return this.headers.get(key);
}
@Override
public List<String> put(String key, List<String> value) {
return this.headers.put(key, value);
}
@Override
public List<String> remove(Object key) {
return this.headers.remove(key);
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> map) {
this.headers.putAll(map);
}
@Override
public void clear() {
this.headers.clear();
}
@Override
public Set<String> keySet() {
return this.headers.keySet();
}
@Override
public Collection<List<String>> values() {
return this.headers.values();
}
@Override
public Set<Entry<String, List<String>>> entrySet() {
return this.headers.entrySet();
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof HttpHeaders)) {
return false;
}
return unwrap(this).equals(unwrap((HttpHeaders) other));
}
private static MultiValueMap<String, String> unwrap(HttpHeaders headers) {
while (headers.headers instanceof HttpHeaders) {
headers = (HttpHeaders) headers.headers;
}
return headers.headers;
}
@Override
public int hashCode() {
return this.headers.hashCode();
}
@Override
public String toString() {
return formatHeaders(this.headers);
}
/**
* Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary.
* <p>Also caches the parsed representations of the "Accept" and "Content-Type" headers.
* @param headers the headers to expose
* @return a read-only variant of the headers, or the original headers as-is
* (in case it happens to be a read-only {@code HttpHeaders} instance already)
* @since 5.3
*/
/* public static HttpHeaders readOnlyHttpHeaders(MultiValueMap<String, String> headers) {
return (headers instanceof HttpHeaders ?
readOnlyHttpHeaders((HttpHeaders) headers) : new ReadOnlyHttpHeaders(headers));
}*/
/**
* Apply a read-only {@code HttpHeaders} wrapper around the given headers, if necessary.
* <p>Also caches the parsed representations of the "Accept" and "Content-Type" headers.
* @param headers the headers to expose
* @return a read-only variant of the headers, or the original headers as-is
*/
/* public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
Assert.notNull(headers, "HttpHeaders must not be null");
return (headers instanceof ReadOnlyHttpHeaders ? headers : new ReadOnlyHttpHeaders(headers.headers));
}*/
/**
* Remove any read-only wrapper that may have been previously applied around
* the given headers via {@link #readOnlyHttpHeaders(HttpHeaders)}.
* @param headers the headers to expose
* @return a writable variant of the headers, or the original headers as-is
* @since 5.1.1
*/
/*public static HttpHeaders writableHttpHeaders(HttpHeaders headers) {
Assert.notNull(headers, "HttpHeaders must not be null");
if (headers == EMPTY) {
return new HttpHeaders();
}
return (headers instanceof ReadOnlyHttpHeaders ? new HttpHeaders(headers.headers) : headers);
}*/
/**
* Helps to format HTTP header values, as HTTP header values themselves can
* contain comma-separated values, can become confusing with regular
* {@link Map} formatting that also uses commas between entries.
* @param headers the headers to format
* @return the headers to a String
* @since 5.1.4
*/
public static String formatHeaders(MultiValueMap<String, String> headers) {
return headers.entrySet().stream()
.map(entry -> {
List<String> values = entry.getValue();
return entry.getKey() + ":" + (values.size() == 1 ?
"\"" + values.get(0) + "\"" :
values.stream().map(s -> "\"" + s + "\"").collect(Collectors.joining(", ")));
})
.collect(Collectors.joining(", ", "[", "]"));
}
/**
* Encode the given username and password into Basic Authentication credentials.
* <p>The encoded credentials returned by this method can be supplied to
* {@link #setBasicAuth(String)} to set the Basic Authentication header.
* @param username the username
* @param password the password
* @param charset the charset to use to convert the credentials into an octet
* sequence. Defaults to {@linkplain StandardCharsets#ISO_8859_1 ISO-8859-1}.
* @throws IllegalArgumentException if {@code username} or {@code password}
* contains characters that cannot be encoded to the given charset
* @since 5.2
* @see #setBasicAuth(String)
* @see #setBasicAuth(String, String)
* @see #setBasicAuth(String, String, Charset)
* @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
*/
public static String encodeBasicAuth(String username, String password, @Nullable Charset charset) {
Assert.notNull(username, "Username must not be null");
Assert.doesNotContain(username, ":", "Username must not contain a colon");
Assert.notNull(password, "Password must not be null");
if (charset == null) {
charset = StandardCharsets.ISO_8859_1;
}
CharsetEncoder encoder = charset.newEncoder();
if (!encoder.canEncode(username) || !encoder.canEncode(password)) {
throw new IllegalArgumentException(
"Username or password contains characters that cannot be encoded to " + charset.displayName());
}
String credentialsString = username + ":" + password;
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(charset));
return new String(encodedBytes, charset);
}
// Package-private: used in ResponseCookie
static String formatDate(long date) {
Instant instant = Instant.ofEpochMilli(date);
ZonedDateTime time = ZonedDateTime.ofInstant(instant, GMT);
return DATE_FORMATTER.format(time);
}
}
/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import com.sun.istack.internal.Nullable;
import org.springframework.util.Assert;
/**
* Represents an HTTP cookie as a name-value pair consistent with the content of
* the "Cookie" request header. The {@link ResponseCookie} sub-class has the
* additional attributes expected in the "Set-Cookie" response header.
*
* @author Rossen Stoyanchev
* @since 5.0
* @see <a href="https://tools.ietf.org/html/rfc6265">RFC 6265</a>
*/
public class HttpCookie {
private final String name;
private final String value;
public HttpCookie(String name, @com.sun.istack.internal.Nullable String value) {
Assert.hasLength(name, "'name' is required and must not be empty.");
this.name = name;
this.value = (value != null ? value : "");
}
/**
* Return the cookie name.
*/
public String getName() {
return this.name;
}
/**
* Return the cookie value or an empty string (never {@code null}).
*/
public String getValue() {
return this.value;
}
@Override
public int hashCode() {
return this.name.hashCode();
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof HttpCookie)) {
return false;
}
HttpCookie otherCookie = (HttpCookie) other;
return (this.name.equalsIgnoreCase(otherCookie.getName()));
}
@Override
public String toString() {
return this.name + '=' + this.value;
}
}
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.http;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* A builder for creating "Cache-Control" HTTP response headers.
*
* <p>Adding Cache-Control directives to HTTP responses can significantly improve the client
* experience when interacting with a web application. This builder creates opinionated
* "Cache-Control" headers with response directives only, with several use cases in mind.
*
* <ul>
* <li>Caching HTTP responses with {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS)}
* will result in {@code Cache-Control: "max-age=3600"}</li>
* <li>Preventing cache with {@code CacheControl cc = CacheControl.noStore()}
* will result in {@code Cache-Control: "no-store"}</li>
* <li>Advanced cases like {@code CacheControl cc = CacheControl.maxAge(1, TimeUnit.HOURS).noTransform().cachePublic()}
* will result in {@code Cache-Control: "max-age=3600, no-transform, public"}</li>
* </ul>
*
* <p>Note that to be efficient, Cache-Control headers should be written along HTTP validators
* such as "Last-Modified" or "ETag" headers.
*
* @author Brian Clozel
* @author Juergen Hoeller
* @since 4.2
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2">rfc7234 section 5.2.2</a>
* @see <a href="https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching">
* HTTP caching - Google developers reference</a>
* @see <a href="https://www.mnot.net/cache_docs/">Mark Nottingham's cache documentation</a>
*/
public class CacheControl {
@Nullable
private Duration maxAge;
private boolean noCache = false;
private boolean noStore = false;
private boolean mustRevalidate = false;
private boolean noTransform = false;
private boolean cachePublic = false;
private boolean cachePrivate = false;
private boolean proxyRevalidate = false;
@Nullable
private Duration staleWhileRevalidate;
@Nullable
private Duration staleIfError;
@Nullable
private Duration sMaxAge;
/**
* Create an empty CacheControl instance.
* @see #empty()
*/
protected CacheControl() {
}
/**
* Return an empty directive.
* <p>This is well suited for using other optional directives without "max-age",
* "no-cache" or "no-store".
* @return {@code this}, to facilitate method chaining
*/
public static CacheControl empty() {
return new CacheControl();
}
/**
* Add a "max-age=" directive.
* <p>This directive is well suited for publicly caching resources, knowing that
* they won't change within the configured amount of time. Additional directives
* can be also used, in case resources shouldn't be cached ({@link #cachePrivate()})
* or transformed ({@link #noTransform()}) by shared caches.
* <p>In order to prevent caches to reuse the cached response even when it has
* become stale (i.e. the "max-age" delay is passed), the "must-revalidate"
* directive should be set ({@link #mustRevalidate()}
* @param maxAge the maximum time the response should be cached
* @param unit the time unit of the {@code maxAge} argument
* @return {@code this}, to facilitate method chaining
* @see #maxAge(Duration)
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
*/
public static CacheControl maxAge(long maxAge, TimeUnit unit) {
return maxAge(Duration.ofSeconds(unit.toSeconds(maxAge)));
}
/**
* Add a "max-age=" directive.
* <p>This directive is well suited for publicly caching resources, knowing that
* they won't change within the configured amount of time. Additional directives
* can be also used, in case resources shouldn't be cached ({@link #cachePrivate()})
* or transformed ({@link #noTransform()}) by shared caches.
* <p>In order to prevent caches to reuse the cached response even when it has
* become stale (i.e. the "max-age" delay is passed), the "must-revalidate"
* directive should be set ({@link #mustRevalidate()}
* @param maxAge the maximum time the response should be cached
* @return {@code this}, to facilitate method chaining
* @since 5.2
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.8">rfc7234 section 5.2.2.8</a>
*/
public static CacheControl maxAge(Duration maxAge) {
CacheControl cc = new CacheControl();
cc.maxAge = maxAge;
return cc;
}
/**
* Add a "no-cache" directive.
* <p>This directive is well suited for telling caches that the response
* can be reused only if the client revalidates it with the server.
* This directive won't disable cache altogether and may result with clients
* sending conditional requests (with "ETag", "If-Modified-Since" headers)
* and the server responding with "304 - Not Modified" status.
* <p>In order to disable caching and minimize requests/responses exchanges,
* the {@link #noStore()} directive should be used instead of {@code #noCache()}.
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.2">rfc7234 section 5.2.2.2</a>
*/
public static CacheControl noCache() {
CacheControl cc = new CacheControl();
cc.noCache = true;
return cc;
}
/**
* Add a "no-store" directive.
* <p>This directive is well suited for preventing caches (browsers and proxies)
* to cache the content of responses.
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.3">rfc7234 section 5.2.2.3</a>
*/
public static CacheControl noStore() {
CacheControl cc = new CacheControl();
cc.noStore = true;
return cc;
}
/**
* Add a "must-revalidate" directive.
* <p>This directive indicates that once it has become stale, a cache MUST NOT
* use the response to satisfy subsequent requests without successful validation
* on the origin server.
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1">rfc7234 section 5.2.2.1</a>
*/
public CacheControl mustRevalidate() {
this.mustRevalidate = true;
return this;
}
/**
* Add a "no-transform" directive.
* <p>This directive indicates that intermediaries (caches and others) should
* not transform the response content. This can be useful to force caches and
* CDNs not to automatically gzip or optimize the response content.
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.4">rfc7234 section 5.2.2.4</a>
*/
public CacheControl noTransform() {
this.noTransform = true;
return this;
}
/**
* Add a "public" directive.
* <p>This directive indicates that any cache MAY store the response,
* even if the response would normally be non-cacheable or cacheable
* only within a private cache.
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.5">rfc7234 section 5.2.2.5</a>
*/
public CacheControl cachePublic() {
this.cachePublic = true;
return this;
}
/**
* Add a "private" directive.
* <p>This directive indicates that the response message is intended
* for a single user and MUST NOT be stored by a shared cache.
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.6">rfc7234 section 5.2.2.6</a>
*/
public CacheControl cachePrivate() {
this.cachePrivate = true;
return this;
}
/**
* Add a "proxy-revalidate" directive.
* <p>This directive has the same meaning as the "must-revalidate" directive,
* except that it does not apply to private caches (i.e. browsers, HTTP clients).
* @return {@code this}, to facilitate method chaining
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.7">rfc7234 section 5.2.2.7</a>
*/
public CacheControl proxyRevalidate() {
this.proxyRevalidate = true;
return this;
}
/**
* Add an "s-maxage" directive.
* <p>This directive indicates that, in shared caches, the maximum age specified
* by this directive overrides the maximum age specified by other directives.
* @param sMaxAge the maximum time the response should be cached
* @param unit the time unit of the {@code sMaxAge} argument
* @return {@code this}, to facilitate method chaining
* @see #sMaxAge(Duration)
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
*/
public CacheControl sMaxAge(long sMaxAge, TimeUnit unit) {
return sMaxAge(Duration.ofSeconds(unit.toSeconds(sMaxAge)));
}
/**
* Add an "s-maxage" directive.
* <p>This directive indicates that, in shared caches, the maximum age specified
* by this directive overrides the maximum age specified by other directives.
* @param sMaxAge the maximum time the response should be cached
* @return {@code this}, to facilitate method chaining
* @since 5.2
* @see <a href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9">rfc7234 section 5.2.2.9</a>
*/
public CacheControl sMaxAge(Duration sMaxAge) {
this.sMaxAge = sMaxAge;
return this;
}
/**
* Add a "stale-while-revalidate" directive.
* <p>This directive indicates that caches MAY serve the response in which it
* appears after it becomes stale, up to the indicated number of seconds.
* If a cached response is served stale due to the presence of this extension,
* the cache SHOULD attempt to revalidate it while still serving stale responses
* (i.e. without blocking).
* @param staleWhileRevalidate the maximum time the response should be used while being revalidated
* @param unit the time unit of the {@code staleWhileRevalidate} argument
* @return {@code this}, to facilitate method chaining
* @see #staleWhileRevalidate(Duration)
* @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a>
*/
public CacheControl staleWhileRevalidate(long staleWhileRevalidate, TimeUnit unit) {
return staleWhileRevalidate(Duration.ofSeconds(unit.toSeconds(staleWhileRevalidate)));
}
/**
* Add a "stale-while-revalidate" directive.
* <p>This directive indicates that caches MAY serve the response in which it
* appears after it becomes stale, up to the indicated number of seconds.
* If a cached response is served stale due to the presence of this extension,
* the cache SHOULD attempt to revalidate it while still serving stale responses
* (i.e. without blocking).
* @param staleWhileRevalidate the maximum time the response should be used while being revalidated
* @return {@code this}, to facilitate method chaining
* @since 5.2
* @see <a href="https://tools.ietf.org/html/rfc5861#section-3">rfc5861 section 3</a>
*/
public CacheControl staleWhileRevalidate(Duration staleWhileRevalidate) {
this.staleWhileRevalidate = staleWhileRevalidate;
return this;
}
/**
* Add a "stale-if-error" directive.
* <p>This directive indicates that when an error is encountered, a cached stale response
* MAY be used to satisfy the request, regardless of other freshness information.
* @param staleIfError the maximum time the response should be used when errors are encountered
* @param unit the time unit of the {@code staleIfError} argument
* @return {@code this}, to facilitate method chaining
* @see #staleIfError(Duration)
* @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a>
*/
public CacheControl staleIfError(long staleIfError, TimeUnit unit) {
return staleIfError(Duration.ofSeconds(unit.toSeconds(staleIfError)));
}
/**
* Add a "stale-if-error" directive.
* <p>This directive indicates that when an error is encountered, a cached stale response
* MAY be used to satisfy the request, regardless of other freshness information.
* @param staleIfError the maximum time the response should be used when errors are encountered
* @return {@code this}, to facilitate method chaining
* @since 5.2
* @see <a href="https://tools.ietf.org/html/rfc5861#section-4">rfc5861 section 4</a>
*/
public CacheControl staleIfError(Duration staleIfError) {
this.staleIfError = staleIfError;
return this;
}
/**
* Return the "Cache-Control" header value, if any.
* @return the header value, or {@code null} if no directive was added
*/
@Nullable
public String getHeaderValue() {
String headerValue = toHeaderValue();
return (StringUtils.hasText(headerValue) ? headerValue : null);
}
/**
* Return the "Cache-Control" header value.
* @return the header value (potentially empty)
*/
private String toHeaderValue() {
StringBuilder headerValue = new StringBuilder();
if (this.maxAge != null) {
appendDirective(headerValue, "max-age=" + this.maxAge.getSeconds());
}
if (this.noCache) {
appendDirective(headerValue, "no-cache");
}
if (this.noStore) {
appendDirective(headerValue, "no-store");
}
if (this.mustRevalidate) {
appendDirective(headerValue, "must-revalidate");
}
if (this.noTransform) {
appendDirective(headerValue, "no-transform");
}
if (this.cachePublic) {
appendDirective(headerValue, "public");
}
if (this.cachePrivate) {
appendDirective(headerValue, "private");
}
if (this.proxyRevalidate) {
appendDirective(headerValue, "proxy-revalidate");
}
if (this.sMaxAge != null) {
appendDirective(headerValue, "s-maxage=" + this.sMaxAge.getSeconds());
}
if (this.staleIfError != null) {
appendDirective(headerValue, "stale-if-error=" + this.staleIfError.getSeconds());
}
if (this.staleWhileRevalidate != null) {
appendDirective(headerValue, "stale-while-revalidate=" + this.staleWhileRevalidate.getSeconds());
}
return headerValue.toString();
}
private void appendDirective(StringBuilder builder, String value) {
if (builder.length() > 0) {
builder.append(", ");
}
builder.append(value);
}
@Override
public String toString() {
return "CacheControl [" + toHeaderValue() + "]";
}
}
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.lang;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.annotation.Nonnull;
import javax.annotation.meta.TypeQualifierNickname;
import javax.annotation.meta.When;
/**
* A common Spring annotation to declare that annotated elements can be {@code null} under
* some circumstance.
*
* <p>Leverages JSR-305 meta-annotations to indicate nullability in Java to common
* tools with JSR-305 support and used by Kotlin to infer nullability of Spring API.
*
* <p>Should be used at parameter, return value, and field level. Methods override should
* repeat parent {@code @Nullable} annotations unless they behave differently.
*
* <p>Can be used in association with {@code @NonNullApi} or {@code @NonNullFields} to
* override the default non-nullable semantic to nullable.
*
* @author Sebastien Deleuze
* @author Juergen Hoeller
* @since 5.0
* @see N on NullApi
* @see N on NullFields
* @see N on Null
*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Nonnull(when = When.MAYBE)
@TypeQualifierNickname
public @interface Nullable {
}
package javax.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.annotation.meta.TypeQualifier;
import javax.annotation.meta.TypeQualifierValidator;
import javax.annotation.meta.When;
@Documented
@TypeQualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Nonnull {
When when() default When.ALWAYS;
static class Checker implements TypeQualifierValidator<Nonnull> {
@Override
public When forConstantValue(Nonnull qualifierqualifierArgument, Object value) {
if (value == null) {
return When.NEVER;
}
return When.ALWAYS;
}
}
}
package javax.annotation.meta;
/**
* Used to describe the relationship between a qualifier T and the set of values
* S possible on an annotated element.
*
* In particular, an issues should be reported if an ALWAYS or MAYBE value is
* used where a NEVER value is required, or if a NEVER or MAYBE value is used
* where an ALWAYS value is required.
*
*
*/
public enum When {
/** S is a subset of T */
ALWAYS,
/** nothing definitive is known about the relation between S and T */
UNKNOWN,
/** S intersection T is non empty and S - T is nonempty */
MAYBE,
/** S intersection T is empty */
NEVER;
}
package javax.annotation.meta;
import java.lang.annotation.Annotation;
import javax.annotation.Nonnull;
public interface TypeQualifierValidator<A extends Annotation> {
/**
* Given a type qualifier, check to see if a known specific constant value
* is an instance of the set of values denoted by the qualifier.
*
* @param annotation
* the type qualifier
* @param value
* the value to check
* @return a value indicating whether or not the value is an member of the
* values denoted by the type qualifier
*/
public @Nonnull
When forConstantValue(@Nonnull A annotation, Object value);
}
package javax.annotation.meta;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
*
* This annotation is applied to a annotation, and marks the annotation as being
* a qualifier nickname. Applying a nickname annotation X to a element Y should
* be interpreted as having the same meaning as applying all of annotations of X
* (other than QualifierNickname) to Y.
*
* <p>
* Thus, you might define a qualifier SocialSecurityNumber as follows:
* </p>
*
*
* <code>
@Documented
@TypeQualifierNickname @Pattern("[0-9]{3}-[0-9]{2}-[0-9]{4}")
@Retention(RetentionPolicy.RUNTIME)
public @interface SocialSecurityNumber {
}
</code>
*
*
*/
@Documented
@Target(ElementType.ANNOTATION_TYPE)
public @interface TypeQualifierNickname {
}
package javax.annotation.meta;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This qualifier is applied to an annotation to denote that the annotation
* should be treated as a type qualifier.
*/
@Documented
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeQualifier {
/**
* Describes the kinds of values the qualifier can be applied to. If a
* numeric class is provided (e.g., Number.class or Integer.class) then the
* annotation can also be applied to the corresponding primitive numeric
* types.
*/
Class<?> applicableTo() default Object.class;
}
7、 Http 主機頭注入攻擊 解決方案
參考:檢測到目标URL存在http host頭攻擊漏洞
簡介
綠盟科技掃描到網站存在http host頭攻擊漏洞,需要對該漏洞進行修複,網站背景是用java寫的,對于這種host頭攻擊的方式,有很多種方式避免。
如從源頭考慮的話,一般網站都配置了nginx或者部署在Apache上面,可以在nginx或者Apache上攔截非法請求頭的;由于我是在本機上測試的,項目是部署在tomcat上,故無法驗證nginx和apache的修複方式。這裡介紹一下從過濾器的層次進行攔截的方式。
過濾器攔截的方式
使用的測試工具
burpsuite,Firefox ;在火狐上面設定代理,用burpsuite攔截請求并修改host測試驗證是否成功,使用burpsuite以及在火狐設定代理的方式這裡就不介紹了,網上有很多詳細的教程
存在漏洞的版本
這裡先介紹漏洞版本的測試樣例,我在火狐代理以及burpsuite上監聽的端口是8081,注意burpsuite預設監聽了8080,一定要取消,不然就會和tomcat啟用端口重複了
火狐浏覽器上通路存在漏洞的頁面(實際上随便通路個網頁測試結果都一樣的)
在burpsuite上監聽攔截到該位址,通過repeater修改host頭,模拟通路請求。原來的host是10.4.0.246:8080,我們将其修改後改為10.4.0.2460:8080,修改後重新send請求過去,有響應,且響應内容為修改後的host。這就存在了host僞裝的情況。
實際上這種非法的host的隻要加了個攔截器最後跳回登入界面就可以了,但是可能存在通過修改host來跨域攻擊以及越過普通權限攻擊,這種隻是有可能實際上系統架構完善了是完全可以避免的。但是既然檢測出了漏洞,那麼理所應當的也要對其進行相應的修複。
修複漏洞的版本
上面我們已經知道了如何測試漏洞的方法了,現在是如何使用過濾器的方式處理漏洞修複。直奔主題由于使用的是javaweb開發的網站,在web.xml下配置一個新的攔截器。
<filter>
<filter-name>hostCleanFilter</filter-name>
<filter-class>com.test.HostCleanFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hostCleanFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注意在這裡添加的攔截器建議放到第一個,即< url-pattern> /* < /url-pattern> 放在第一個,因為一個項目可能本身的過濾器較多,當其他的過濾器起作用後可能就輪不到這個過濾器執行了,那麼檢測監聽的時候會發現漏洞沒被修複,這點也是我自己測試的時候踩的坑,當時發現有的頁面修複了漏洞,有的頁面沒被修複,這是因為攔截器對于請求的結果可能導緻了不同的去向。
配置攔截器的代碼:
public class HostCleanFilter implements Filter {
public static Logger logger = Logger.getLogger(HostCleanFilter.class);
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestHost = request.getHeader("host");
if (requestHost != null && isRightHost(requestHost)){
response.setStatus(403);
return;
}
chain.doFilter(request,response);
}
public boolean isRightHost(String requestHost){
if (!StringUtils.equals(requestHost,WebConstants.HostOne)
&&!StringUtils.equals(requestHost,WebConstants.HostTwo)){ //注意:該2處标紅位置 内容代碼缺失
//域名非法
logger.info("非法的Host:" + requestHost);
return true;
}
logger.info("合法的Host:" + requestHost);
return false;
}
public void destroy() {
}
}
上面的WebConstants.HostOne、WebConstants.HostTwo是我自己配置合法host,攔截每一個請求若檢驗host不對則傳回403。一般網站的話隻要檢驗兩個host就可以了,一個是ip位址類型的,如我本機測試允許的是10.4.0.246:8080,假如是生産環境上的可以在HostTwo修改為 域名:port,如 baidu.com:8080 這種,根據自身的真實情況添加或修改。
以上步驟修改完後,可以開始重新測試剛剛的位址了,讓我們來看下會有什麼變化呢。繼續通路剛才的位址,在burpsuite上監聽端口并重新repeater。老規矩還是修改host為10.4.0.2460:8080,重寫了host檢視到傳回403。
至此漏洞已經修複完畢,可以多測幾個不同的網頁檢測下是否一緻。測試時候需注意幾點的是:
在本機上測試的時候ip一般是localhost或者127.0.0.1位址作為網站的通路ip位址,會與火狐浏覽器預設的不代理位址沖突,導緻無法正常測試,我這裡的10.4.0.246是在ipconfig上檢視配置設定給的子網ip,本地通路網站的時候使用配置設定的ip進行通路而不是直接通路本機ip,這麼做就能成功的代理火狐浏覽器上的請求。
火狐或者其他浏覽器設定的代理端口盡量不要與本地tomcat或者Apache重提,是以我設定的是8081端口,burpsuite上監聽的也是8081端口。
web.xml設定過濾器建議放在第一位,因為不同的項目過濾器可能執行結果不一樣,比如是直接通路這個漏洞網頁還是登陸使用者後再通路這個漏洞網頁,所傳回的結果可能都會不一樣,因為登入前後攔截的session不一樣了。假如你熟悉你的項目那麼可以設定過濾的順序,我這次是把host頭過濾放在了執行順序第二位,不确定的話可以直接放第一位,省去一些麻煩。
本次文章的編寫參考了部落格:
https://blog.csdn.net/Orangesir/article/details/84999847https://blog.csdn.net/weixin_43922510/article/details/99657007
參考: springboot解決目标URL存在http host頭攻擊漏洞
1.增加過濾器類進行host白名單過濾
package com.todaytech.pwp.core.web;
import com.todaytech.pwp.core.exception.BizException;
import com.todaytech.pwp.core.util.Config;
import com.todaytech.pwp.core.util.lang.StringUtil;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
/**
* http host頭攻擊漏洞處理過濾器, 需要在配置檔案添加allowed.servernames可通路host白名單,
* 多個host用逗号隔開,本地開發使用127.0.0.1,localhost
*
* @author liufr
*/
@Component
public class HostFilter implements Filter {
/**
* 自定義實作host白名單添加
*/
private String ALLOWED_SERVERNAMES = null;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// System.out.println("Filter初始化中");
}
/**
* host攔截
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String host = request.getHeader("host");
String serverName = request.getServerName();
System.out.println("========自定義實作host白名單添加:請求通路ip 頭資訊=================serverName-debug:" + serverName +";host:"+host+"====================");
if (!isEmpty(serverName)) {
if (checkBlankList(serverName)) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
System.out.println("[serverName deny access tips]->" + serverName);
// response.getWriter().print("host deny");
response.setStatus(403);
response.flushBuffer();
}
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
@Override
public void destroy() {
// System.out.println("Filter銷毀");
}
/**
* 校驗目前host是否在白名單中
*/
private boolean ALLOWED_SERVERNAMES = Config.getConfigProperty("allowed_servernames", null);
BizException.throwWhenTrue(StringUtil.isBlank(ALLOWED_SERVERNAMES), "處理“主機頭注入攻擊”的配置參數:allowed_servernames的ip資訊不存在 "); //說明:此處代碼為從配置檔案了讀取 參數 allowed_servernames的内容,不同的項目系統的處理方法不同。按照實際情況來處理= ALLOWED_SERVERNAMES.split(",");
List<String> serverNameList = Arrays.asList(allowdServerName);
for (String str : serverNameList) {
if (!isEmpty(str) && str.equals(serverName)) {
return true;
}
}
return false;
}
/**
* 判空
*/
public boolean isEmpty(Object str) {
return str == null || "".equals(str);
}
}
2.配置檔案增加配置allowed.servernames
allowed_servernames=127.0.0.1,localhost
3.web.xml配置 該攔截器的資訊
<!--處理“主機頭注入攻擊”網絡攻擊問題-->
<filter>
<filter-name>HostFilter</filter-name>
<filter-class>com.todaytech.pwp.core.web.HostFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HostFilter</filter-name>
<url-pattern>*.do</url-pattern>
<url-pattern>*.vop</url-pattern>
<url-pattern>*.vopx</url-pattern>
<url-pattern>*.js</url-pattern>
</filter-mapping>
8、 檢測到目标Referrer-Policy響應頭缺失
問題展示:
項目進行安全掃描 遇到以下低危的風險需要處理~
響應頭缺失 需要更新背景檔案 作為一枚前端菜鳥~我就這樣開始了摸索的道路
因為項目是用tomcat部署到伺服器上的 是以我們需要修改背景服務的檔案web.xml
在web.xml中新增一下内容 重新開機:
<filter>
<filter-name>httpHeaderSecurity</filter-name>
<filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>hstsEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>hstsMaxAgeSeconds</param-name>
<param-value>31536000</param-value>
</init-param>
<init-param>
<param-name>antiClickJackingEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>antiClickJackingOption</param-name>
<param-value>SAMEORIGIN</param-value>
</init-param>
<init-param>
<param-name>blockContentTypeSniffingEnabled</param-name> <!-- X-Content-Type-Options 預設: true(nosniff) -->
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>xssProtectionEnabled</param-name> <!-- X-XSS-Protection 預設: true(1; mode=block) -->
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>httpHeaderSecurity</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
重新開機之後 我們發現響應頭中
但是 如果我們要添加Content-Security-Policy 這樣類似的響應頭 怎麼添加呢 ?我們需要在web.xml中添加過濾器 然後在過濾器中配置響應頭即可,廢話不多說 上代碼
建立一個headerFilter.java檔案:
package xx.xx.xx // 你的項目路徑
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class headerFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {
// System.out.println("FirstFilter init...");
}
public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 添加響應頭
httpResponse.setHeader("Content-Security-Policy","frame-ancestors 'self'");
httpResponse.setHeader("X-Permitted-Cross-Domain-Policies","master-only");
httpResponse.setHeader("X-Download-Options","noopen");
httpResponse.setHeader("Strict-Transport-Security","max-age=31536000; includeSubDomains");
httpResponse.setHeader("Referrer-Policy","no-referrer");
chain.doFilter(request,response);
}
public void destroy() {
}
}
在web.xml中添加一下内容:
// xx.xx.xx 為項目部署的路徑
<filter-name>headerFilter</filter-name>
<filter-class>xx.xx.xx.headerFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>headerFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>
修改完成後 重新開機 檢視前端的network 我們可以看到響應頭中已添加
http 政策之 Referrer-Policy 細節内容講解
一、背景
說道 referer ,大家想必知道的清楚一些。referer是用來防止 CORS(跨站請求僞造)的一種最常見及有效的方式。對于自身伺服器,通過用戶端發來的請求中帶有的referer資訊,可以判斷該請求是否來源于本網站。這樣就可以一定程度上避免其他網站盜取自身伺服器資訊,或者可以通過referer來實作廣告流量引流,說白了,referer是一種用戶端帶到伺服器的用戶端資訊,而Referrer-Policy則是用戶端對這個帶資訊政策的配置。
二、配置方式
1 、HTML 配置
既然是用戶端政策,那麼在HTML中的配置想必大家應該都清楚:
<meta name="referrer" content="origin">
或者用 <a>、<area>、<img>、<iframe>、<script> 或者 <link> 元素上的 referrerpolicy 屬性為其設定獨立的請求政策。
<a href="http://example.com" referrerpolicy="origin">
另外也可以在 <a>、<area> 或者 <link> 元素上将 rel屬性設定為 noreferrer。
<a href="http://example.com" rel="noreferrer">
2、CSP響應頭設定
CSP(Content Security Policy)
Content-Security-Policy:
referrer no-referrer|no-referrer-when-downgrade|origin|origin-when-cross-origin|unsafe-url;
三、API
Referrer-Policy: no-referrer
Referrer-Policy: no-referrer-when-downgrade
Referrer-Policy: origin
Referrer-Policy: origin-when-cross-origin
Referrer-Policy: same-origin
Referrer-Policy: strict-origin
Referrer-Policy: strict-origin-when-cross-origin
Referrer-Policy: unsafe-url
值
no-referrer
整個 Referer 首部會被移除。通路來源資訊不随着請求一起發送
no-referrer-when-downgrade (預設值)
在沒有指定任何政策的情況下使用者代理的預設行為。在同等安全級别的情況下,引用頁面的位址會被發送(HTTPS->HTTPS),但是在降級的情況下不會被發送 (HTTPS->HTTP)。
origin
在任何情況下,僅發送檔案的源作為引用位址。例如 https://example.com/page.html 會将 https://example.com/ 作為引用位址。
origin-when-cross-origin
對于同源的請求,會發送完整的URL作為引用位址,但是對于非同源請求僅發送檔案的源。
same-origin
對于同源的請求會發送引用位址,但是對于非同源請求則不發送引用位址資訊
strict-origin
在同等安全級别的情況下,發送檔案的源作為引用位址(HTTPS->HTTPS),但是在降級的情況下不會發送 (HTTPS->HTTP)。
strict-origin-when-cross-origin
對于同源的請求,會發送完整的URL作為引用位址;在同等安全級别的情況下,發送檔案的源作為引用位址(HTTPS->HTTPS);在降級的情況下不發送此首部 (HTTPS->HTTP)。
unsafe-url
無論是同源請求還是非同源請求,都發送完整的 URL(移除參數資訊之後)作為引用位址。(最不安全的政策了)
四、推薦
推薦使用strict-origin-when-cross-origin 作為預設的referer政策。這是适配同源模式下,防止CSRF攻擊的最佳實踐