我们公司在用nginx的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 userid 是一个 http filter 模块,请求进来后通过调用 ngx_http_userid_filter 这个函数来执行 userid的逻辑,ngx_http_userid_filter这个函数主要调用了 ngx_http_userid_get_uid 和 ngx_http_userid_set_uid。分别用于获取和生成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 。
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冲突。
根据上面的生成逻辑,我们可以知道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
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了,这需要后续仔细看下。
然后我们在使用中遇到一个问题是,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内部的框架逻辑,大部分都是自己阅读的,难免会有纰漏,恳请各位大佬斧正~