nginx userid到底做了啥?

2023-12-22 23:04:03

我们公司在用nginx的userid模块作为简单的用户请求追踪使用。这个模块其实并不能真正记录用户的请求状态,只能作为一个辅助使用。但是在一些场景下会有一些异常。下面我们简单介绍一下这个模块到底做了什么。

userid 模块简介

官网说明文档
ngx_http_userid_module

官网示例

userid         on;
userid_name    uid;
userid_domain  example.com;
userid_path    /;
userid_expires 365d;
userid_p3p     'policyref="/w3c/p3p.xml", CP="CUR ADM OUR NOR STA NID"';
配置说明
userid on |v1 | log | off;userid开关
userid_name uid;userid (cookie)名
userid_domain example.com;userid (cookie) domain
userid_path /;userid (cookie) 路径
userid_expires 365d;userid (cookie) 过期时间
userid_p3p ‘policyref=“/w3c/p3p.xml”, CP=“CUR ADM OUR NOR STA NID”’;p3p header 标记

简单来说这个模块的作用就是当客户端的请求cookie中,未携带userid字段,或者userid字段不合法时,nginx在response中会加一个Set-Cookie 的 header。如果配置了p3p,会额外返回p3p的header

set-cookie: uid=CrINEGWBDAFNOTILCEHMAg==; expires=Thu, 18-Dec-25 03:20:33 GMT; domain=example.com; path=/
p3p: policyref="/w3c/p3p.xml", CP="CUR ADM OUR NOR STA NID"

这样同一个客户端将会获得相同的uid,可以作为用户请求追踪的请求特征。但是要注意的是这个cookie的设置逻辑很简单,并且没有用户的登录态吧,所以并不可靠。如果用户使用不同浏览器或者无痕访问就会获得不同的uid,通过他来进行uv等数据统计,获得的结果会虚高。

nginx官网对userid模块的介绍比较简单,我们可以看下他的源码来分析一下他的生成和校验逻辑细节。

我们以文章发布时候最新的1.24版本的nginx源码为例

nginx github路径

userid filter核心函数

nginx userid 是一个 http filter 模块,请求进来后通过调用 ngx_http_userid_filter 这个函数来执行 userid的逻辑,ngx_http_userid_filter这个函数主要调用了 ngx_http_userid_get_uid 和 ngx_http_userid_set_uid。分别用于获取和生成userid

userid的生成逻辑

我们先看下ngx_http_userid_get_uid 这个获取uid的函数。我节选一些核心代码

static ngx_http_userid_ctx_t *
ngx_http_userid_get_uid(ngx_http_request_t *r, ngx_http_userid_conf_t *conf)
{
    ctx = ngx_http_get_module_ctx(r, ngx_http_userid_filter_module);

	...

    cookie = ngx_http_parse_multi_header_lines(r, r->headers_in.cookie,
                                               &conf->name, &ctx->cookie);
    if (cookie == NULL) {
        return ctx;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "uid cookie: \"%V\"", &ctx->cookie);

    if (ctx->cookie.len < 22) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "client sent too short userid cookie \"%V\"",
                      &cookie->value);
        return ctx;
    }

    src = ctx->cookie;

    /*
     * we have to limit the encoded string to 22 characters because
     *  1) cookie may be marked by "userid_mark",
     *  2) and there are already the millions cookies with a garbage
     *     instead of the correct base64 trail "=="
     */

    src.len = 22;

    dst.data = (u_char *) ctx->uid_got;

    if (ngx_decode_base64(&dst, &src) == NGX_ERROR) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "client sent invalid userid cookie \"%V\"",
                      &cookie->value);
        return ctx;
    }

    ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "uid: %08XD%08XD%08XD%08XD",
                   ctx->uid_got[0], ctx->uid_got[1],
                   ctx->uid_got[2], ctx->uid_got[3]);

    return ctx;
}

首先通过 ngx_http_parse_multi_header_lines 查找cookie中 uid的字段值,存到ctx的结构体中。

 cookie = ngx_http_parse_multi_header_lines(r, r->headers_in.cookie,
                                           &conf->name, &ctx->cookie);

ngx_http_parse_multi_header_lines这个函数虽然叫分析header,但是我看了下他的代码实现更像是解析cookie的。它传入3个参数,存放header(其实是cookie,如果请求中有多个cookie header字段,那么就会对应多个数组元素)的数组,cookie字段名的字符串,以及要将查找出来的字符串存放到的位置。返回值cookie字段所在的header数组的index,没查到则返回 NGX_DECLINED,是一个负值。这个函数的返回值在这里没啥太大作用。

拿到 uid之后,就做了两个简单的操作,一个是长度是否小于22,另一是base64解码,解码的时候只会取uid的前22个字符,所以只要前22个字符合法就可以,并存到ctx->uid_got。

异常的话分别会打error log client sent too short userid cookie 或者 client sent invalid userid cookie 。

生成uid

ngx_http_userid_set_uid会先通过调用 ngx_http_userid_create_uid来生成uid。

ngx_http_userid_create_uid会将uid的四个int数据存到 ctx->uid_set中,uid_set和uid_got一样都是一个长度为4的int数组,如果ctx->uid_got已经有数据了,就会直接复制到uid_set中。

如果uid_got中没有的话就会生成uid。根据配置中的userid的on和v1的区别,生存逻辑略有不同。v1的生成逻辑比较简单。

	        if (conf->service == NGX_CONF_UNSET) {
	            ctx->uid_set[0] = 0;
	        } else {
	            ctx->uid_set[0] = conf->service;
	        }
	        ctx->uid_set[1] = (uint32_t) ngx_time();
	        ctx->uid_set[2] = start_value;
	        ctx->uid_set[3] = sequencer_v1;
	        sequencer_v1 += 0x100;

uid_set[0] 是个固定值,uid_set[2]每个worker是固定的。

默认的on的逻辑稍微复杂一些,比如uid_set[0]使用了监听连接地址。但是总得来看他们的生成逻辑差不太多,如果你一直使用同一个nginx,同一个worker接收请求,会发现生成出来的uid有很多位是一直不变的。uid_set[1] 和 uid_set[3]分别是nginx的当前时间和一个计数器,uid的生成更接近一个顺序增加产生的,由于里面包含时间信息,几乎不用担心uid冲突。

uid 信息提取

根据上面的生成逻辑,我们可以知道nginx userid 模块生成的cookie是有服务端地址和生成时间的,我们可以写一个简单的脚本来分析这个cookie。 下面是一段python3代码

import base64
import datetime

class CookieUID(object):
    def __init__(self, cookie_uid):
        self.cookie_uid = cookie_uid
        self.b_cookie_uid = b''
        self.check_and_b64decode()

    def check_and_b64decode(self):
        if len(self.cookie_uid) != 22 and len(self.cookie_uid) != 24:
            raise ValueError('cookie uid 的长度需要时22或者24')
        if len(self.cookie_uid) == 22:
            self.cookie_uid += '=='
        elif self.cookie_uid[-2:] != '==':
            raise ValueError('24字节的cookie_uid 需要以 == 结尾')
            
        self.b_cookie_uid = base64.b64decode(self.cookie_uid)

    def print_info(self):
        self.print_server_addr()
        self.print_generated_date()

    def print_server_addr(self):
        print('server_addr: ', end='')
        for i in range(4):
            print(self.b_cookie_uid[i], end='')
            if i < 3:
                print('.', end='')
            else:
                print('')

    def print_generated_date(self):
        generated_timestamp = int.from_bytes(self.b_cookie_uid[4:8])
        print('cookie uid generate time: ', datetime.datetime.fromtimestamp(generated_timestamp))


if __name__ == '__main__':
    cookie_uid = CookieUID('fwAAAWWFOcoflzElAwMGAg==')

输出结果是

server_addr: 127.0.0.1
cookie uid generate time:  2023-12-22 15:24:58

写入uid

ngx_http_userid_set_uid 调用完生成userid_create_uid 之后就进行生产cookie的操作。
他会先计算一下将要生产的cookie长度,然后申请一块内存。

cookie = ngx_pnalloc(r->pool, len);

然后将要生成的cookie数据写入或拷贝到cookie的内存中,第一段写入的就是userid对应的cookie

    p = ngx_copy(cookie, conf->name.data, conf->name.len);
    *p++ = '=';

    if (ctx->uid_got[3] == 0 || ctx->reset) {
        src.len = 16;
        src.data = (u_char *) ctx->uid_set;
        dst.data = p;

        ngx_encode_base64(&dst, &src);

        p += dst.len;

        if (conf->mark) {
            *(p - 2) = conf->mark;
        }

    } else {
        p = ngx_cpymem(p, ctx->cookie.data, 22);
        *p++ = conf->mark;
        *p++ = '=';
    }

他会先检查之前ctx->uid_got有没有获取到数据,有的话就直接拷贝之前存在ctx->cookie的数据,并且只会拷贝22个字符。没有的话,就通过之前create生成到ctx->uid_set中的字节通过base64变成成字符串。之后会写入一写其他cookie字段,比如配置中配的domain之类的。

最后通过 ngx_list_push申请header的链表节点结构体,将value指向之前生成的cookie数据上。

	set_cookie = ngx_list_push(&r->headers_out.headers);
    set_cookie->hash = 1;
    ngx_str_set(&set_cookie->key, "Set-Cookie");
    set_cookie->value.len = p - cookie;
    set_cookie->value.data = cookie;

p3p因为是一个单独的header,所以他也是通过 ngx_list_push 这种方式新增一个header节点。

写到这里其实有个疑问,按照这个模块的逻辑,不管之前请求中是否携带userid,响应头中都会进行set-cookie的操作,这个跟我们实际的现象不太相符。实际中如果有合法的userid cookie,nginx响应头不会再次进行返回set-cookie的header了,这需要后续仔细看下。

uid的插入时机

然后我们在使用中遇到一个问题是,nginx生成的uid是否能通过某些手段控制他的生成呢?比如满足某些情况通过add_header 将其set-cookie置空。这就涉及到nginx模块的执行循序问题。

nginx的header模块执行顺序是通过一个单向链表来实现,每个模块在初始化的时候,会将自己放到链表的头部

	static ngx_int_t
	ngx_http_userid_init(ngx_conf_t *cf)
	{
	    ngx_http_next_header_filter = ngx_http_top_header_filter;
	    ngx_http_top_header_filter = ngx_http_userid_filter;
	
	    return NGX_OK;
	}

nginx在处理请求时会遍历这个链表,依次执行对应的filter模块。所以模块初始化的逆序就是各个filter模块的执行顺序。而模块的初始化是在nginx编译的时候进行的,所以可以通过configure生成的ngx_modules.c的顺序来判断filter模块执行顺序。还是以add_header 和 userid为例。add_header属于ngx_http_header_filter_module,userid属于ngx_http_userid_filter_module。
在这里插入图片描述
userid在add_header(ngx_http_userid_filter_module)的上面,执行顺序是先执行add_header再执行userid。由于这两个都控制header的filter,所以按照优先级来看userid的优先级更高。

结语

以上就是全部内容了。这个简单的nginx http filter模块依然涉及很多nginx内部的框架逻辑,大部分都是自己阅读的,难免会有纰漏,恳请各位大佬斧正~

文章来源:https://blog.csdn.net/weixin_40619578/article/details/135067747
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。