一、需求
整个网关要满足的目标可以概括为一句话:让员工能用 DeepSeek API,但既拿不到真实 Key,也无法相互影响或伪造身份。 拆开来是五条:
- 隐藏真实 Key。 真实 DeepSeek Key 只能存在于服务器上,员工抓包、看代码都不应该看到它。员工持有的是可单独吊销的内部令牌。
- 防止滥用。 单个员工的请求要能按人限流,避免一个人把账号级配额或余额占满,连累其他人。
- 可审计、可归因。 每一笔调用都要能追溯到具体的人,便于成本分摊和异常排查。
- 多用户隔离。 同一账号下不同员工之间,缓存与调度要相互隔离,且身份不可伪造。
- 不改变员工的使用习惯。 员工继续用标准 OpenAI / Anthropic SDK,只换
base_url和 key,不需要改业务代码。
需要特别说明的是第 4 点。DeepSeek 的对话接口本身是无状态的——完整对话历史装在每次请求的 messages 里,服务端不保存会话,所以不会出现不同用户上下文被串号的情况。真正会”相互影响”的是账号级资源:并发是按账号计算的,与用哪个 Key 无关,超限会返回 HTTP 429;余额也是共享的。DeepSeek 为此提供了 user_id 参数,用于内容安全隔离、KVCache 隔离(隐私)和调度隔离。我们的网关要做的,就是在中转层把这个 user_id 强制写成已鉴权的身份,让员工既无法省略、也无法伪造。
二、使用配置清单
软硬件环境
- 服务器:RHEL 系 Linux(RHEL 9 / Rocky 9 / Alma 9),x86_64,部署在
192.168.x内网 - 网关软件:OpenResty(nginx + LuaJIT + lua-nginx-module + lua-cjson 一体打包,版本约 1.29.x)
- 上游:DeepSeek API(
https://api.deepseek.com,Anthropic 格式为https://api.deepseek.com/anthropic) - 认证方式:HTTP Bearer,请求头
Authorization: Bearer <key>
为什么是 OpenResty 而不是发行版自带 nginx
发行版自带的 nginx 的标准源里没有 Lua 模块——dnf search nginx 只能看到 brotli、mail、stream 等模块。而我们改写请求体注入 user_id 必须依赖 access_by_lua。给现成 nginx 编译挂载 Lua 动态模块需要与 nginx 版本、LuaJIT 严格匹配,极易踩坑。而OpenResty 是独立发行版,把这套东西全部打包好,开箱即用,是更稳妥的选择。
关键配置文件清单
| 文件 | 作用 | 权限 |
|---|---|---|
/usr/local/openresty/nginx/secrets/deepseek.conf | 用 map 把真实 Key 定义成 $deepseek_api_key 变量 | chmod 600,root 可读 |
/usr/local/openresty/nginx/secrets/tokens.conf | 内部令牌 → 员工标识的映射($client_name) | chmod 600 |
/usr/local/openresty/nginx/ssl/proxy.crt / proxy.key | 员工 ↔ 代理之间的 TLS 证书 | 私钥 600 |
/usr/local/openresty/nginx/conf/nginx.conf | 主配置:白名单、限流、日志、access_by_lua 注入逻辑 | — |
网关内置的能力清单
- 端点白名单:默认拒绝,仅放行
/chat/completions、/completions、/models、/anthropic/、/beta/、/v1/;敏感的/user/balance(查余额)被自动屏蔽 - 按人限流:
limit_req/limit_conn以$client_name为键 - 审计日志:JSON 格式,记录 client、IP、URI、状态码、请求/响应字节、上游耗时
- 流式支持:
proxy_buffering off,SSE 逐 token 实时返回 - DNS 稳定性:用静态
upstream块在启动时解析一次,规避内网无法访问公共 DNS 的问题 - user_id 注入:
access_by_lua强制覆盖,兼容 OpenAI 与 Anthropic 两种格式
三、操作细节
第 1 步:打通到 DeepSeek 的网络(DNS 坑)
第一次跑起来就遇到 502,且日志里 upstream_time 为空——说明 Nginx 根本没从上游拿到响应,失败在”连接/解析上游”这一步。根因是nginx未配置公共 DNS解析,且内网服务器通过网关解析 api.deepseek.com 超时。
解法有两种,我们选了更稳的第一种:用 resolver 来解析域名,运行期不依赖网关DNS解析:
resolver 223.5.5.5 119.29.29.29 valid=300s ipv6=off;排查时的关键命令:
tail -50 /var/log/nginx/error.log # 看具体报错nslookup api.deepseek.com # 系统 DNS 能否解析curl -v https://api.deepseek.com/models -H "Authorization: Bearer sk-真实key" # 验证出网+TLS第 2 步:安装 OpenResty
确认系统版本后,按官方方式添加 RHEL 9 系的仓库并安装:
sudo dnf install -y wgetwget https://openresty.org/package/rhel/openresty2.reposudo mv openresty2.repo /etc/yum.repos.d/openresty.reposudo dnf check-updatesudo dnf install -y openresty
/usr/local/openresty/bin/openresty -v # 应显示 openresty/1.29.xOpenResty 是独立安装,会与自带 nginx 抢 80 端口,所以要修改 openresty 的默认配置文件,删除监听80端口的 server块
sudo vim /usr/local/openresty/nginx/conf/nginx.confsudo systemctl enable --now openresty几个路径差异要记住:二进制在 /usr/local/openresty/nginx/sbin/nginx,默认配置在 /usr/local/openresty/nginx/conf/nginx.conf,服务名是 openresty。由于配置里 secrets 和 ssl 都用绝对路径,原有目录可直接沿用。
第 3 步:核心配置与 user_id 注入
下面是主配置的关键部分。鉴权、端点校验、user_id 注入都在 access_by_lua_block 里完成,转发前用真实 Key 覆盖 Authorization。
# 用 map 把真实 key 定义成一个变量,集中管理、便于轮换map $host $deepseek_api_key { default "sk-your-deepseek-api-key";}# 内部令牌 → 员工/团队标识(标识用于限流与日志归因)# 令牌建议用 `openssl rand -hex 16` 生成,前缀方便辨识map $http_authorization $client_name { default ""; # 不在清单 → 视为非法 "Bearer [token]" "[username]"; #备注}115 collapsed lines
http { include /usr/local/openresty/nginx/secrets/deepseek.conf; # $deepseek_api_key include /usr/local/openresty/nginx/secrets/tokens.conf; # $client_name
map $uri $endpoint_allowed { default 0; ~^/chat/completions$ 1; ~^/completions$ 1; ~^/models$ 1; ~^/anthropic/ 1; ~^/beta/ 1; ~^/v1/ 1; }
limit_req_zone $client_name zone=req_per_user:10m rate=20r/m; limit_conn_zone $client_name zone=conn_per_user:10m;
log_format llm_audit escape=json '{' '"time":"$time_iso8601","client":"$client_name","ip":"$remote_addr",' '"method":"$request_method","uri":"$uri","status":$status,' '"req_bytes":$request_length,"resp_bytes":$body_bytes_sent,' '"upstream_time":"$upstream_response_time"' '}';
upstream deepseek { server api.deepseek.com:443; keepalive 32; }
resolver 223.5.5.5 119.29.29.29 valid=300s ipv6=off;
server { listen 9443; #listen 443 ssl; server_name [所在服务器内网IP];
#ssl_certificate /etc/nginx/ssl/proxy.crt; #ssl_certificate_key /etc/nginx/ssl/proxy.key; #ssl_protocols TLSv1.2 TLSv1.3;
access_log /var/log/nginx/llm_audit.log llm_audit; client_max_body_size 20m; client_body_buffer_size 1m;
location / { limit_req zone=req_per_user burst=20 nodelay; limit_conn conn_per_user 10;
access_by_lua_block { local client = ngx.var.client_name if client == nil or client == "" then ngx.status = 401 ngx.header.content_type = "application/json" ngx.say('{"error":{"message":"invalid internal token"}}') return ngx.exit(401) end if ngx.var.endpoint_allowed ~= "1" then ngx.status = 403 ngx.header.content_type = "application/json" ngx.say('{"error":{"message":"endpoint not allowed"}}') return ngx.exit(403) end
local method = ngx.req.get_method() local ctype = ngx.req.get_headers()["content-type"] or "" if method == "POST" and ctype:find("application/json", 1, true) then ngx.req.read_body() local body = ngx.req.get_body_data() -- 大 body 被写到临时文件时,get_body_data() 返回 nil,需读文件 if not body then local fname = ngx.req.get_body_file() if fname then local fh = io.open(fname, "rb") if fh then body = fh:read("*a"); fh:close() end end end if body and #body > 0 then local cjson = require "cjson.safe" local data = cjson.decode(body) if not data then ngx.status = 400 ngx.header.content_type = "application/json" ngx.say('{"error":{"message":"invalid JSON body"}}') return ngx.exit(400) end -- 强制覆盖为已鉴权身份,客户端传什么都不算数 if ngx.var.uri:find("^/anthropic") then if type(data.metadata) ~= "table" then data.metadata = {} end data.metadata.user_id = client -- Anthropic 格式 else data.user_id = client -- OpenAI 格式 end ngx.req.set_body_data(cjson.encode(data)) end end }
proxy_pass https://deepseek; proxy_http_version 1.1; proxy_ssl_server_name on; proxy_ssl_name api.deepseek.com; proxy_set_header Host api.deepseek.com; proxy_set_header Authorization "Bearer $deepseek_api_key"; proxy_set_header Connection "";
proxy_buffering off; proxy_cache off; proxy_connect_timeout 10s; proxy_send_timeout 300s; proxy_read_timeout 300s; default_type application/json; } }}这里有三个细节值得强调。
- 一是大请求体兜底:当 body 超过
client_body_buffer_size被写到临时文件时,get_body_data()会返回 nil,必须用get_body_file()读文件,否则像 100KB 这种请求会注入失败。 - 二是lua-cjson 的空数组坑:decode 后无法区分空数组
[]和空对象{},re-encode 时空表默认输出成{};LLM 请求体里几乎不会出现tools:[]这类空数组,影响很小,但属于已知风险。 - 三是永远不要开
proxy_cache:给 LLM 接口开响应缓存且缓存键不含用户维度,才是真正会导致跨用户泄露的操作。
第 4 步:验证注入是否成功
注入发生在请求体里、随后被转发,肉眼看不到。最可靠的方式是让 Lua 把改完的 body 打到日志(验证完即删):
ngx.req.set_body_data(cjson.encode(data))ngx.log(ngx.ERR, "USERID_INJECT client=", client, " body=", cjson.encode(data)) -- 临时然后用一组测试覆盖三个场景,其中最关键的是”伪造被覆盖”:
# 客户端故意伪造 user_id:"hacker" → 日志里应被改成令牌对应的 alicecurl -s http://[ip]:[port]/chat/completions \ -H "Authorization: Bearer int_alice_xxx" -H "Content-Type: application/json" \ -d '{"model":"deepseek-v4-flash","user_id":"hacker","messages":[{"role":"user","content":"hi"}]}'安全红线:验证时绝不能把
proxy_pass指向任何外部回显服务(httpbin 之类),因为代理会带上真实 Key。要看离开代理前的字节,只能用监听127.0.0.1的本地回显 server。
第 5 步:员工接入(Anthropic 方式)
员工开发一般使用vscode等IDE工具配合claude code扩展,只须在安装claude code插件后,在用户目录根下的 .claude/settins.json 配置文件即可调用DeepSeek API内网中转网关
claude code配置文件:
- Windows:
C:\Users\[username]\.claude\settings.json - Linux:
/home/[username]/.claude/settings.json
{ "env": { "ANTHROPIC_AUTH_TOKEN": "[token]", "ANTHROPIC_BASE_URL": "http://[ip]:[port]/anthropic", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "deepseek-v4-flash", "ANTHROPIC_DEFAULT_OPUS_MODEL": "deepseek-v4-pro[1M]", "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "deepseek-v4-pro", "ANTHROPIC_DEFAULT_SONNET_MODEL": "deepseek-v4-pro[1M]", "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "deepseek-v4-pro", "ANTHROPIC_MODEL": "deepseek-v4-pro" }, "includeCoAuthoredBy": false}员工不需要、也无法设置 user_id——即便在 extra_body 里传了也会被代理覆盖,这正是设计目的。流式只需加 stream=True,工具调用、JSON 模式等参数都在请求体里,代理原样透传。

四、实现效果
上线后,对照最初的五条需求逐一达成:
- 密钥隐藏:真实 Key 只存在于服务器上
chmod 600的文件里。员工抓包只能看到自己的内部令牌;某人离职或令牌泄露,只需删掉tokens.conf里对应一行并reload,即可单独吊销,不影响其他人。 - 滥用受控:限流以
$client_name为键,单个员工的突发流量被挡在自己的桶里,不会消耗他人配额,从而把账号级的并发与余额风险拆解到个人维度。 - 可审计:每笔调用都以 JSON 落日志,可按 client 维度统计用量、做成本分摊,也能快速定位异常调用方。
- 多用户隔离:网关在转发前强制写入
user_id,员工无法省略或伪造。借此拿到 DeepSeek 的 KVCache 隔离(不同员工缓存互不干扰)、内容安全隔离,以及在账号扩容后按 user_id 单独限并发的能力。 - 零侵入接入:员工继续用标准 OpenAI / Anthropic SDK,仅改
base_url与 key,流式、工具调用、JSON 模式全部照常。
更重要的是一个认知层面的结论:对话上下文不会在用户之间串联。API 无状态、Nginx 每请求隔离、keepalive 连接严格一问一答,三层共同保证了这一点;唯一需要主动规避的,是绝不给 LLM 接口开响应缓存。
整套方案的最终形态,是一台内网服务器上的 OpenResty 网关:对内提供一个统一的 HTTPS 入口和一套可吊销的令牌体系,对外用单一真实 Key 与 DeepSeek 通信,中间完成鉴权、限流、审计、身份注入与格式适配。后续若要做按 token 计费的精确额度控制,可在日志侧统计 usage 字段,或把鉴权升级为 auth_request + 后端数据库的动态管理。

五、文章引用
- DeepSeek API 文档首页(调用格式、Base URL、认证):https://api-docs.deepseek.com/
- DeepSeek 限流与隔离(并发上限、
user_id隔离、保活机制):https://api-docs.deepseek.com/quick_start/rate_limit - DeepSeek Anthropic API 指南(
/anthropic格式、metadata.user_id):https://api-docs.deepseek.com/guides/anthropic_api - DeepSeek 错误码(429、4xx 含义):https://api-docs.deepseek.com/quick_start/error_codes
- OpenResty 官方 Linux 软件包安装(RHEL
openresty2.repo):https://openresty.org/en/linux-packages.html - OpenResty lua-nginx-module(
access_by_lua、ngx.req.*API):https://github.com/openresty/lua-nginx-module - lua-cjson(编解码与空表行为):https://github.com/openresty/lua-cjson
说明:本文所述配置中的令牌、域名、Key 均为示例,部署时请替换为实际值;限流阈值(
rate=20r/m、burst、limit_conn)需按团队规模与预算调整。