從零手寫實現(xiàn) nginx-23-nginx 對于 cookie 的操作

前言

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

我們?yōu)?java 開發(fā)者實現(xiàn)了 java 版本的 nginx

https://github.com/houbb/nginx4j

如果你想知道 servlet 如何處理的,可以參考我的另一個項目:

手寫從零實現(xiàn)簡易版 tomcat minicat

手寫 nginx 系列

如果你對 nginx 原理感興趣,可以閱讀:

從零手寫實現(xiàn) nginx-01-為什么不能有 java 版本的 nginx?

從零手寫實現(xiàn) nginx-02-nginx 的核心能力

從零手寫實現(xiàn) nginx-03-nginx 基于 Netty 實現(xiàn)

從零手寫實現(xiàn) nginx-04-基于 netty http 出入?yún)?yōu)化處理

從零手寫實現(xiàn) nginx-05-MIME類型(Multipurpose Internet Mail Extensions,多用途互聯(lián)網(wǎng)郵件擴展類型)

從零手寫實現(xiàn) nginx-06-文件夾自動索引

從零手寫實現(xiàn) nginx-07-大文件下載

從零手寫實現(xiàn) nginx-08-范圍查詢

從零手寫實現(xiàn) nginx-09-文件壓縮

從零手寫實現(xiàn) nginx-10-sendfile 零拷貝

從零手寫實現(xiàn) nginx-11-file+range 合并

從零手寫實現(xiàn) nginx-12-keep-alive 連接復(fù)用

從零手寫實現(xiàn) nginx-13-nginx.conf 配置文件介紹

從零手寫實現(xiàn) nginx-14-nginx.conf 和 hocon 格式有關(guān)系嗎?

從零手寫實現(xiàn) nginx-15-nginx.conf 如何通過 java 解析處理?

從零手寫實現(xiàn) nginx-16-nginx 支持配置多個 server

從零手寫實現(xiàn) nginx-17-nginx 默認(rèn)配置優(yōu)化

從零手寫實現(xiàn) nginx-18-nginx 請求頭+響應(yīng)頭操作

從零手寫實現(xiàn) nginx-19-nginx cors

從零手寫實現(xiàn) nginx-20-nginx 占位符 placeholder

從零手寫實現(xiàn) nginx-21-nginx modules 模塊信息概覽

從零手寫實現(xiàn) nginx-22-nginx modules 分模塊加載優(yōu)化

從零手寫實現(xiàn) nginx-23-nginx cookie 的操作處理

前言

大家好,我是老馬。

這一節(jié)我們將配置的加載,拆分為不同的模塊加載處理,便于后續(xù)拓展。

1. proxy_set_header Cookie 指令

介紹下 nginx proxy_set_header Cookie "admin_cookie=admin_value; $http_cookie"; 操作 cookie 的指令

在 Nginx 配置文件中,proxy_set_header 指令用于設(shè)置在代理請求中傳遞的 HTTP 頭部字段。

通過 proxy_set_header 可以在將請求轉(zhuǎn)發(fā)給上游服務(wù)器時添加、修改或刪除請求頭部字段。

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

它將一個新的 cookie(admin_cookie=admin_value)添加到現(xiàn)有的請求 cookie 中。詳細(xì)解釋如下:

  1. proxy_set_header 指令:這是 Nginx 用來設(shè)置請求頭部字段的指令。
  2. Cookie:這是要設(shè)置的頭部字段名稱。在這種情況下,設(shè)置的是 HTTP 請求的 Cookie 頭部。
  3. "admin_cookie=admin_value; $http_cookie":這是要設(shè)置的頭部字段值。
    • admin_cookie=admin_value:這是要添加的新 cookie 值。admin_cookie 是 cookie 的名稱,admin_value 是它的值。
    • ;:分號用來分隔多個 cookie。
    • $http_cookie:這是一個 Nginx 的內(nèi)置變量,它包含了當(dāng)前請求中的所有 cookie 值。

通過這條指令,Nginx 會在轉(zhuǎn)發(fā)請求到上游服務(wù)器之前,將一個新的 cookie 添加到現(xiàn)有的 cookie 中。這樣上游服務(wù)器就會收到一個包含新添加的 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";
    }
}

在這個示例中,當(dāng)客戶端向 example.com 發(fā)起請求時,Nginx 會將請求轉(zhuǎn)發(fā)給 backend_server,并在請求頭部的 Cookie 字段中添加一個新的 admin_cookie=admin_value。

其他相關(guān)的 Nginx 指令

  • proxy_pass:用于定義請求轉(zhuǎn)發(fā)到的上游服務(wù)器。
  • proxy_set_header:用于設(shè)置轉(zhuǎn)發(fā)請求的頭部字段。

注意事項

  1. 安全性:在操作 cookie 時需要注意安全性,尤其是涉及敏感信息的 cookie。
  2. 兼容性:確保上游服務(wù)器能夠正確處理添加的 cookie。
  3. 配置順序proxy_set_header 通常放在 locationserver 塊中,并在 proxy_pass 指令之前。

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

netty 如何實現(xiàn) 對于 cookie 的新增/修改/刪除?

這個我們原來就支持了

    /**
     * # 增加或修改請求頭
     * proxy_set_header X-Real-IP $remote_addr;
     * # 刪除請求頭
     * proxy_set_header X-Unwanted-Header "";
     *
     * @param configParam 參數(shù)
     * @param context     上下文
     */
    @Override
    public void doBeforeDispatch(NginxCommonConfigParam configParam, NginxRequestDispatchContext context) {
        List<String> values = configParam.getValues();

        // $ 占位符號后續(xù)處理

        String headerName = values.get(0);
        String headerValue = values.get(1);

        FullHttpRequest fullHttpRequest = context.getRequest();

        // 設(shè)置
        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 的一個指令,用于修改代理服務(wù)器響應(yīng)中的 Set-Cookie 頭部的 Domain 屬性。

這個指令通常用于在反向代理配置中,當(dāng)上游服務(wù)器設(shè)置的 Domain 屬性與客戶端訪問的域名不一致時,通過重寫 Domain 屬性來解決跨域問題。

語法

proxy_cookie_domain [上游服務(wù)器的域名] [要重寫為的域名];
  • 上游服務(wù)器的域名:指定要匹配并重寫的 Domain 屬性值。
  • 要重寫為的域名:指定新的 Domain 屬性值。

默認(rèn)值

proxy_cookie_domain off;

如果不設(shè)置 proxy_cookie_domain,則默認(rèn)不對 Set-Cookie 頭部的 Domain 屬性進(jìn)行任何修改。

配置范圍

該指令可以在 http、serverlocation 塊中配置。

示例

假設(shè)我們有一個后端服務(wù)器 backend.example.com,它在設(shè)置 Cookie 時將 Domain 屬性設(shè)為 backend.example.com。

但是,客戶端訪問的是 www.example.com

我們可以使用 proxy_cookie_domain 來重寫 Domain 屬性,以便客戶端能夠正確地接收和發(fā)送這些 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;
        }
    }
}

在這個配置中,當(dāng)上游服務(wù)器 backend.example.com 在響應(yīng)中返回 Set-Cookie 頭部時:

Set-Cookie: sessionid=abcd1234; Domain=backend.example.com; Path=/

Nginx 會將其重寫為:

Set-Cookie: sessionid=abcd1234; Domain=www.example.com; Path=/

使用場景

  1. 跨域 Cookie 共享:當(dāng)后端服務(wù)器和客戶端使用不同的域名時,通過 proxy_cookie_domain 重寫 Set-Cookie 頭部的 Domain 屬性,使 Cookie 能夠在客戶端域名下有效。
  2. 域名變更:如果網(wǎng)站的域名發(fā)生變化,通過該指令可以確保舊域名設(shè)置的 Cookie 仍然有效。
  3. 子域名問題:在使用子域名時,可以通過該指令將所有子域名的 Cookie 統(tǒng)一到主域名下。

注意事項

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

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

如何通過 netty,實現(xiàn) proxy_cookie_domain 指令特性?

核心實現(xiàn)如下:

/**
 * 參數(shù)處理類 響應(yīng)頭處理
 *
 * @since 0.20.0
 * @author 老馬嘯西風(fēng)
 */
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);
        // 目標(biāo)
        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 指令用于設(shè)置從代理服務(wù)器返回給客戶端的 Set-Cookie 頭中特定 cookie 的屬性標(biāo)志。主要支持的配置選項包括:

  1. HttpOnly:將 HttpOnly 標(biāo)志添加到 cookie,使得 JavaScript 無法通過 document.cookie 訪問該 cookie。

    proxy_cookie_flags <cookie_name> HttpOnly;
    
  2. Secure:將 Secure 標(biāo)志添加到 cookie,僅在通過 HTTPS 協(xié)議發(fā)送時才會發(fā)送該 cookie。

    proxy_cookie_flags <cookie_name> Secure;
    
  3. SameSite:設(shè)置 SameSite 標(biāo)志,限制瀏覽器僅在同站點請求時發(fā)送該 cookie,有助于防止跨站點請求偽造(CSRF)攻擊。

    proxy_cookie_flags <cookie_name> SameSite=Strict;
    

    支持的 SameSite 值包括 Strict、LaxNone

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

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

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

    proxy_cookie_flags <cookie_name> Domain=example.com;
    
  7. Path:設(shè)置 Path 屬性,指定該 cookie 的路徑范圍。

    proxy_cookie_flags <cookie_name> Path=/;
    

示例

以下是一些示例,展示如何使用 proxy_cookie_flags 指令設(shè)置不同的 cookie 標(biāo)志:

server {
    listen 80;
    server_name example.com;

    location / {
        # 添加 HttpOnly 和 Secure 標(biāo)志
        proxy_cookie_flags session_cookie HttpOnly Secure;
        
        # 設(shè)置 SameSite 標(biāo)志為 Strict
        proxy_cookie_flags mycookie SameSite=Strict;
        
        # 設(shè)置 Max-Age 為 1 小時
        proxy_cookie_flags persistent_cookie Max-Age=3600;
        
        # 設(shè)置 Expires 屬性
        proxy_cookie_flags old_cookie Expires=Wed, 21 Oct 2026 07:28:00 GMT;
        
        # 設(shè)置 Domain 屬性
        proxy_cookie_flags global_cookie Domain=example.com;
        
        # 設(shè)置 Path 屬性
        proxy_cookie_flags local_cookie Path=/subpath;
        
        proxy_pass http://backend;
    }
}

通過這些配置,您可以靈活地控制從代理服務(wù)器返回的 Set-Cookie 頭中各個 cookie 的屬性,以滿足安全需求和業(yè)務(wù)邏輯。

java 核心實現(xiàn)

    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 指令用于修改傳遞到后端服務(wù)器的 HTTP 請求中的 Cookie 的路徑。

這個指令通常在反向代理服務(wù)器配置中使用,用于調(diào)整傳遞給后端服務(wù)器的 Cookie 的路徑信息,以適應(yīng)后端服務(wù)器的預(yù)期路徑結(jié)構(gòu)。

語法和用法

語法:

proxy_cookie_path regex path;

參數(shù)解釋:

  • regex:一個正則表達(dá)式,用于匹配要修改的 Cookie 的路徑。
  • path:要替換成的路徑。

示例

假設(shè)有如下配置:

location /app/ {
    proxy_pass http://backend.example.com;
    proxy_cookie_path ~*^/app(.*) $1;
}

在這個示例中:

  • proxy_cookie_path 指令配合 proxy_pass 使用,表示將從客戶端接收的帶有路徑 /app/ 的 Cookie 的路徑信息去除 /app 部分后再傳遞給后端服務(wù)器。

例如,如果客戶端發(fā)送的 Cookie 路徑是 /app/session, Nginx 將修改為 /session 后傳遞給后端服務(wù)器。

注意事項

  • 使用 proxy_cookie_path 時,確保理解你的后端服務(wù)器期望接收的 Cookie 路徑格式,以便正確設(shè)置正則表達(dá)式和路徑。
  • 正則表達(dá)式必須能夠正確匹配客戶端發(fā)送的 Cookie 路徑。
  • 這個指令通常用于調(diào)整不同路徑的代理請求,以便與后端服務(wù)器的預(yù)期路徑結(jié)構(gòu)匹配。

java 核心實現(xiàn)

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個參數(shù)");
    }

    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);
}

小結(jié)

對于 cookie 的處理,讓我們的請求可以更加強大靈活。

  1. proxy_cookie_domain: 設(shè)置后端服務(wù)器響應(yīng)的 Cookie 中的域名。

  2. proxy_cookie_flags: 設(shè)置后端服務(wù)器響應(yīng)的 Cookie 的標(biāo)志位。

  3. proxy_cookie_path: 設(shè)置后端服務(wù)器響應(yīng)的 Cookie 的路徑。

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

開源地址

為了便于大家學(xué)習(xí),已經(jīng)將 nginx 開源

https://github.com/houbb/nginx4j

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容