解读nginx第三方模块ngx_http_auth_request_module

nginx强大的可扩展性和可定制性不仅保证自己的许多功能模块化,而且也催生许多优秀的第三方模块,本文要介绍的ngx_http_auth_request_module就是其中之一。

这个模块可以实现基于服务器内部子请求的返回结果来控制用户鉴权。比如子请求返回4xx则提示说没有权限,如果是2xx则会返回资源给客户端,返回401错误则把子请求的鉴权头透传给客户端。这样通过服务器内部逻辑就可以控制鉴权,省去不少业务逻辑层的脚本代码(当然还是不能否认php处理业务时的强大)

我们主要是出于学习的目的,看如何利用nginx提供的函数实现服务器内部子请求的访问和控制(主要注释关键数据结构和关键代码):

/*
 * Copyright (C) Maxim Dounin
 */


#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>


typedef struct {
    ngx_str_t                 uri;		    //子请求的uri地址
} ngx_http_auth_request_conf_t;

typedef struct {
    ngx_uint_t                done;		    //标记子请求是否完成
    ngx_uint_t                status;       //子请求响应的状态码:2xx,4xx,5xx...
    ngx_http_request_t       *subrequest;   //指向子请求的指针
} ngx_http_auth_request_ctx_t;			    //auth_request模块的上下文


//auth_request的handler函数
static ngx_int_t ngx_http_auth_request_handler(ngx_http_request_t *r);

//子请求结束时调用的函数
static ngx_int_t ngx_http_auth_request_done(ngx_http_request_t *r,
    void *data, ngx_int_t rc);
	
//下面三个函数是模块初始配置,大家都熟悉的
static void *ngx_http_auth_request_create_conf(ngx_conf_t *cf);
static char *ngx_http_auth_request_merge_conf(ngx_conf_t *cf,
    void *parent, void *child);
static ngx_int_t ngx_http_auth_request_init(ngx_conf_t *cf);


static ngx_command_t  ngx_http_auth_request_commands[] = {
    { ngx_string("auth_request"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_auth_request_conf_t, uri),
      NULL },

      ngx_null_command
};


static ngx_http_module_t  ngx_http_auth_request_module_ctx = {
    NULL,                                  /* preconfiguration */
    ngx_http_auth_request_init,            /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_auth_request_create_conf,     /* create location configuration */
    ngx_http_auth_request_merge_conf       /* merge location configuration */
};


ngx_module_t  ngx_http_auth_request_module = {
    NGX_MODULE_V1,
    &ngx_http_auth_request_module_ctx,     /* module context */
    ngx_http_auth_request_commands,        /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};


static ngx_int_t
ngx_http_auth_request_handler(ngx_http_request_t *r)
{
    ngx_table_elt_t               *h, *ho;	//nginx一个表结构,可以当做一个简单的关联结构体 key -> value
    ngx_http_request_t            *sr;      //子请求
    ngx_http_post_subrequest_t    *ps;      //子请求结束时的回调结构,可注册回调函数
    ngx_http_auth_request_ctx_t   *ctx;     //父请求的上下文,可保存一些中间内容
    ngx_http_auth_request_conf_t  *arcf;    //auth_request的配置信息

    arcf = ngx_http_get_module_loc_conf(r, ngx_http_auth_request_module);

    if (arcf->uri.len == 0) {				//如果没有配置此模块的指令,则返回NGX_DECLINED继续进行余下的phase处理
        return NGX_DECLINED;
    }

    ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "auth request handler");

    ctx = ngx_http_get_module_ctx(r, ngx_http_auth_request_module);   //获取父请求的上下文,子请求是不会进入到ngx_http_auth_request_handler函数的,因为它的作用域是location内

    if (ctx != NULL) {         //父请求第n+1次进入此handler函数(n>0)
        if (!ctx->done) {      //如果子请求没有结束,则返回NGX_AGAIN告诉nginx还需要等待网络事件再次进行处理
            return NGX_AGAIN;
        }

		//子请求结束了
        if (ctx->status == NGX_HTTP_FORBIDDEN) {   //返回403错误
            return ctx->status;
        }

        if (ctx->status == NGX_HTTP_UNAUTHORIZED) {  //如果是401没有鉴权错误,则获取子请求的响应头中的鉴权信息,添加到父请求的响应头中去
            sr = ctx->subrequest;

            h = sr->headers_out.www_authenticate;

            if (!h && sr->upstream) {
                h = sr->upstream->headers_in.www_authenticate;
            }

            if (h) {
                ho = ngx_list_push(&r->headers_out.headers); //headers数组结构,调用ngx_list_push增加一个新元素
                if (ho == NULL) {
                    return NGX_ERROR;
                }

                *ho = *h;

                r->headers_out.www_authenticate = ho;
            }

            return ctx->status;
        }

        if (ctx->status >= NGX_HTTP_OK            //子请求鉴权通过则返回正确资源
            && ctx->status < NGX_HTTP_SPECIAL_RESPONSE)
        {
            return NGX_OK;
        }

        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                      "auth request unexpected status: %d", ctx->status);

        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

	//父请求第1次进入ngx_http_auth_request_handler函数
    ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_auth_request_ctx_t)); //第一次进入父请求,需要初始化上下文
    if (ctx == NULL) {
        return NGX_ERROR;
    }

	//初始化子请求回调结构体
    ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t));
    if (ps == NULL) {
        return NGX_ERROR;
    }

	//注册子请求结束时的回调函数,把父请求的上下文挂载到子请求的数据指针中
	//这样ngx_http_auth_request_done函数的第二个参数就是父请求的上下文信息了
    ps->handler = ngx_http_auth_request_done;
    ps->data = ctx;

	//发起子请求
    if (ngx_http_subrequest(r, &arcf->uri, NULL, &sr, ps,
                            NGX_HTTP_SUBREQUEST_WAITED)
        != NGX_OK)
    {
        return NGX_ERROR;
    }

    sr->header_only = 1;  //子请求返回时只包含响应头信息

    ctx->subrequest = sr; //父请求上下文记录子请求的指针

    ngx_http_set_ctx(r, ctx, ngx_http_auth_request_module); //保存父请求上下文

    return NGX_AGAIN;
}


static ngx_int_t
ngx_http_auth_request_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
{
    ngx_http_auth_request_ctx_t   *ctx = data;  //获取父请求的上下文

    ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                   "auth request done s:%d", r->headers_out.status);

    ctx->done = 1;								//标记子请求已完成且记录子请求响应头信息
    ctx->status = r->headers_out.status;

    return rc;
}


static void *
ngx_http_auth_request_create_conf(ngx_conf_t *cf)
{
    ngx_http_auth_request_conf_t  *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_auth_request_conf_t));
    if (conf == NULL) {
        return NULL;
    }

    /*
     * set by ngx_pcalloc():
     *
     *     conf->uri.len = { 0, NULL };
     */

    return conf;
}


static char *
ngx_http_auth_request_merge_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_auth_request_conf_t *prev = parent;
    ngx_http_auth_request_conf_t *conf = child;

    ngx_conf_merge_str_value(conf->uri, prev->uri, "");

    return NGX_CONF_OK;
}


static ngx_int_t
ngx_http_auth_request_init(ngx_conf_t *cf)
{
    ngx_http_handler_pt        *h;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_ACCESS_PHASE].handlers);	//挂载到ACCESS的PHASE阶段
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_http_auth_request_handler;

    return NGX_OK;
}
下面参照一些Emiler关于 nginx模块高级开发的章节来详细说说子请求,首先是发起子请求的函数定义

ngx_int_t ngx_http_subrequest(ngx_http_request_t *r,
    ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **psr, 
        ngx_http_post_subrequest_t *ps, ngx_uint_t flags)

*r 表示父请求
*uri 和 *args 分别指向子请求的uri地址和传递参数
**psr 指向子请求的指针
*ps 子请求结束时的回调结构
flags 比如NGX_HTTP_SUBREQUEST_WAITED
其次,由于我们分析的是一个handler模块(作用域在Location中),所以handler函数不会被子请求所调用(想想一般子请求一般都是访问其它location或者索性其它的server),所以不存在区分父请求和子请求的问题。但是倘若我们要增加filter的过滤函数,则可以通过下面方法区分。

if (r == r->main) { 
    //父请求(r->parent) or 祖先请求(r->main)
} else {
    //子请求
}
我们或者会想,顺序地发起多个子请求可不可以照下面这样做呢:

int rc1, rc2, rc3;
rc1 = ngx_http_subrequest(r, uri1, ...);
rc2 = ngx_http_subrequest(r, uri2, ...);
rc3 = ngx_http_subrequest(r, uri3, ...);
Emiler给我们的答案是否定的,因为nginx是单线程的,需要通过事件信号驱动其各个请求的工作,比如下面几个信号

NGX_OK: 子请求完成了
NGX_DONE: 客户端重置了连接,比如关闭浏览器
NGX_ERROR: 出现内部错误
NGX_AGAIN: nginx需要过一段时间得到信号后再进来处理

这些都需要及时返回给调用的函数,它才能知道各个子请求的进展情况,下面才是正确的做法

typedef struct {
    ngx_array_t  uris;	//所有子请求的uri数组
    int          i;		//当前正在处理的子请求
} my_ctx_t;

static ngx_int_t
ngx_http_multiple_uris_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    my_ctx_t  *ctx;
    int rc = NGX_OK;
    ngx_http_request_t *sr;

	//子请求
    if (r != r->main) { 
        return ngx_http_next_body_filter(r, in);
    }

	//父请求,需要发送多个子请求
    ctx = ngx_http_get_module_ctx(r, my_module);
    if (ctx == NULL) {
        /* populate ctx and ctx->uris here */
    }
    while (rc == NGX_OK && ctx->i < ctx->uris.nelts) {
	    //每发起一个子请求就查看其状态,如果没有完成,就返回结果并把ctx->i加加,表示说下一次进来就需要发下一个请求
        rc = ngx_http_subrequest(r, &((ngx_str_t *)ctx->uris.elts)[ctx->i++],
            NULL /* args */, &sr, NULL /* cb */, 0 /* flags */);
    }

    return rc; /* NGX_OK/NGX_ERROR/NGX_DONE/NGX_AGAIN */
}

如果要同时(不是上面的顺序)发起多个连接怎么做呢,Emiler没有介绍,说是可以参照nginx自带的ssi模块,代码逻辑非常复杂,我也正在看,ssi模块上下文中有个wait变量好像是保证子请求和父请求的顺序的,还不是很清楚。希望有哪位大牛能出来介绍介绍。



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值