lolly/docs/34-nginx-oidc-module.md
xfy 4d108267c3 docs(nginx): 新增健康检查详解与7个高级模块文档
- 04-proxy-loadbalancing: 新增第18节主动健康检查详解
  - 被动检查vs主动检查对比、NGINX Plus health_check/match指令
  - Stream健康检查、gRPC健康检查、开源替代方案
- 新增 MQTT 模块文档 (33): broker负载均衡、Client ID路由
- 新增 OIDC 模块文档 (34): OpenID Connect认证、JWT验证
- 新增 Keyval 模块文档 (35): 动态键值存储、API管理接口
- 新增 流媒体模块文档 (36): HLS/FLV/MP4伪流媒体配置
- 新增 WebDAV 模块文档 (37): 文件共享服务器配置
- 新增 Zone Sync 模块文档 (38): 多节点状态同步
- 新增 HTTP Tunnel 模块文档 (39): HTTP CONNECT代理隧道
- 更新 README.md 目录索引

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 10:36:34 +08:00

42 KiB
Raw Blame History

NGINX OpenID Connect 模块指南

1. OIDC 模块概述

什么是 OpenID Connect

OpenID Connect (OIDC) 是基于 OAuth 2.0 协议的身份认证层允许客户端应用程序验证用户身份并获取用户基本信息。NGINX Plus 提供原生 OIDC 支持,可作为 Relying Party (RP) 与 Identity Provider (IdP) 集成,实现单点登录 (SSO) 和集中式身份认证。

模块用途

场景 说明
单点登录 (SSO) 用户一次登录,访问多个受保护应用
集中式认证 统一认证入口,后端服务无需处理认证逻辑
API 保护 验证 JWT Token保护 API 端点
身份代理 将身份信息传递给后端应用
会话管理 集中管理用户会话生命周期

架构图

┌─────────┐         ┌─────────────┐         ┌─────────────────┐
│  Client │────────▶│  NGINX Plus │────────▶│   IdP Provider  │
│ (浏览器) │         │   (OIDC RP)  │         │ (Keycloak/Okta) │
└─────────┘         └──────┬──────┘         └─────────────────┘
                           │
                           ▼
                    ┌──────────────┐
                    │   Backend    │
                    │ Applications │
                    └──────────────┘

工作流程

  1. 用户访问受保护资源NGINX 检查会话状态
  2. 无有效会话时,重定向到 IdP 登录页面
  3. 用户在 IdP 完成认证IdP 返回授权码
  4. NGINX 使用授权码交换 ID Token 和 Access Token
  5. 用户携带会话 Cookie 访问资源NGINX 验证 JWT
  6. 后端应用接收带有用户信息的请求

2. OIDC 认证流程

2.1 授权码流程 (Authorization Code Flow)

NGINX Plus 使用标准的 OAuth 2.0 授权码流程,支持 PKCE (Proof Key for Code Exchange) 增强安全性。

完整流程图

用户                          NGINX Plus                    IdP Provider
 |                                │                             │
 |── 1. 访问 /app ───────────────▶│                             │
 |                                │── 2. 检查会话 ─────────────▶│
 |                                │                             │
 |◀─ 3. 重定向到 /authorize ─────│                             │
 |                                │                             │
 |── 4. 登录认证 ───────────────────────────────▶│              │
 |                                │                             │
 |◀─ 5. 返回授权码 ───────────────│                             │
 |                                │                             │
 |── 6. 携带 code 回调 ──────────▶│                             │
 |                                │── 7. Token 请求 ───────────▶│
 |                                │                             │
 |                                │◀─ 8. ID/Access/Refresh Token─│
 |                                │                             │
 |◀─ 9. 设置 Session Cookie ─────│                             │
 |                                │                             │
 |── 10. 携带 Cookie 访问 ───────▶│                             │
 |                                │── 11. 验证 JWT ────────────▶│
 |                                │                             │
 |                                │── 12. 代理到后端 ──────────▶│

2.2 支持的 IdP 提供商

提供商 Discovery URL 格式
Keycloak https://keycloak.example.com/realms/{realm}/.well-known/openid-configuration
Okta https://{your-domain}.okta.com/.well-known/openid-configuration
Auth0 https://{your-domain}.auth0.com/.well-known/openid-configuration
Azure AD / Entra ID https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
Google https://accounts.google.com/.well-known/openid-configuration
GitHub https://token.actions.githubusercontent.com/.well-known/openid-configuration
自建 IdP https://auth.example.com/.well-known/openid-configuration

3. 指令参考

3.1 核心指令

auth_jwt

启用 JWT 认证。

属性 说明
语法 auth_jwt "realm" [token=$variable] | off;
默认值 off
上下文 http, server, location
# 基本用法
location /protected/ {
    auth_jwt "API Access";
    auth_jwt_key_file /etc/nginx/jwks.json;
}

# 从 Cookie 读取 Token
location /api/ {
    auth_jwt "API Access" token=$cookie_auth_token;
    auth_jwt_key_request /_jwks_uri;
}

# 从 Header 读取 Token
location /api/ {
    auth_jwt "API Access" token=$http_authorization;
    auth_jwt_key_request /_jwks_uri;
}

auth_jwt_key_file

指定 JWKS (JSON Web Key Set) 文件路径,用于验证 JWT 签名。

属性 说明
语法 auth_jwt_key_file file;
默认值
上下文 http, server, location
# 本地 JWKS 文件
auth_jwt_key_file /etc/nginx/oidc/jwks.json;

# 使用变量(动态配置)
auth_jwt_key_file $oidc_jwt_keyfile;

auth_jwt_key_request

从指定 location 动态获取 JWKS。

属性 说明
语法 auth_jwt_key_request uri;
默认值
上下文 http, server, location
server {
    location /protected/ {
        auth_jwt "API Access" token=$cookie_auth_token;
        auth_jwt_key_request /_jwks_uri;
    }

    # 内部 JWKS 端点
    location = /_jwks_uri {
        internal;                    # 仅限内部子请求访问
        proxy_pass https://idp.example.com/.well-known/jwks.json;
        proxy_cache jwks_cache;
        proxy_cache_valid 200 1h;
    }
}

auth_jwt_require

配置 JWT 验证的额外要求。

属性 说明
语法 auth_jwt_require $claim [value] ...;
默认值
上下文 http, server, location
# 要求特定的 issuer
location /protected/ {
    auth_jwt "API Access" token=$cookie_auth_token;
    auth_jwt_key_request /_jwks_uri;
    auth_jwt_require $jwt_claim_iss "https://idp.example.com";
}

# 要求特定的 audience
location /api/admin/ {
    auth_jwt "Admin Access" token=$cookie_auth_token;
    auth_jwt_key_request /_jwks_uri;
    auth_jwt_require $jwt_claim_aud "my-api-client";
}

# 组合条件
location /api/sensitive/ {
    auth_jwt "Sensitive Access" token=$cookie_auth_token;
    auth_jwt_key_request /_jwks_uri;
    auth_jwt_require $jwt_claim_iss "https://idp.example.com" $jwt_claim_aud "sensitive-api";
}

3.2 会话管理指令

keyval_zone

定义用于存储 OIDC 会话数据的共享内存区域。

属性 说明
语法 keyval_zone zone=name:size [state=file] [timeout=time];
默认值
上下文 http
# 基本配置
keyval_zone zone=oidc_id_tokens:1M timeout=1h;
keyval_zone zone=oidc_access_tokens:1M timeout=1h;
keyval_zone zone=oidc_refresh_tokens:1M timeout=8h;

# 带状态持久化
keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;

参数说明

  • zone=name:size - 共享内存区域名称和大小
  • state=file - 状态持久化文件路径
  • timeout=time - 数据过期时间

keyval

定义键值对存储。

属性 说明
语法 keyval $variable $variable zone=name;
默认值
上下文 http
# 定义存储变量
keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens;
keyval $cookie_auth_token $access_token zone=oidc_access_tokens;
keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens;

3.3 JavaScript 模块指令

js_import

导入 NGINX JavaScript (njs) 模块。

属性 说明
语法 js_import module [as namespace];
默认值
上下文 http
# 导入 OIDC 处理脚本
js_import /etc/nginx/conf.d/openid_connect.js;

# 使用命名空间
js_import /etc/nginx/conf.d/openid_connect.js as oidc;

js_set

设置 JavaScript 变量。

属性 说明
语法 js_set $variable function;
默认值
上下文 http
# 设置 OIDC 认证头
js_set $oidc_auth_header oidc.authHeader;

# 设置 ID Token
js_set $id_token oidc.idToken;

js_content

使用 JavaScript 生成响应内容。

属性 说明
语法 js_content function;
默认值
上下文 location
location /login {
    js_content oidc.login;
}

location /logout {
    js_content oidc.logout;
}

location = /redirect_uri {
    js_content oidc.redirect;
}

3.4 代理配置指令

auth_jwt_set

将 JWT Claims 提取到变量。

属性 说明
语法 auth_jwt_set $variable claim;
默认值
上下文 http, server, location
server {
    location /api/ {
        auth_jwt "API Access" token=$cookie_auth_token;
        auth_jwt_key_request /_jwks_uri;

        # 提取常用 Claims
        auth_jwt_set $jwt_sub sub;
        auth_jwt_set $jwt_email email;
        auth_jwt_set $jwt_name name;

        proxy_pass http://backend;
        proxy_set_header X-User-ID $jwt_sub;
        proxy_set_header X-User-Email $jwt_email;
        proxy_set_header X-User-Name $jwt_name;
    }
}

4. 变量参考

4.1 JWT Claims 变量

变量 说明
$jwt_claim_sub 用户唯一标识 (Subject)
$jwt_claim_iss Token 签发者 (Issuer)
$jwt_claim_aud Token 受众 (Audience)
$jwt_claim_exp Token 过期时间 (Expiration)
$jwt_claim_iat Token 签发时间 (Issued At)
$jwt_claim_nbf Token 生效时间 (Not Before)
$jwt_claim_jti Token 唯一标识 (JWT ID)
$jwt_claim_email 用户邮箱
$jwt_claim_name 用户名称
$jwt_claim_preferred_username 首选用户名
$jwt_claim_groups 用户组/角色
$jwt_claim_scope 授权范围

4.2 会话变量

变量 说明
$cookie_auth_token 会话 Cookie 值
$session_jwt 存储的 ID Token
$access_token 存储的 Access Token
$refresh_token 存储的 Refresh Token

5. IdP 集成配置示例

5.1 Keycloak 集成

Keycloak 配置要点

  • 创建 Client启用 Standard Flow (Authorization Code)
  • 配置 Valid Redirect URIs: https://app.example.com/redirect_uri
  • 启用 Client authentication,记录 Client Secret
  • 配置 Web Origins: https://app.example.com

NGINX 配置

http {
    # 加载 JavaScript 模块
    load_module modules/ngx_http_js_module.so;

    # 会话存储
    keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
    keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
    keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h;

    keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens;
    keyval $cookie_auth_token $access_token zone=oidc_access_tokens;
    keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens;

    # IdP 端点配置
    map $host $oidc_authz_endpoint {
        default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth";
    }

    map $host $oidc_token_endpoint {
        default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token";
    }

    map $host $oidc_jwks_uri {
        default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs";
    }

    map $host $oidc_userinfo_endpoint {
        default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/userinfo";
    }

    map $host $oidc_end_session_endpoint {
        default "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/logout";
    }

    # Client 配置
    map $host $oidc_client {
        default "my-nginx-client";
    }

    map $host $oidc_client_secret {
        default "your-client-secret-here";
    }

    map $host $oidc_scopes {
        default "openid+profile+email";
    }

    map $host $oidc_pkce_enable {
        default "1";
    }

    # 导入 OIDC 脚本
    js_import /etc/nginx/conf.d/openid_connect.js;

    # 缓存配置
    proxy_cache_path /var/cache/nginx/jwks levels=1:2 keys_zone=jwks_cache:1m max_size=10m inactive=60m use_temp_path=off;

    server {
        listen 443 ssl;
        server_name app.example.com;

        ssl_certificate /etc/nginx/ssl/app.example.com.crt;
        ssl_certificate_key /etc/nginx/ssl/app.example.com.key;

        # 会话 Cookie 配置
        set $session_cookie "auth_token";
        set $session_cookie_flags "Path=/; Secure; HttpOnly; SameSite=Strict";

        location / {
            # JWT 认证
            auth_jwt "Keycloak SSO" token=$cookie_auth_token;
            auth_jwt_key_request /_jwks_uri;

            # 提取用户信息
            auth_jwt_set $jwt_sub sub;
            auth_jwt_set $jwt_email email;

            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-User-ID $jwt_sub;
            proxy_set_header X-User-Email $jwt_email;
            proxy_set_header Authorization "Bearer $access_token";
        }

        # OIDC 端点
        location /login {
            js_content openid_connect.login;
        }

        location /logout {
            js_content openid_connect.logout;
        }

        location = /redirect_uri {
            js_content openid_connect.redirect;
        }

        # 内部 JWKS 端点
        location = /_jwks_uri {
            internal;
            proxy_pass $oidc_jwks_uri;
            proxy_cache jwks_cache;
            proxy_cache_valid 200 1h;
            proxy_cache_use_stale error timeout updating;
            proxy_ssl_server_name on;
        }
    }
}

5.2 Okta 集成

Okta 配置要点

  • 创建 App Integration选择 OIDC - OpenID Connect
  • Application type: Web Application
  • Sign-in redirect URIs: https://app.example.com/redirect_uri
  • Sign-out redirect URIs: https://app.example.com/
  • Grant type: Authorization Code + Refresh Token

NGINX 配置

http {
    load_module modules/ngx_http_js_module.so;

    # 会话存储
    keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
    keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
    keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h;

    keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens;
    keyval $cookie_auth_token $access_token zone=oidc_access_tokens;
    keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens;

    # Okta 端点配置
    map $host $oidc_authz_endpoint {
        default "https://myorg.okta.com/oauth2/default/v1/authorize";
        # 或使用自定义授权服务器
        # default "https://myorg.okta.com/oauth2/ausxxxxxx/v1/authorize";
    }

    map $host $oidc_token_endpoint {
        default "https://myorg.okta.com/oauth2/default/v1/token";
    }

    map $host $oidc_jwks_uri {
        default "https://myorg.okta.com/oauth2/default/v1/keys";
    }

    map $host $oidc_userinfo_endpoint {
        default "https://myorg.okta.com/oauth2/default/v1/userinfo";
    }

    map $host $oidc_end_session_endpoint {
        default "https://myorg.okta.com/oauth2/default/v1/logout";
    }

    map $host $oidc_client {
        default "0oaxxxxxxxxxxx";
    }

    map $host $oidc_client_secret {
        default "your-client-secret";
    }

    map $host $oidc_scopes {
        default "openid+profile+email+groups";
    }

    map $host $oidc_pkce_enable {
        default "1";
    }

    js_import /etc/nginx/conf.d/openid_connect.js;

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

    server {
        listen 443 ssl;
        server_name app.example.com;

        ssl_certificate /etc/nginx/ssl/app.example.com.crt;
        ssl_certificate_key /etc/nginx/ssl/app.example.com.key;

        location / {
            auth_jwt "Okta SSO" token=$cookie_auth_token;
            auth_jwt_key_request /_jwks_uri;

            auth_jwt_set $jwt_sub sub;
            auth_jwt_set $jwt_email email;
            auth_jwt_set $jwt_groups groups;

            proxy_pass http://backend;
            proxy_set_header X-User-ID $jwt_sub;
            proxy_set_header X-User-Email $jwt_email;
            proxy_set_header X-User-Groups $jwt_groups;
            proxy_set_header Authorization "Bearer $access_token";
        }

        location /login {
            js_content openid_connect.login;
        }

        location /logout {
            js_content openid_connect.logout;
        }

        location = /redirect_uri {
            js_content openid_connect.redirect;
        }

        location = /_jwks_uri {
            internal;
            proxy_pass $oidc_jwks_uri;
            proxy_cache jwks_cache;
            proxy_cache_valid 200 1h;
            proxy_ssl_server_name on;
        }
    }
}

5.3 Auth0 集成

Auth0 配置要点

  • 创建 Application类型选择 Regular Web Application
  • Allowed Callback URLs: https://app.example.com/redirect_uri
  • Allowed Logout URLs: https://app.example.com/
  • Allowed Web Origins: https://app.example.com
  • 启用 Refresh Token 轮换(可选)

NGINX 配置

http {
    load_module modules/ngx_http_js_module.so;

    # 会话存储
    keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
    keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
    keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h;

    keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens;
    keyval $cookie_auth_token $access_token zone=oidc_access_tokens;
    keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens;

    # Auth0 端点配置
    map $host $oidc_authz_endpoint {
        default "https://myapp.auth0.com/authorize";
    }

    map $host $oidc_token_endpoint {
        default "https://myapp.auth0.com/oauth/token";
    }

    map $host $oidc_jwks_uri {
        default "https://myapp.auth0.com/.well-known/jwks.json";
    }

    map $host $oidc_userinfo_endpoint {
        default "https://myapp.auth0.com/userinfo";
    }

    map $host $oidc_end_session_endpoint {
        default "https://myapp.auth0.com/v2/logout";
    }

    map $host $oidc_client {
        default "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    }

    map $host $oidc_client_secret {
        default "your-client-secret";
    }

    map $host $oidc_scopes {
        default "openid+profile+email";
    }

    map $host $oidc_pkce_enable {
        default "1";
    }

    # Auth0 特定:返回首页参数
    map $host $oidc_logout_redirect {
        default "https://app.example.com/";
    }

    js_import /etc/nginx/conf.d/openid_connect.js;

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

    server {
        listen 443 ssl;
        server_name app.example.com;

        ssl_certificate /etc/nginx/ssl/app.example.com.crt;
        ssl_certificate_key /etc/nginx/ssl/app.example.com.key;

        location / {
            auth_jwt "Auth0 SSO" token=$cookie_auth_token;
            auth_jwt_key_request /_jwks_uri;

            auth_jwt_set $jwt_sub sub;
            auth_jwt_set $jwt_email email;
            auth_jwt_set $jwt_name name;
            auth_jwt_set $jwt_nickname nickname;

            proxy_pass http://backend;
            proxy_set_header X-User-ID $jwt_sub;
            proxy_set_header X-User-Email $jwt_email;
            proxy_set_header X-User-Name $jwt_name;
            proxy_set_header Authorization "Bearer $access_token";
        }

        location /login {
            js_content openid_connect.login;
        }

        location /logout {
            js_content openid_connect.logout;
        }

        location = /redirect_uri {
            js_content openid_connect.redirect;
        }

        location = /_jwks_uri {
            internal;
            proxy_pass $oidc_jwks_uri;
            proxy_cache jwks_cache;
            proxy_cache_valid 200 1h;
            proxy_ssl_server_name on;
        }
    }
}

5.4 Azure AD / Entra ID 集成

Azure AD 配置要点

  • 在 Azure Portal 注册应用
  • 添加平台配置Web
  • 重定向 URI: https://app.example.com/redirect_uri
  • 启用 ID tokens (用于隐式和混合流)
  • 创建 Client Secret

NGINX 配置

http {
    load_module modules/ngx_http_js_module.so;

    # 会话存储
    keyval_zone zone=oidc_id_tokens:1M state=/var/lib/nginx/state/oidc_id_tokens.json timeout=1h;
    keyval_zone zone=oidc_access_tokens:1M state=/var/lib/nginx/state/oidc_access_tokens.json timeout=1h;
    keyval_zone zone=oidc_refresh_tokens:1M state=/var/lib/nginx/state/oidc_refresh_tokens.json timeout=8h;

    keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens;
    keyval $cookie_auth_token $access_token zone=oidc_access_tokens;
    keyval $cookie_auth_token $refresh_token zone=oidc_refresh_tokens;

    # Azure AD 端点配置
    map $host $oidc_authz_endpoint {
        default "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize";
    }

    map $host $oidc_token_endpoint {
        default "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token";
    }

    map $host $oidc_jwks_uri {
        default "https://login.microsoftonline.com/{tenant-id}/discovery/v2.0/keys";
    }

    map $host $oidc_userinfo_endpoint {
        default "https://graph.microsoft.com/oidc/userinfo";
    }

    map $host $oidc_end_session_endpoint {
        default "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/logout";
    }

    map $host $oidc_client {
        default "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    }

    map $host $oidc_client_secret {
        default "your-client-secret";
    }

    # Azure AD 使用空格分隔 scopes
    map $host $oidc_scopes {
        default "openid+profile+email";
    }

    map $host $oidc_pkce_enable {
        default "1";
    }

    js_import /etc/nginx/conf.d/openid_connect.js;

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

    server {
        listen 443 ssl;
        server_name app.example.com;

        ssl_certificate /etc/nginx/ssl/app.example.com.crt;
        ssl_certificate_key /etc/nginx/ssl/app.example.com.key;

        location / {
            auth_jwt "Azure AD SSO" token=$cookie_auth_token;
            auth_jwt_key_request /_jwks_uri;

            auth_jwt_set $jwt_sub sub;
            auth_jwt_set $jwt_email email;
            auth_jwt_set $jwt_name name;
            auth_jwt_set $jwt_oid oid;  # Azure AD Object ID

            proxy_pass http://backend;
            proxy_set_header X-User-ID $jwt_sub;
            proxy_set_header X-User-Email $jwt_email;
            proxy_set_header X-User-Name $jwt_name;
            proxy_set_header X-Azure-Object-ID $jwt_oid;
            proxy_set_header Authorization "Bearer $access_token";
        }

        location /login {
            js_content openid_connect.login;
        }

        location /logout {
            js_content openid_connect.logout;
        }

        location = /redirect_uri {
            js_content openid_connect.redirect;
        }

        location = /_jwks_uri {
            internal;
            proxy_pass $oidc_jwks_uri;
            proxy_cache jwks_cache;
            proxy_cache_valid 200 1h;
            proxy_ssl_server_name on;
        }
    }
}

6. JWT 验证与 Token 刷新

6.1 JWT 验证机制

验证流程

1. 提取 Token ──▶ 2. 解析 Header ──▶ 3. 获取公钥 ──▶ 4. 验证签名
                                              │
                                              ▼
5. 验证 Claims ◀── 6. 检查过期 ◀── 4. 验证签名

关键验证点

  • 签名验证:使用 IdP 的公钥验证 JWT 签名
  • Issuer 验证:确认 Token 来自正确的 IdP
  • Audience 验证:确认 Token 为此应用签发
  • 过期验证:检查 exp Claim
  • 生效时间:检查 nbf (Not Before) Claim

6.2 JWKS 缓存配置

# JWKS 缓存
proxy_cache_path /var/cache/nginx/jwks levels=1:2 keys_zone=jwks_cache:1m 
                 max_size=10m inactive=60m use_temp_path=off;

server {
    location = /_jwks_uri {
        internal;
        proxy_pass $oidc_jwks_uri;
        proxy_cache jwks_cache;
        proxy_cache_valid 200 1h;
        proxy_cache_use_stale error timeout updating;
        proxy_ssl_server_name on;
        
        # 连接超时设置
        proxy_connect_timeout 5s;
        proxy_send_timeout 5s;
        proxy_read_timeout 5s;
    }
}

6.3 Token 刷新机制

自动刷新流程

// openid_connect.js 中的刷新逻辑
function refreshToken(r) {
    // 1. 检查 Token 是否即将过期
    var token = r.variables.session_jwt;
    var payload = JSON.parse(jwtPayload(token));
    var exp = payload.exp;
    var now = Math.floor(Date.now() / 1000);
    
    // 2. 提前 60 秒刷新
    if (exp - now < 60) {
        // 3. 使用 Refresh Token 获取新 Token
        var refreshToken = r.variables.refresh_token;
        return exchangeRefreshToken(r, refreshToken);
    }
    
    return token;
}

配置示例

server {
    location / {
        # 使用 JavaScript 处理 Token 刷新
        js_set $validated_token oidc.validateAndRefresh;
        
        auth_jwt "API" token=$validated_token;
        auth_jwt_key_request /_jwks_uri;

        proxy_pass http://backend;
    }
}

6.4 多 IdP 支持

http {
    # 根据域名选择 IdP
    map $host $oidc_config {
        default                "keycloak";
        "app1.example.com"     "keycloak";
        "app2.example.com"     "okta";
        "app3.example.com"     "auth0";
    }

    # Keycloak 配置
    map $oidc_config $keycloak_authz_endpoint {
        "keycloak" "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/auth";
        default    "";
    }

    map $oidc_config $keycloak_token_endpoint {
        "keycloak" "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token";
        default    "";
    }

    # Okta 配置
    map $oidc_config $okta_authz_endpoint {
        "okta"  "https://myorg.okta.com/oauth2/default/v1/authorize";
        default "";
    }

    map $oidc_config $okta_token_endpoint {
        "okta"  "https://myorg.okta.com/oauth2/default/v1/token";
        default "";
    }

    # 统一端点变量
    map $oidc_config $oidc_authz_endpoint {
        "keycloak" $keycloak_authz_endpoint;
        "okta"     $okta_authz_endpoint;
    }

    map $oidc_config $oidc_token_endpoint {
        "keycloak" $keycloak_token_endpoint;
        "okta"     $okta_token_endpoint;
    }
}

7. JavaScript 模块 (njs) 实现

7.1 基础 openid_connect.js

// openid_connect.js
var qs = require('querystring');
var jwt = require('jwt');

// 生成随机状态码
function generateState() {
    return Math.random().toString(36).substring(2, 15) + 
           Math.random().toString(36).substring(2, 15);
}

// 生成 PKCE code_verifier
function generateCodeVerifier() {
    var bytes = [];
    for (var i = 0; i < 32; i++) {
        bytes.push(Math.floor(Math.random() * 256));
    }
    return bytesToBase64Url(bytes);
}

// Base64 URL 编码
function bytesToBase64Url(bytes) {
    var base64 = '';
    var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    for (var i = 0; i < bytes.length; i += 3) {
        var b1 = bytes[i];
        var b2 = bytes[i + 1] || 0;
        var b3 = bytes[i + 2] || 0;
        var bitmap = (b1 << 16) | (b2 << 8) | b3;
        base64 += chars[(bitmap >> 18) & 63];
        base64 += chars[(bitmap >> 12) & 63];
        base64 += (i + 1 < bytes.length) ? chars[(bitmap >> 6) & 63] : '=';
        base64 += (i + 2 < bytes.length) ? chars[bitmap & 63] : '=';
    }
    return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// 登录处理
function login(r) {
    var state = generateState();
    var redirectUri = 'https://' + r.headersIn['Host'] + '/redirect_uri';
    
    var params = {
        response_type: 'code',
        client_id: r.variables.oidc_client,
        redirect_uri: redirectUri,
        scope: r.variables.oidc_scopes.replace(/\+/g, ' '),
        state: state
    };
    
    // 启用 PKCE
    if (r.variables.oidc_pkce_enable === '1') {
        var codeVerifier = generateCodeVerifier();
        params.code_challenge = codeVerifier;
        params.code_challenge_method = 'S256';
        // 存储 code_verifier 用于后续 token 交换
        r.variables.code_verifier = codeVerifier;
    }
    
    var authUrl = r.variables.oidc_authz_endpoint + '?' + qs.stringify(params);
    r.return(302, authUrl);
}

// Token 交换处理
function redirect(r) {
    var args = r.args;
    
    // 检查错误
    if (args.error) {
        r.return(500, 'Authentication error: ' + args.error_description);
        return;
    }
    
    var code = args.code;
    var redirectUri = 'https://' + r.headersIn['Host'] + '/redirect_uri';
    
    // Token 请求
    var tokenReq = {
        method: 'POST',
        body: qs.stringify({
            grant_type: 'authorization_code',
            code: code,
            redirect_uri: redirectUri,
            client_id: r.variables.oidc_client,
            client_secret: r.variables.oidc_client_secret
        }),
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };
    
    // 发送 Token 请求
    r.subrequest('/_token', tokenReq, function(res) {
        if (res.status !== 200) {
            r.return(500, 'Token exchange failed');
            return;
        }
        
        var tokenData = JSON.parse(res.responseBody);
        var sessionId = generateState();
        
        // 存储 Token
        r.variables.session_jwt = tokenData.id_token;
        r.variables.access_token = tokenData.access_token;
        r.variables.refresh_token = tokenData.refresh_token || '';
        
        // 设置 Cookie 并重定向
        r.headersOut['Set-Cookie'] = 'auth_token=' + sessionId + '; ' + 
                                      r.variables.session_cookie_flags;
        r.return(302, '/');
    });
}

// 登出处理
function logout(r) {
    var sessionId = r.variables.cookie_auth_token;
    
    // 清除存储的 Token
    if (sessionId) {
        delete r.variables.session_jwt;
        delete r.variables.access_token;
        delete r.variables.refresh_token;
    }
    
    // 清除 Cookie
    r.headersOut['Set-Cookie'] = 'auth_token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT';
    
    // 如果配置了 end_session_endpoint重定向到 IdP 登出
    if (r.variables.oidc_end_session_endpoint) {
        var logoutUrl = r.variables.oidc_end_session_endpoint + 
                       '?post_logout_redirect_uri=' + 
                       encodeURIComponent('https://' + r.headersIn['Host'] + '/');
        r.return(302, logoutUrl);
    } else {
        r.return(302, '/');
    }
}

// Token 验证和刷新
function validateAndRefresh(r) {
    var token = r.variables.session_jwt;
    if (!token) {
        return '';
    }
    
    try {
        var payload = jwt.decode(token).payload;
        var exp = payload.exp;
        var now = Math.floor(Date.now() / 1000);
        
        // Token 即将过期,尝试刷新
        if (exp - now < 60 && r.variables.refresh_token) {
            // 异步刷新 Token
            refreshAccessToken(r);
        }
        
        return token;
    } catch (e) {
        return '';
    }
}

// 导出函数
export default { login, logout, redirect, validateAndRefresh };

7.2 高级功能扩展

// 前端频道登出处理
function frontChannelLogout(r) {
    var sid = r.args.sid;
    if (sid) {
        // 根据 sid 查找并清除会话
        clearSessionBySid(r, sid);
    }
    r.return(200, 'OK');
}

// 用户信息服务
function userInfo(r) {
    var token = r.variables.access_token;
    if (!token) {
        r.return(401, 'Unauthorized');
        return;
    }
    
    r.subrequest('/_userinfo', {
        method: 'GET',
        headers: {
            'Authorization': 'Bearer ' + token
        }
    }, function(res) {
        r.headersOut['Content-Type'] = 'application/json';
        r.return(res.status, res.responseBody);
    });
}

// 会话检查
function checkSession(r) {
    var token = r.variables.session_jwt;
    if (!token) {
        r.return(401, JSON.stringify({ active: false }));
        return;
    }
    
    try {
        var payload = jwt.decode(token).payload;
        r.return(200, JSON.stringify({
            active: true,
            sub: payload.sub,
            exp: payload.exp
        }));
    } catch (e) {
        r.return(401, JSON.stringify({ active: false }));
    }
}

8. 与 Lolly 项目的关系和建议

8.1 Lolly 项目概述

Lolly 是使用 Go 语言编写的高性能 HTTP 服务器与反向代理,与 NGINX 有相似的功能定位:

特性 NGINX Plus Lolly
语言 C Go
OIDC 支持 内置 (auth_jwt, njs) 待实现
配置 nginx.conf YAML
扩展 njs / C 模块 Go 中间件
性能 极高 (C + 事件驱动) 高 (Go + fasthttp)
HTTP/3 实验性 原生支持
热重载 支持 支持

8.2 Lolly OIDC 实现建议

基于 NGINX OIDC 模块的设计,建议 Lolly 实现以下功能:

配置结构示例

# Lolly OIDC 配置
oidc:
  enabled: true
  providers:
    - name: keycloak
      issuer: "https://keycloak.example.com/realms/myrealm"
      client_id: "lolly-client"
      client_secret: "${KEYCLOAK_SECRET}"
      scopes: ["openid", "profile", "email"]
      pkce_enabled: true
      redirect_uri: "https://app.example.com/auth/callback"
      
      # JWKS 配置
      jwks:
        url: "https://keycloak.example.com/realms/myrealm/protocol/openid-connect/certs"
        cache_ttl: "1h"
        
      # 会话配置
      session:
        cookie_name: "auth_token"
        cookie_secure: true
        cookie_http_only: true
        cookie_same_site: "Strict"
        ttl: "1h"
        
      # Token 刷新
      refresh:
        enabled: true
        before_expiry: "5m"
        
      # 用户信息转发
      claims_forward:
        - claim: "sub"
          header: "X-User-ID"
        - claim: "email"
          header: "X-User-Email"
        - claim: "name"
          header: "X-User-Name"

server:
  listen: ":443"
  
  # 全局 OIDC 保护
  oidc:
    provider: keycloak
    
  routes:
    # 公开路径(无需认证)
    - path: "/health"
      public: true
      static:
        response: '{"status":"ok"}'
        
    # 受保护 API
    - path: "/api/*"
      oidc:
        provider: keycloak
        require_claims:
          - claim: "groups"
            values: ["api-users"]
      proxy:
        target: "http://backend:8080"
        
    # 管理后台(更严格)
    - path: "/admin/*"
      oidc:
        provider: keycloak
        require_claims:
          - claim: "groups"
            values: ["admin"]
      proxy:
        target: "http://admin:8080"

建议的实现架构

┌─────────────────────────────────────────────────────────────┐
│                        Lolly Server                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │  OIDC Middleware │  │  Session Store   │  │  Token Validator │  │
│  │                  │  │  - In-Memory     │  │  - JWKS Fetch    │  │
│  │  - Login Handler │  │  - Redis         │  │  - JWT Verify    │  │
│  │  - Callback      │  │  - Cookie        │  │  - Claims Extract│  │
│  │  - Logout        │  │                  │  │                  │  │
│  │  - Refresh       │  │                  │  │                  │  │
│  └──────────────┘  └──────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────┘

推荐依赖库

功能 推荐库 说明
OIDC Client coreos/go-oidc 官方推荐,功能完整
JWT 解析 golang-jwt/jwt 最流行的 Go JWT 库
OAuth2 golang.org/x/oauth2 标准 OAuth2 实现
Session gorilla/sessions 成熟的 Session 管理

实现优先级建议

  1. Phase 1 - 基础功能

    • JWT 验证中间件
    • JWKS 获取和缓存
    • 基本 Claims 提取
  2. Phase 2 - 完整认证

    • 授权码流程
    • Session Cookie 管理
    • 登录/登出端点
  3. Phase 3 - 高级功能

    • Token 自动刷新
    • PKCE 支持
    • 多 IdP 支持
  4. Phase 4 - 企业特性

    • 前端频道登出
    • 用户信息服务
    • 细粒度权限控制

8.3 迁移建议

从 NGINX Plus OIDC 迁移到 Lolly

  1. 配置转换:使用工具将 nginx.conf 的 map 转换为 YAML
  2. IdP 配置复用Issuer、Client ID、Secret 保持不变
  3. 后端适配:验证 Header 名称一致性
  4. Session 迁移:逐步迁移,支持双写
  5. 灰度切换:基于域名或路径逐步切换

9. 故障排查

9.1 常见问题

Token 验证失败

# 启用详细日志
error_log /var/log/nginx/error.log debug;

# 检查 JWKS 是否获取成功
location = /_jwks_uri {
    internal;
    proxy_pass $oidc_jwks_uri;
    
    # 记录响应
    add_header X-JWKS-Status $upstream_status always;
    add_header X-JWKS-Cache $upstream_cache_status always;
}
# 确保 Cookie 配置正确
set $session_cookie "auth_token";
set $session_cookie_flags "Path=/; Secure; HttpOnly; SameSite=Lax";

# 检查 Cookie 是否设置
add_header X-Debug-Cookie $cookie_auth_token always;

9.2 调试端点

server {
    # 健康检查端点
    location /auth/health {
        auth_jwt off;
        default_type application/json;
        return 200 '{"status":"ok","provider":"$oidc_config"}';
    }
    
    # Token 信息端点(仅调试)
    location /auth/debug {
        auth_jwt "Debug" token=$cookie_auth_token;
        auth_jwt_key_request /_jwks_uri;
        
        auth_jwt_set $jwt_sub sub;
        auth_jwt_set $jwt_exp exp;
        
        default_type application/json;
        return 200 '{
            "sub": "$jwt_sub",
            "exp": "$jwt_exp",
            "provider": "$oidc_config"
        }';
    }
}

9.3 日志分析

# 查看认证相关日志
grep "auth_jwt\|oidc\|openid" /var/log/nginx/error.log | tail -100

# 监控 Token 刷新
awk '/token.*refresh/ {print $0}' /var/log/nginx/access.log

10. 最佳实践

10.1 安全建议

  1. 始终启用 HTTPS

    # 拒绝 HTTP 访问
    server {
        listen 80;
        return 301 https://$host$request_uri;
    }
    
  2. 使用 Secure Cookie

    set $session_cookie_flags "Path=/; Secure; HttpOnly; SameSite=Strict";
    
  3. 启用 PKCE

    map $host $oidc_pkce_enable {
        default "1";
    }
    
  4. 定期轮换 Client Secret

10.2 性能优化

  1. JWKS 缓存

    proxy_cache_valid 200 1h;
    proxy_cache_use_stale error timeout updating;
    
  2. 会话存储优化

    # 共享内存区域大小根据并发用户数调整
    keyval_zone zone=oidc_id_tokens:10M timeout=1h;
    
  3. 连接池

    upstream idp_backend {
        server keycloak.example.com:443;
        keepalive 32;
    }
    

10.3 高可用配置

# JWKS 多源备份
upstream jwks_upstream {
    server keycloak-primary.example.com:443;
    server keycloak-backup.example.com:443 backup;
}

location = /_jwks_uri {
    internal;
    proxy_pass https://jwks_upstream/realms/myrealm/protocol/openid-connect/certs;
    proxy_cache jwks_cache;
    proxy_cache_valid 200 1h;
}

参考资料