前言
大家好,我是老馬。很高興遇到你。
我們為 java 開發者實現了 java 版本的 nginx
https://github.com/houbb/nginx4j
如果你想知道 servlet 如何處理的,可以參考我的另一個專案:
手寫從零實現簡易版 tomcat minicat
手寫 nginx 系列
如果你對 nginx 原理感興趣,可以閱讀:
從零手寫實現 nginx-01-為什麼不能有 java 版本的 nginx?
從零手寫實現 nginx-02-nginx 的核心能力
從零手寫實現 nginx-03-nginx 基於 Netty 實現
從零手寫實現 nginx-04-基於 netty http 出入參最佳化處理
從零手寫實現 nginx-05-MIME型別(Multipurpose Internet Mail Extensions,多用途網際網路郵件擴充套件型別)
從零手寫實現 nginx-06-資料夾自動索引
從零手寫實現 nginx-07-大檔案下載
從零手寫實現 nginx-08-範圍查詢
從零手寫實現 nginx-09-檔案壓縮
從零手寫實現 nginx-10-sendfile 零複製
從零手寫實現 nginx-11-file+range 合併
從零手寫實現 nginx-12-keep-alive 連線複用
從零手寫實現 nginx-13-nginx.conf 配置檔案介紹
從零手寫實現 nginx-14-nginx.conf 和 hocon 格式有關係嗎?
從零手寫實現 nginx-15-nginx.conf 如何透過 java 解析處理?
從零手寫實現 nginx-16-nginx 支援配置多個 server
從零手寫實現 nginx-17-nginx 預設配置最佳化
從零手寫實現 nginx-18-nginx 請求頭+響應頭操作
從零手寫實現 nginx-19-nginx cors
從零手寫實現 nginx-20-nginx 佔位符 placeholder
從零手寫實現 nginx-21-nginx modules 模組資訊概覽
從零手寫實現 nginx-22-nginx modules 分模組載入最佳化
從零手寫實現 nginx-23-nginx cookie 的操作處理
前言
大家好,我是老馬。
這一節我們將配置的載入,拆分為不同的模組載入處理,便於後續擴充。
1. proxy_set_header Cookie 指令
介紹下 nginx proxy_set_header Cookie "admin_cookie=admin_value; $http_cookie"; 操作 cookie 的指令
在 Nginx 配置檔案中,proxy_set_header
指令用於設定在代理請求中傳遞的 HTTP 頭部欄位。
透過 proxy_set_header
可以在將請求轉發給上游伺服器時新增、修改或刪除請求頭部欄位。
具體來說,proxy_set_header Cookie "admin_cookie=admin_value; $http_cookie";
這條指令用於修改請求頭中的 Cookie
欄位。
它將一個新的 cookie(admin_cookie=admin_value
)新增到現有的請求 cookie 中。詳細解釋如下:
proxy_set_header
指令:這是 Nginx 用來設定請求頭部欄位的指令。Cookie
:這是要設定的頭部欄位名稱。在這種情況下,設定的是 HTTP 請求的Cookie
頭部。"admin_cookie=admin_value; $http_cookie"
:這是要設定的頭部欄位值。admin_cookie=admin_value
:這是要新增的新 cookie 值。admin_cookie
是 cookie 的名稱,admin_value
是它的值。;
:分號用來分隔多個 cookie。$http_cookie
:這是一個 Nginx 的內建變數,它包含了當前請求中的所有 cookie 值。
透過這條指令,Nginx 會在轉發請求到上游伺服器之前,將一個新的 cookie 新增到現有的 cookie 中。這樣上游伺服器就會收到一個包含新新增的 admin_cookie=admin_value
的 Cookie
頭部。
示例配置片段如下:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend_server;
proxy_set_header Cookie "admin_cookie=admin_value; $http_cookie";
}
}
在這個示例中,當客戶端向 example.com
發起請求時,Nginx 會將請求轉發給 backend_server
,並在請求頭部的 Cookie
欄位中新增一個新的 admin_cookie=admin_value
。
其他相關的 Nginx 指令
proxy_pass
:用於定義請求轉發到的上游伺服器。proxy_set_header
:用於設定轉發請求的頭部欄位。
注意事項
- 安全性:在操作 cookie 時需要注意安全性,尤其是涉及敏感資訊的 cookie。
- 相容性:確保上游伺服器能夠正確處理新增的 cookie。
- 配置順序:
proxy_set_header
通常放在location
或server
塊中,並在proxy_pass
指令之前。
透過合理配置 proxy_set_header
指令,可以在 Nginx 中靈活地操作 HTTP 請求頭部,滿足各種代理需求。
netty 如何實現 對於 cookie 的新增/修改/刪除?
這個我們原來就支援了
/**
* # 增加或修改請求頭
* proxy_set_header X-Real-IP $remote_addr;
* # 刪除請求頭
* proxy_set_header X-Unwanted-Header "";
*
* @param configParam 引數
* @param context 上下文
*/
@Override
public void doBeforeDispatch(NginxCommonConfigParam configParam, NginxRequestDispatchContext context) {
List<String> values = configParam.getValues();
// $ 佔位符號後續處理
String headerName = values.get(0);
String headerValue = values.get(1);
FullHttpRequest fullHttpRequest = context.getRequest();
// 設定
HttpHeaders headers = fullHttpRequest.headers();
if (StringUtil.isEmpty(headerValue)) {
headers.remove(headerName);
logger.info(">>>>>>>>>>>> doBeforeDispatch headers.remove({})", headerName);
} else {
// 是否包含
if (headers.contains(headerName)) {
headers.set(headerName, headerValue);
logger.info(">>>>>>>>>>>> doBeforeDispatch headers.set({}, {});", headerName, headerValue);
} else {
headers.add(headerName, headerValue);
logger.info(">>>>>>>>>>>> doBeforeDispatch headers.set({}, {});", headerName, headerValue);
}
}
}
proxy_cookie_domain 指令
解釋
proxy_cookie_domain
是 Nginx 的一個指令,用於修改代理伺服器響應中的 Set-Cookie
頭部的 Domain
屬性。
這個指令通常用於在反向代理配置中,當上遊伺服器設定的 Domain
屬性與客戶端訪問的域名不一致時,透過重寫 Domain
屬性來解決跨域問題。
語法
proxy_cookie_domain [上游伺服器的域名] [要重寫為的域名];
- 上游伺服器的域名:指定要匹配並重寫的
Domain
屬性值。 - 要重寫為的域名:指定新的
Domain
屬性值。
預設值
proxy_cookie_domain off;
如果不設定 proxy_cookie_domain
,則預設不對 Set-Cookie
頭部的 Domain
屬性進行任何修改。
配置範圍
該指令可以在 http
、server
或 location
塊中配置。
示例
假設我們有一個後端伺服器 backend.example.com
,它在設定 Cookie 時將 Domain
屬性設為 backend.example.com
。
但是,客戶端訪問的是 www.example.com
。
我們可以使用 proxy_cookie_domain
來重寫 Domain
屬性,以便客戶端能夠正確地接收和傳送這些 Cookie。
http {
server {
listen 80;
server_name www.example.com;
location / {
proxy_pass http://backend.example.com;
proxy_cookie_domain backend.example.com www.example.com;
}
}
}
在這個配置中,當上遊伺服器 backend.example.com
在響應中返回 Set-Cookie
頭部時:
Set-Cookie: sessionid=abcd1234; Domain=backend.example.com; Path=/
Nginx 會將其重寫為:
Set-Cookie: sessionid=abcd1234; Domain=www.example.com; Path=/
使用場景
- 跨域 Cookie 共享:當後端伺服器和客戶端使用不同的域名時,透過
proxy_cookie_domain
重寫Set-Cookie
頭部的Domain
屬性,使 Cookie 能夠在客戶端域名下有效。 - 域名變更:如果網站的域名發生變化,透過該指令可以確保舊域名設定的 Cookie 仍然有效。
- 子域名問題:在使用子域名時,可以透過該指令將所有子域名的 Cookie 統一到主域名下。
注意事項
- 安全性:確保重寫的域名是可信任的,以防止 Cookie 被不當共享。
- 精確匹配:
proxy_cookie_domain
的匹配是精確匹配的,因此需要確保指定的上游伺服器域名與實際的Set-Cookie
頭部中的Domain
屬性完全一致。
透過合理使用 proxy_cookie_domain
指令,可以有效地解決跨域 Cookie 共享的問題,確保在反向代理場景下的 Cookie 設定和使用正確無誤。
如何透過 netty,實現 proxy_cookie_domain 指令特性?
核心實現如下:
/**
* 引數處理類 響應頭處理
*
* @since 0.20.0
* @author 老馬嘯西風
*/
public class NginxParamHandleProxyCookieDomain extends AbstractNginxParamLifecycleWrite {
private static final Log logger = LogFactory.getLog(NginxParamHandleProxyCookieDomain.class);
@Override
public void doBeforeWrite(NginxCommonConfigParam configParam, ChannelHandlerContext ctx, Object object, NginxRequestDispatchContext context) {
if(!(object instanceof HttpResponse)) {
return;
}
List<String> values = configParam.getValues();
if(CollectionUtil.isEmpty(values) || values.size() < 2) {
return;
}
// 原始
String upstreamDomain = values.get(0);
// 目標
String targetDomain = values.get(1);
HttpResponse response = (HttpResponse) object;
HttpHeaders headers = response.headers();
String setCookieHeader = headers.get(HttpHeaderNames.SET_COOKIE);
if (setCookieHeader != null) {
Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(setCookieHeader);
Set<Cookie> modifiedCookies = cookies.stream().map(cookie -> {
if (upstreamDomain.equals(cookie.domain())) {
Cookie newCookie = new DefaultCookie(cookie.name(), cookie.value());
newCookie.setDomain(targetDomain);
newCookie.setPath(cookie.path());
newCookie.setMaxAge(cookie.maxAge());
newCookie.setSecure(cookie.isSecure());
newCookie.setHttpOnly(cookie.isHttpOnly());
return newCookie;
}
return cookie;
}).collect(Collectors.toSet());
List<String> encodedCookies = ServerCookieEncoder.STRICT.encode(modifiedCookies);
headers.set(HttpHeaderNames.SET_COOKIE, encodedCookies);
}
logger.info(">>>>>>>>>>>> doBeforeWrite proxy_hide_header upstreamDomain={} => targetDomain={}", upstreamDomain, targetDomain);
}
@Override
public void doAfterWrite(NginxCommonConfigParam configParam, ChannelHandlerContext ctx, Object object, NginxRequestDispatchContext context) {
}
@Override
protected String getKey(NginxCommonConfigParam configParam, ChannelHandlerContext ctx, Object object, NginxRequestDispatchContext context) {
return "proxy_hide_header";
}
}
proxy_cookie_flags 指令
支援哪些?
在 Nginx 中,proxy_cookie_flags
指令用於設定從代理伺服器返回給客戶端的 Set-Cookie
頭中特定 cookie 的屬性標誌。主要支援的配置選項包括:
-
HttpOnly:將
HttpOnly
標誌新增到 cookie,使得 JavaScript 無法透過document.cookie
訪問該 cookie。proxy_cookie_flags <cookie_name> HttpOnly;
-
Secure:將
Secure
標誌新增到 cookie,僅在透過 HTTPS 協議傳送時才會傳送該 cookie。proxy_cookie_flags <cookie_name> Secure;
-
SameSite:設定
SameSite
標誌,限制瀏覽器僅在同站點請求時傳送該 cookie,有助於防止跨站點請求偽造(CSRF)攻擊。proxy_cookie_flags <cookie_name> SameSite=Strict;
支援的
SameSite
值包括Strict
、Lax
和None
。 -
Max-Age:設定
Max-Age
屬性,指定 cookie 的過期時間(秒)。通常用於設定持久化 cookie 的過期時間。proxy_cookie_flags <cookie_name> Max-Age=3600;
-
Expires:設定
Expires
屬性,指定 cookie 的過期時間點。通常以 GMT 格式的日期字串指定。proxy_cookie_flags <cookie_name> Expires=Wed, 21 Oct 2026 07:28:00 GMT;
-
Domain:設定
Domain
屬性,指定可接受該 cookie 的域名範圍。透過proxy_cookie_domain
指令更常用地配置。proxy_cookie_flags <cookie_name> Domain=example.com;
-
Path:設定
Path
屬性,指定該 cookie 的路徑範圍。proxy_cookie_flags <cookie_name> Path=/;
示例
以下是一些示例,展示如何使用 proxy_cookie_flags
指令設定不同的 cookie 標誌:
server {
listen 80;
server_name example.com;
location / {
# 新增 HttpOnly 和 Secure 標誌
proxy_cookie_flags session_cookie HttpOnly Secure;
# 設定 SameSite 標誌為 Strict
proxy_cookie_flags mycookie SameSite=Strict;
# 設定 Max-Age 為 1 小時
proxy_cookie_flags persistent_cookie Max-Age=3600;
# 設定 Expires 屬性
proxy_cookie_flags old_cookie Expires=Wed, 21 Oct 2026 07:28:00 GMT;
# 設定 Domain 屬性
proxy_cookie_flags global_cookie Domain=example.com;
# 設定 Path 屬性
proxy_cookie_flags local_cookie Path=/subpath;
proxy_pass http://backend;
}
}
透過這些配置,您可以靈活地控制從代理伺服器返回的 Set-Cookie
頭中各個 cookie 的屬性,以滿足安全需求和業務邏輯。
java 核心實現
public void doBeforeWrite(NginxCommonConfigParam configParam, ChannelHandlerContext ctx, Object object, NginxRequestDispatchContext context) {
if(!(object instanceof HttpResponse)) {
return;
}
List<String> values = configParam.getValues();
if(CollectionUtil.isEmpty(values) || values.size() < 2) {
return;
}
HttpResponse response = (HttpResponse) object;
HttpHeaders headers = response.headers();
String cookieHeader = headers.get(HttpHeaderNames.COOKIE);
final String cookieName = values.get(0);
if (cookieHeader != null) {
Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(cookieHeader);
Set<Cookie> modifiedCookies = cookies.stream().map(cookie -> {
// 相同的名字
if (cookieName.equals(cookie.name())) {
// HttpOnly Secure
for(int i = 1; i < values.size(); i++) {
String value = values.get(i);
if("HttpOnly".equals(value)) {
cookie.setHttpOnly(true);
}
if("Secure".equals(value)) {
cookie.setSecure(true);
}
// 拆分
if(!value.contains("=")) {
return cookie;
}
String[] items = value.split("=");
String itemKey = items[0];
String itemVal = items[1];
// if("SameSite".equals(itemKey) && "Strict".equals(itemVal)) {
// }
if("Max-Age".equals(itemKey)) {
cookie.setMaxAge(Long.parseLong(itemVal));
}
if("Expires".equals(itemKey)) {
Date expireDate = calcDate(itemVal);
long maxAge = expireDate.getTime() - System.currentTimeMillis();
cookie.setMaxAge(maxAge);
}
if("Domain".equals(itemKey)) {
cookie.setDomain(itemVal);
}
if("Path".equals(itemKey)) {
cookie.setPath(itemVal);
}
}
}
return cookie;
}).collect(Collectors.toSet());
List<String> encodedCookies = ServerCookieEncoder.STRICT.encode(modifiedCookies);
headers.set(HttpHeaderNames.COOKIE, encodedCookies);
}
logger.info(">>>>>>>>>>>> doBeforeWrite proxy_cookie_flags values={}", values);
}
nginx proxy_cookie_path 指令
介紹
在 Nginx 中,proxy_cookie_path
指令用於修改傳遞到後端伺服器的 HTTP 請求中的 Cookie 的路徑。
這個指令通常在反向代理伺服器配置中使用,用於調整傳遞給後端伺服器的 Cookie 的路徑資訊,以適應後端伺服器的預期路徑結構。
語法和用法
語法:
proxy_cookie_path regex path;
引數解釋:
regex
:一個正規表示式,用於匹配要修改的 Cookie 的路徑。path
:要替換成的路徑。
示例
假設有如下配置:
location /app/ {
proxy_pass http://backend.example.com;
proxy_cookie_path ~*^/app(.*) $1;
}
在這個示例中:
proxy_cookie_path
指令配合proxy_pass
使用,表示將從客戶端接收的帶有路徑/app/
的 Cookie 的路徑資訊去除/app
部分後再傳遞給後端伺服器。
例如,如果客戶端傳送的 Cookie 路徑是 /app/session
, Nginx 將修改為 /session
後傳遞給後端伺服器。
注意事項
- 使用
proxy_cookie_path
時,確保理解你的後端伺服器期望接收的 Cookie 路徑格式,以便正確設定正規表示式和路徑。 - 正規表示式必須能夠正確匹配客戶端傳送的 Cookie 路徑。
- 這個指令通常用於調整不同路徑的代理請求,以便與後端伺服器的預期路徑結構匹配。
java 核心實現
public void doBeforeDispatch(NginxCommonConfigParam configParam, NginxRequestDispatchContext context) {
List<String> values = configParam.getValues();
if(CollectionUtil.isEmpty(values) || values.size() < 2) {
throw new Nginx4jException("proxy_cookie_path 必須包含2個引數");
}
FullHttpRequest request = context.getRequest();
// 原始
String regex = values.get(0);
String path = values.get(1);
HttpHeaders headers = request.headers();
String cookieHeader = headers.get(HttpHeaderNames.COOKIE);
if (cookieHeader != null) {
String modifiedCookieHeader = cookieHeader.replaceAll(regex, path);
headers.set(HttpHeaderNames.COOKIE, modifiedCookieHeader);
}
logger.info(">>>>>>>>>>>> doBeforeDispatch proxy_cookie_path replace regex={} => path={}", regex, path);
}
小結
對於 cookie 的處理,讓我們的請求可以更加強大靈活。
-
proxy_cookie_domain
: 設定後端伺服器響應的 Cookie 中的域名。 -
proxy_cookie_flags
: 設定後端伺服器響應的 Cookie 的標誌位。 -
proxy_cookie_path
: 設定後端伺服器響應的 Cookie 的路徑。
我是老馬,期待與你的下次重逢。
開源地址
為了便於大家學習,已經將 nginx 開源
https://github.com/houbb/nginx4j