Nginx authrequest 入门与实践 – wiki基地


Nginx auth_request 入门与实践

1. 引言

在现代 Web 应用架构中,认证和授权是不可或缺的安全组成部分。Nginx 作为高性能的 Web 服务器和反向代理,提供了多种实现认证授权的方式,其中 auth_request 模块以其灵活、解耦的特性,成为构建微服务和 API 网关认证层的重要工具。

本文将详细介绍 Nginx auth_request 模块的工作原理、配置方法,并通过实例展示如何在实践中应用它来增强 Web 服务的安全性。

2. 什么是 auth_request 模块?

auth_request 是 Nginx 的一个标准模块,它允许 Nginx 通过发送一个“子请求”(subrequest)到一个外部认证服务来验证客户端请求的合法性。根据子请求返回的 HTTP 状态码,Nginx 决定是否允许原始请求继续处理。

为什么使用 auth_request

  • 解耦认证逻辑: 将认证逻辑从业务服务中剥离,集中到一个独立的认证服务中处理。这样可以使认证服务独立部署、升级和扩展,并减少业务服务的复杂度。
  • 灵活性: 认证服务可以使用任何语言或框架实现(如 Go、Python、Node.js、Java),只需遵循 HTTP 协议即可。
  • 统一认证: 对于多个后端服务,可以在 Nginx 层实现统一的认证策略,无需在每个服务中重复实现。
  • 高性能: Nginx 异步处理子请求,对性能影响较小。

3. auth_request 的工作原理

当 Nginx 接收到客户端请求时,如果配置了 auth_request,它会执行以下步骤:

  1. 发送子请求: Nginx 会向 auth_request 指令指定的 URL 发送一个内部子请求。这个子请求会包含原始请求的某些头部和数据(如果配置了传递)。
  2. 认证服务处理: 外部认证服务接收到子请求后,会根据请求中的信息(如 Authorization 头部的 Token、Cookie 等)进行身份验证和权限检查。
  3. 返回状态码:
    • 如果认证成功,认证服务返回 2xx 范围内的 HTTP 状态码(通常是 200 OK)。
    • 如果认证失败(如 Token 无效、过期、缺少),认证服务返回 401 Unauthorized403 Forbidden
    • 其他状态码,例如 5xx,通常表示认证服务内部错误。
  4. Nginx 处理:
    • 如果子请求返回 200204,Nginx 认为认证成功,会继续处理原始客户端请求,并将其代理到后端业务服务。
    • 如果子请求返回 401403,Nginx 会拒绝原始客户端请求,并将相应的状态码直接返回给客户端。
    • 对于其他非 2xx 的状态码,Nginx 默认也会拒绝原始请求。可以通过 error_page 指令进行定制化处理。

请求头部的传递:

原始请求的头部默认不会全部传递给子请求。通常需要使用 proxy_set_headerauth_request_set 等指令显式地将必要的头部(如 AuthorizationCookie 等)传递给认证子请求,以便认证服务进行验证。

4. 核心配置指令

  • auth_request <uri>;
    • 上下文: http, server, location
    • 作用: 开启 auth_request 功能,并指定用于认证的子请求 URI。当此 URI 返回 200204 时,认证成功;返回 401403 时,认证失败。
  • auth_request_set $variable <value>;
    • 上下文: http, server, location
    • 作用: 在子请求处理完成后,将子请求响应中的特定头部或变量值保存到 Nginx 变量中。这在认证服务需要将用户 ID 等信息回传给业务服务时非常有用。
      • 例如:auth_request_set $user $upstream_http_x_user_id; 表示将认证服务响应头 X-User-ID 的值赋给 Nginx 变量 $user
  • auth_request_set_headers $variable <value>; (不常用,通过 proxy_set_header 传递给子请求)
    • 这个指令用于从子请求的响应中提取头部,然后将这些头部添加到原始请求中,以便发送到最终的后端服务器。但通常我们直接使用 auth_request_set 结合 proxy_set_header 在上游传递。
  • proxy_pass_request_headers on|off;
    • 上下文: http, server, location
    • 作用: 控制原始请求的头部是否传递给代理服务器(包括子请求)。on 为默认值,表示传递。
  • proxy_pass_request_body on|off;
    • 上下文: http, server, location
    • 作用: 控制原始请求的请求体是否传递给代理服务器(包括子请求)。on 为默认值,表示传递。在认证服务需要验证请求体中的某些信息时可能需要设置为 on

5. 实践示例:使用 auth_request 进行 Token 认证

本示例将展示如何配置 Nginx,使其将所有对 /api/ 路径的请求转发给一个认证服务进行 Token 验证。

架构图:

客户端请求 --> Nginx (反向代理)
|
|-- 发送子请求 (包含认证 Token) --> 认证服务 (验证 Token)
| |
| `-- 返回 200/401/403
|
`-- 如果认证成功 --> 后端业务服务

5.1 认证服务 (auth-service)

假设我们有一个简单的认证服务,它部署在 http://127.0.0.1:8081/auth
这个服务会检查请求头部 Authorization 是否包含有效的 Bearer Token。

认证服务逻辑(伪代码):

收到请求 /auth
从请求头部获取 "Authorization"
如果 Authorization 头部不存在 或 不以 "Bearer " 开头
返回 401 Unauthorized
提取 Token (e.g., "Bearer YOUR_TOKEN_HERE")
验证 Token (例如:检查是否是预设的有效 Token,或者调用 IDP 验证)
如果 Token 有效
在响应中添加头部 X-User-ID: some_user_id
返回 200 OK
否则
返回 401 Unauthorized

5.2 Nginx 配置

我们将配置两个 location 块:一个用于业务 API (/api/),一个用于内部认证服务 (/_auth)。

“`nginx

nginx.conf

http {
include mime.types;
default_type application/octet-stream;

sendfile        on;
keepalive_timeout  65;

# 定义上游认证服务
upstream auth_backend {
    server 127.0.0.1:8081; # 认证服务地址
    # server auth-service:8081; # 如果在 Docker 或 K8s 中,可以使用服务名
}

# 定义上游业务服务
upstream api_backend {
    server 127.0.0.1:8080; # 业务服务地址
    # server my-api-service:8080; # 如果在 Docker 或 K8s 中,可以使用服务名
}

server {
    listen       80;
    server_name  localhost;

    # 任何对 /api/ 的请求都需要进行认证
    location /api/ {
        # 启用 auth_request 并指定认证子请求的 URI
        auth_request /_auth;

        # 可选:将认证服务返回的特定头部(如用户ID)注入到原始请求中,再传给业务服务
        # 例如,认证服务验证成功后返回 X-User-ID 头部
        auth_request_set $user_id $upstream_http_x_user_id;
        proxy_set_header X-User-ID $user_id;

        # 将客户端的 Authorization 头部转发给认证服务 (在 /_auth location 中处理)
        # 同时也转发给最终的业务服务
        proxy_set_header Authorization $http_authorization;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 代理到实际的业务服务
        proxy_pass http://api_backend;

        # 认证失败时的自定义处理 (可选)
        error_page 401 = @unauthorized;
        error_page 403 = @forbidden;
    }

    # 内部认证子请求的处理 location
    # 注意:这个 location 不应该直接暴露给外部客户端访问
    location = /_auth {
        internal; # 标记为内部请求,外部无法直接访问

        # 将客户端的 Authorization 头部转发给认证服务
        proxy_set_header Authorization $http_authorization;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 将请求代理到认证服务
        proxy_pass http://auth_backend/auth;
    }

    # 认证失败时的处理 (可选)
    location @unauthorized {
        return 401 "Unauthorized - Please provide a valid token.\n";
    }

    location @forbidden {
        return 403 "Forbidden - You do not have permission.\n";
    }

    # 其他静态文件或非认证路径
    location / {
        root   html;
        index  index.html index.htm;
    }

    # error_page 500 502 503 504  /50x.html;
    # location = /50x.html {
    #     root   html;
    # }
}

}
“`

5.3 运行验证

  1. 启动业务服务 (mock-api-service):
    创建一个简单的 Web 服务,例如使用 Python Flask,监听 8080 端口:

    “`python

    api_service.py

    from flask import Flask, request, jsonify

    app = Flask(name)

    @app.route(‘/api/data’)
    def get_data():
    user_id = request.headers.get(‘X-User-ID’, ‘Guest’)
    return jsonify({“message”: f”Hello, {user_id}! This is your data.”})

    if name == ‘main‘:
    app.run(port=8080, debug=True)
    ``
    运行:
    python api_service.py`

  2. 启动认证服务 (auth-service):
    创建一个简单的 Web 服务,例如使用 Python Flask,监听 8081 端口:

    “`python

    auth_service.py

    from flask import Flask, request, Response

    app = Flask(name)

    VALID_TOKEN = “my-secret-token-123”

    @app.route(‘/auth’)
    def auth():
    auth_header = request.headers.get(‘Authorization’)
    if not auth_header or not auth_header.startswith(‘Bearer ‘):
    return Response(“Unauthorized: Missing or invalid Authorization header”, status=401)

    token = auth_header.split('Bearer ')[1]
    if token == VALID_TOKEN:
        resp = Response("OK", status=200)
        resp.headers['X-User-ID'] = 'user_abc' # 返回用户ID给 Nginx
        return resp
    else:
        return Response("Unauthorized: Invalid token", status=401)
    

    if name == ‘main‘:
    app.run(port=8081, debug=True)
    ``
    运行:
    python auth_service.py`

  3. 启动 Nginx:
    确保 Nginx 配置正确并启动。

  4. 测试:

    • 无 Token 访问:
      curl http://localhost/api/data
      预期结果:401 Unauthorized - Please provide a valid token. (由 Nginx 返回)

    • 错误 Token 访问:
      curl -H "Authorization: Bearer wrong-token" http://localhost/api/data
      预期结果:401 Unauthorized - Please provide a valid token. (由 Nginx 返回)

    • 正确 Token 访问:
      curl -H "Authorization: Bearer my-secret-token-123" http://localhost/api/data
      预期结果:{"message": "Hello, user_abc! This is your data."} (由业务服务返回)

6. 高级用法与最佳实践

6.1 传递自定义头部给业务服务

在上述示例中,我们使用了 auth_request_set $user_id $upstream_http_x_user_id; 来捕获认证服务响应头中的 X-User-ID,然后通过 proxy_set_header X-User-ID $user_id; 将其重新添加到原始请求的头部中,再转发给后端业务服务。这样业务服务就能直接获取到认证后的用户信息,而无需再次解析 Token。

6.2 错误页面定制

通过 error_page 指令,你可以为 401403 错误码定义自定义的错误页面或处理逻辑,例如重定向到登录页面。

“`nginx
location /api/ {
auth_request /_auth;
error_page 401 = @unauthorized; # 当认证服务返回401时,内部跳转到 @unauthorized
# …
}

location @unauthorized {
# 可以返回自定义的错误信息
return 401 “Need to login. Please provide a valid Authorization header.”;
# 也可以重定向到登录页面
# rewrite ^ /login.html redirect;
}
“`

6.3 缓存认证结果

对于频繁的认证请求,认证服务的开销可能较大。可以通过 Nginx 的 proxy_cache 机制来缓存认证子请求的结果。但这需要谨慎配置,因为认证信息通常是动态且有时效性的。

“`nginx

http 块中配置

proxy_cache_path /var/cache/nginx/auth_cache levels=1:2 keys_zone=auth_cache:10m inactive=60m;

server {
# …
location = /_auth {
internal;
proxy_cache auth_cache; # 启用缓存
proxy_cache_valid 200 204 1m; # 缓存成功的认证结果1分钟
proxy_cache_valid 401 403 1s; # 缓存失败的认证结果1秒 (避免短时间内重复尝试无效Token)
proxy_cache_key “$host$request_uri$http_authorization”; # 缓存键包含 Authorization 头

    proxy_pass http://auth_backend/auth;
}
# ...

}
“`
注意: 缓存认证结果需要非常小心,确保缓存的有效性和安全性。通常只对某些不频繁变动的认证信息进行短期缓存。

6.4 将请求体传递给认证服务

如果认证逻辑需要验证请求体中的某些内容(例如注册请求中的密码),你可以将 proxy_pass_request_body 设置为 on,并在 location = /_auth 中配置 proxy_pass_request_body on;

nginx
location = /_auth {
internal;
proxy_pass_request_body on; # 允许传递请求体
proxy_set_header Content-Type $http_content_type; # 确保 Content-Type 也传递
# ...
proxy_pass http://auth_backend/auth;
}

但这会增加认证服务的负载,并且通常不推荐将请求体用于认证,而应在 Token 中包含所有必要信息。

7. 安全注意事项

  • 内部 location 认证子请求的 location(例如 /_auth)必须使用 internal; 指令,防止外部客户端直接访问认证服务,绕过 Nginx 的认证逻辑。
  • 敏感信息传输: 确保认证 Token 等敏感信息通过 HTTPS 加密传输,无论是在客户端与 Nginx 之间,还是 Nginx 与认证服务之间。
  • 错误信息: 避免在认证失败时返回过于详细的错误信息,以免泄露内部系统信息。
  • 认证服务鲁棒性: 认证服务本身应该具备高可用性和容错能力,因为它是整个系统的安全入口。
  • 拒绝服务攻击: 考虑在 Nginx 层对异常的认证尝试进行限速或封禁,以防御暴力破解和拒绝服务攻击。

8. 总结

Nginx 的 auth_request 模块是一个强大而灵活的工具,能够有效地将认证和授权逻辑从核心业务服务中解耦出来,实现统一的安全策略管理。通过合理配置,你可以构建出高性能、可扩展且安全的 Web 应用。在实践中,理解其工作原理,并结合高级特性和安全最佳实践,将帮助你更好地利用这一模块。


滚动至顶部