從零手寫實現 nginx-23-nginx 對於 cookie 的操作

老马啸西风發表於2024-06-14

前言

大家好,我是老馬。很高興遇到你。

我們為 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 指令用於設定在代理請求中傳遞的 HTTP 頭部欄位。

透過 proxy_set_header 可以在將請求轉發給上游伺服器時新增、修改或刪除請求頭部欄位。

具體來說,proxy_set_header Cookie "admin_cookie=admin_value; $http_cookie"; 這條指令用於修改請求頭中的 Cookie 欄位。

它將一個新的 cookie(admin_cookie=admin_value)新增到現有的請求 cookie 中。詳細解釋如下:

  1. proxy_set_header 指令:這是 Nginx 用來設定請求頭部欄位的指令。
  2. Cookie:這是要設定的頭部欄位名稱。在這種情況下,設定的是 HTTP 請求的 Cookie 頭部。
  3. "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_valueCookie 頭部。

示例配置片段如下:

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:用於設定轉發請求的頭部欄位。

注意事項

  1. 安全性:在操作 cookie 時需要注意安全性,尤其是涉及敏感資訊的 cookie。
  2. 相容性:確保上游伺服器能夠正確處理新增的 cookie。
  3. 配置順序proxy_set_header 通常放在 locationserver 塊中,並在 proxy_pass 指令之前。

透過合理配置 proxy_set_header 指令,可以在 Nginx 中靈活地操作 HTTP 請求頭部,滿足各種代理需求。

這個我們原來就支援了

    /**
     * # 增加或修改請求頭
     * 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 屬性進行任何修改。

配置範圍

該指令可以在 httpserverlocation 塊中配置。

示例

假設我們有一個後端伺服器 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=/

使用場景

  1. 跨域 Cookie 共享:當後端伺服器和客戶端使用不同的域名時,透過 proxy_cookie_domain 重寫 Set-Cookie 頭部的 Domain 屬性,使 Cookie 能夠在客戶端域名下有效。
  2. 域名變更:如果網站的域名發生變化,透過該指令可以確保舊域名設定的 Cookie 仍然有效。
  3. 子域名問題:在使用子域名時,可以透過該指令將所有子域名的 Cookie 統一到主域名下。

注意事項

  1. 安全性:確保重寫的域名是可信任的,以防止 Cookie 被不當共享。
  2. 精確匹配proxy_cookie_domain 的匹配是精確匹配的,因此需要確保指定的上游伺服器域名與實際的 Set-Cookie 頭部中的 Domain 屬性完全一致。

透過合理使用 proxy_cookie_domain 指令,可以有效地解決跨域 Cookie 共享的問題,確保在反向代理場景下的 Cookie 設定和使用正確無誤。

核心實現如下:

/**
 * 引數處理類 響應頭處理
 *
 * @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 的屬性標誌。主要支援的配置選項包括:

  1. HttpOnly:將 HttpOnly 標誌新增到 cookie,使得 JavaScript 無法透過 document.cookie 訪問該 cookie。

    proxy_cookie_flags <cookie_name> HttpOnly;
    
  2. Secure:將 Secure 標誌新增到 cookie,僅在透過 HTTPS 協議傳送時才會傳送該 cookie。

    proxy_cookie_flags <cookie_name> Secure;
    
  3. SameSite:設定 SameSite 標誌,限制瀏覽器僅在同站點請求時傳送該 cookie,有助於防止跨站點請求偽造(CSRF)攻擊。

    proxy_cookie_flags <cookie_name> SameSite=Strict;
    

    支援的 SameSite 值包括 StrictLaxNone

  4. Max-Age:設定 Max-Age 屬性,指定 cookie 的過期時間(秒)。通常用於設定持久化 cookie 的過期時間。

    proxy_cookie_flags <cookie_name> Max-Age=3600;
    
  5. Expires:設定 Expires 屬性,指定 cookie 的過期時間點。通常以 GMT 格式的日期字串指定。

    proxy_cookie_flags <cookie_name> Expires=Wed, 21 Oct 2026 07:28:00 GMT;
    
  6. Domain:設定 Domain 屬性,指定可接受該 cookie 的域名範圍。透過 proxy_cookie_domain 指令更常用地配置。

    proxy_cookie_flags <cookie_name> Domain=example.com;
    
  7. 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 的處理,讓我們的請求可以更加強大靈活。

  1. proxy_cookie_domain: 設定後端伺服器響應的 Cookie 中的域名。

  2. proxy_cookie_flags: 設定後端伺服器響應的 Cookie 的標誌位。

  3. proxy_cookie_path: 設定後端伺服器響應的 Cookie 的路徑。

我是老馬,期待與你的下次重逢。

開源地址

為了便於大家學習,已經將 nginx 開源

https://github.com/houbb/nginx4j

相關文章