nginx userid到底做了啥?

我们公司在用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 首先会判断之前的userid_get 中是否已经正确的获取数据,并进行userid_mark 校验。

if (conf->mark == '\0'
                || (ctx->cookie.len > 23
                    && ctx->cookie.data[22] == conf->mark
                    && ctx->cookie.data[23] == '='))
            {
                return NGX_OK;
            }

            ctx->uid_set[0] = ctx->uid_got[0];
            ctx->uid_set[1] = ctx->uid_got[1];
            ctx->uid_set[2] = ctx->uid_got[2];
            ctx->uid_set[3] = ctx->uid_got[3];

            return NGX_OK;

有数据或者userid_mark校验没问题就会之间返回NGX_OK。ngx_http_userid_set_uid发现 ctx->uid_set[3] 中没数据就会认为不需要设置cookie,会返回NGX_OK结束函数。所以当用户请求的cookie中携带合法的userid字段时,nginx就不会进行set-cookie操作。

如果userid_mark校验没有通过,则会将ctx->uid_got中的数据复制到ctx->uid_set中。此时nginx会在set-cookie中设置正确的userid_mark并返回给用户。

如果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==')
    cookie_uid.print_info()

输出结果是

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节点。

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的优先级更高。

总结

上面的源码解读通过代码执行的顺序读的,看起来可能有些乱,下面我简单总结一下。

  1. userid模块会进行请求校验:userid的长度是否不小22个字符,前22位是否能被base64解码,是否含userid_mark标记。都通过则不会进行任何操作。

  2. userid的长度是否不小22个字符,是否能被base64解码这两个校验不通过的话nginx error log 会记录 client sent too short userid cookie 或者 client sent invalid userid cookie 。并重新生成userid。userid_mark校验不通过会沿用之前的userid。这些情况下都会重新返回set-cookie。

  3. nginx userid的生成包含nginx server_addr 和 生成时间信息,这些可以通过base64解码打印出来。

  4. nginx userid对header操作默认优先级要高于add_header。如果要修改需要修改ngx_modules.c 并重新编译

结语

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

  • 19
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值