开头部分,我直接从最真实的场景进入:裸奔的接口如何被打、为什么本地跑通和对外提供服务完全是两码事。然后用“如果你准备把 Qwen3-1.7B 提供给团队或公网使用,那么密钥和权限控制这一课逃不掉”来定调。
让我把篇幅铺开,每个 H2 下尽量写满、写透。注意每段要超过150字。段落自然衔接。 先说一句可能不太中听的话:本地把 Qwen3-1.7B 跑起来、用 vLLM 起一个 OpenAI 兼容接口,这只是万里长征第一步。真正让人头疼的不是模型效果,而是你把--host 0.0.0.0写上去的那一刻,服务就已经暴露在网络上,而绝大多数人根本没有给这个接口做任何保护。我自己见过不止一次——开发者在自己的 VPS 上部署完模型,为了图方便直接用 FastAPI 包了一层接口,端口一开,把地址甩给同事,然后就撒手不管了。没过几天一看日志,全是陌生 IP 的 401/200 扫描请求,上下文窗口被刷得飞起,GPU 跑满,账单感人。
这篇文章就围绕一个主题展开:Qwen3-1.7B 对外提供 API 服务时,怎么把密钥管理和访问权限控制做到位。我会从网络层、应用层、数据层三个维度拆开讲,并给出一套可以直接照抄的实操方案。无论你是个人开发者、小团队还是企业内网部署,这套思路都适用。下文默认你已有一台机器、已能通过某种方式(vLLM、Ollama、llama.cpp 任选)启动 Qwen3-1.7B 的本地接口,本文专注的是如何让这个接口安全地对外服务。
1. 先搞清楚你的服务暴露给了谁
在做任何密钥和权限设计之前,第一件事不是写代码,而是想清楚一个问题:你部署的 Qwen3-1.7B 到底要服务谁?是只在本机回环地址上给自己调试用,还是给整个内网团队用,还是直接暴露在公网上让远程业务调用?
1.1 三种常见的部署形态
我习惯把部署形态分成三类,每一类对应的安全策略完全不同:
- 本机调试模式:接口只监听
127.0.0.1,只有本机能访问,或者通过 SSH 隧道让特定的人访问。这种场景下不配置密钥问题不大,模型本身就在你手里,网络层面已经挡住了外部流量。 - 内网服务模式:接口监听在
0.0.0.0或内网 IP 上,供公司局域网内的多个团队调用。这时候必须加密钥,因为内网不等于安全——公司内部同样存在越权调用、误请求、甚至是同事的电脑中招后变成跳板的风险。 - 公网暴露模式:你需要让外部系统或远程开发者调用这个模型接口,只能把端口映射到公网。这种模式最危险,任何密钥缺失、权限配置不当,都会在几分钟内被全网扫描器识别并利用。
看清楚自己属于哪一类,是后续所有配置的基础。很多人一上来就搜“API Key 怎么加”,但没有意识到:如果只是本机调试,你加不加密钥无所谓;如果是公网暴露,你只加一个密钥远远不够,还需要限流、审计、网络隔离多管齐下。
1.2 两个设计原则:默认拒绝与纵深防御
部署安全的核心原则就两条,简单到甚至不需要解释,但绝大多数人根本没做到。
第一条是默认拒绝。默认拒绝的意思是:除了你明确允许的请求路径和身份之外,其余请求一律拒绝。落地到 Qwen3-1.7B 部署场景,就是默认只有携带合法密钥的请求才能打到模型推理接口,不带密钥的请求直接 401。很多人的习惯是反过来——先开放服务,遇到问题再加限制,这就等于把门开着等小偷进来。严格来说,OpenAI 兼容服务本身不提供密钥校验,需要你自己在前面加一层。这一层就是默认拒绝的落地。
第二条是纵深防御。纵深防御的意思是:安全不能寄托在单点上。即使你的 API Key 校验逻辑写错了、被绕过了,网络层的防火墙、绑定地址的限制、访问日志的审计依然能兜底。一个常见错误是:开发者在应用层写了很复杂的密钥逻辑,但把端口直接暴露在公网,监听了0.0.0.0。一旦应用层代码有逻辑漏洞(比如某个路径忘了加校验),攻击者就能直接触达模型推理接口。如果把监听地址改成127.0.0.1,再用 Nginx 反代对外只放行 443 端口,即使应用层出了 bug,攻击者也够不着真正的服务端口。
把这个观念理顺,再看后面的方案就不会觉得繁琐。下面从密钥本身开始讲。
2. API 密钥的生成、存储与轮换
密钥是整个权限控制系统里唯一的身份凭证。你用什么算法生成密钥、把密钥放在哪里、数据库里存什么、密钥丢了怎么吊销、过期怎么轮换,每个环节都能看出部署者的专业程度。
2.1 密钥生成:别用 uuid 糊弄
我见过不少项目直接用uuid.uuid4().hex当 API Key 用。不是说 uuid 完全不能用——它的随机性是够的,问题在于它没有任何身份语义,也没有办法在生成时控制熵的长度编码形式。更关键的是,很多语言的 uuid 实现格式固定,攻击者一旦拿到几个样本,就能确认你的密钥格式,后续做字典攻击或者针对性的碰撞会容易很多。
比较推荐的做法是用密码学安全的随机数生成器生成高熵字符串。举个具体的例子:
python -c "import secrets; print(secrets.token_urlsafe(32))"这行命令会生成 32 字节随机数据、经过 Base64 URL 编码,长度大约 43 个字符。这串字符里包含了 256 位熵,暴力枚举在现实中完全不可行。如果你愿意,还可以在这里加上一个版本前缀,比如qwen3_live_开头,这样日志里一眼就能看出是哪个环境的密钥。我自己的习惯是生成成sk-qwen3-开头的一段随机串,既方便日志过滤,又不影响安全性。
生成密钥时还有几个细节要注意。第一,不要自己写随机数生成器,random模块的伪随机数不可用于安全场景,一定用secrets或os.urandom。第二,不要把密钥写死在代码仓库里,这是底线。第三,一个用户或一个服务原则上只对应一个密钥,不要做“一把钥匙开所有门”的事。
2.2 密钥存储:环境变量、配置中心还是 KMS
密钥生成之后放在哪里,这里也有方法上的区分。放在代码里肯定不行,放在.env文件里算及格,但还有更稳的方案。
| 存储方式 | 适用规模 | 优点 | 缺点 |
|---|---|---|---|
| 环境变量 | 个人/小团队 | 简单直接,不落盘 | 进程内可见,shell 历史可能泄漏 |
| .env 文件 | 小团队 | 集中管理,容易复制 | 文件权限要小心,容易被误提交 |
| 配置中心 | 中型团队 | 支持动态变更,集中下发 | 需要额外维护一套基础设施 |
| KMS/密钥管理服务 | 企业级 | 审计完善,自动轮换,加密存储 | 接入成本高,依赖外部服务 |
对于大多数个人部署和小团队场景,环境变量加.env文件已经够了,但要注意给文件设置权限。比如在 Linux 下,chmod 600 .env是一个好习惯,避免其他系统用户把这个文件读走。有些人喜欢把.env提交进 Git 仓库,这是我的大忌——历史上无数数据泄露事故都是从仓库里翻出密钥开始的。
如果你在公司环境里批量部署多个模型服务,我建议直接上配置中心或者 KMS。原因很简单:密钥数量多了之后,人工管理必然出错。配置中心可以把密钥集中管理,并且支持按环境(测试、生产)隔离。企业级场景下,KMS 还能提供自动轮换、加密存储和访问审计,安全等级完全不一样。
2.3 数据库只存哈希
很多人在做密钥鉴权时会犯一个经典错误:把明文密钥直接存在数据库里。这会导致一个连锁风险——数据库一旦被拖库,攻击者手里就是所有有效密钥。正确的做法是只存密钥的哈希值。校验的时候,把客户端传过来的密钥做同样的哈希运算,然后比对哈希值是否一致。
具体实现很简单:
import hashlib def hash_api_key(api_key: str) -> str: return hashlib.sha256(api_key.encode("utf-8")).hexdigest()客户端请求时,服务端对传过来的 key 做 SHA-256 哈希,然后和库里存的哈希比对。哪怕数据库泄露了,攻击者拿到的也只是哈希值,还原不出明文密钥(前提是你的密钥本身熵足够高,彩虹表起不了作用)。当然有人会问:为什么不用bcrypt这种加盐慢哈希?API Key 不是用户口令,它本身就是高熵随机字符串,不存在低频密码被爆破的问题,加盐和慢哈希收益不大,反而增加校验延迟。用 SHA-256 完全够用,重点在于密钥生成时熵要足。
还有一点容易被忽略:比对时不要用==直接比较字符串,推荐使用恒定时间比较,防止时序侧信道攻击。Python 里可以用hmac.compare_digest:
import hmac if hmac.compare_digest(stored_hash, computed_hash): # 校验通过这行代码在极端情况下能帮你挡掉一种很偏门的攻击方式,虽然实际中很少有人对着 API 服务做时序攻击,但养成好习惯不亏。
2.4 轮换与吊销要策略化
密钥不是设置了就一劳永逸的。密钥有生命周期,到期了要换,泄露了要吊销,业务下线了要回收。很多团队根本没有轮换机制,一个密钥用了三年,离职员工还拿着旧 key 在调接口,这就是定时炸弹。
轮换策略上,我常用的做法是给密钥加版本号和有效期字段。数据库表结构大致如下:
| 字段 | 说明 |
|---|---|
| key_id | 主键,对外凭证标识 |
| key_hash | 密钥哈希,用于校验 |
| owner | 归属用户/服务 |
| role | 权限角色(admin/dev/readonly) |
| created_at | 创建时间 |
| expires_at | 过期时间(可空表示永不过期) |
| revoked | 是否被吊销 |
轮换时,不要立刻删除旧密钥,而是给它设置一个宽限期。比如新密钥生效后,旧密钥保留 48 小时,期间两个密钥都能通过校验,48 小时后再正式吊销。这样做的原因是:很多客户端会把密钥写死在配置里,不是你后端一改就能立刻全部更新的。没有宽限期,你早上轮换密钥,饭点就能收到一堆“401 报错,服务不可用”的投诉。
3. 访问权限控制的分层防线
密钥解决了“你是谁”的问题,权限控制解决的是“你能干什么”的问题。光有密钥没有权限控制,服务依然是一座不设防但需要查证件的大楼。对于模型推理服务,权限控制的粒度可以从网络层一直延伸到业务层。
3.1 第一道防线:网络层
很多人拿到一台云服务器,第一件事就是把防火墙规则当成摆设,觉得“端口开着没事,加了密钥就行”。这是我见过的最危险的想法。密钥只是应用层的一把锁,而网络层才是房子的外墙。
网络层控制的第一原则是:模型服务端口绝不监听在0.0.0.0。在启动 vLLM 时,设置--host 127.0.0.1;如果模型和 Nginx 反代在同一台机器上,这样设置就够了。后续请求统一从 Nginx 的 443 端口进来,Nginx 再把流量转发给本机的 8000 端口。云服务器上,安全组规则只放行 443 端口(或者只对特定 IP 放行),8000 端口不对公网开放。
如果你的 Qwen3-1.7B 部署在带安全组的云环境里,安全组规则的优先级最高——它是在云平台底层隔离的,不需要和应用层配合。举个例子,你可以只允许办公网 IP 段访问 443 端口,其他 IP 一律拒绝。这就意味着即使你的密钥泄露了,攻击者不在允许的 IP 地址段内,同样无法发起请求。这招非常管用,但很多人因为配置麻烦而不愿意做。
再补充一点,对于内网服务,网络隔离同样适用。比如可以让模型服务的端口只在生产网段之间互通,测试网段和开发网段不允许直接触达模型端口。网络层的控制粒度越细,应用层的安全压力就越小。
3.2 第二道防线:网关鉴权
光靠网络层隔离还不够,因为总有一些调用方是跨网络的。网关层鉴权的核心思路是:所有请求先经过一个统一的入口校验,只有携带合法密钥的请求才能路由到后面的模型推理服务。这个“网关”可以是独立的 API 网关组件,也可以是你自己写的 FastAPI 中间件。对于大多数 Qwen3-1.7B 部署场景,写一个轻量级的鉴权中间件就够了,没必要为了一个小模型服务引入完整的微服务网关体系。
网关层鉴权实现的要点有两个。第一,你要拦截所有需要保护的路径——/v1/chat/completions、/v1/embeddings之类的模型接口一个都不放过。第二,为了兼容 OpenAI SDK 的调用习惯,推荐使用Authorization: Bearer <your_api_key>从请求头里读取密钥。这样不管是 curl、OpenAI 官方 SDK、还是各种第三方客户端,都能无缝对接。
还有一个小细节:如果 Nginx 层做了 TLS 终结,应用层拿到的是解密后的 HTTP 请求。这时候在应用层做鉴权完全没问题。但如果你把鉴权放在 Nginx 层(比如用auth_request模块),一定要记得鉴权服务本身不能被外部直接访问,否则别人绕过 Nginx 也能验证密钥。
3.3 第三道防线:用户与角色
密钥鉴权通过之后,还要考虑权限角色的差异。不是每个拿到密钥的人都应该有完全相同的权限。举个例子,业务部门的调用者只需要chat/completions接口,而管理员还需要看到调用统计、管理密钥、查看日志。这种差异化控制在一些人看来有点过度设计——一个小模型服务,搞什么 RBAC?但当你把接口开放给外部合作伙伴时,就会发现角色控制是必须的。
我在实际项目中用过的做法是把角色写在密钥对应的记录里。常见的角色可以分成三类:
admin:可以调用推理接口,也可以管理密钥、查看审计日志。dev:可以调用推理接口,查看调用统计。readonly:只读的调用权限,适合测试和只做推理的业务方。
在代码里,每个角色对应一组允许访问的路由或操作。比如管理员密钥可以访问/admin/keys,普通开发密钥访问这个路径就返回 403。这样一套设计下来,权限控制的粒度从“能不能用”提升到了“能用哪些能力”。
再加上按用户维度的配额控制:每个用户每天最多调用多少次,每个密钥的并发请求上限是多少。配额和角色配合使用,才能在多人共享服务的场景下不互相干扰。关于限流后面实操部分我会给代码。
4. 实操:给 Qwen3-1.7B 加一层完整鉴权
前面讲到思路,这一节直接给配置和代码。我们以标准的 FastAPI 作为 API 层、vLLM 作为推理后端为例。整体架构如下:客户端 -> Nginx(TLS + 简单限流) -> FastAPI(鉴权 + 配额) -> vLLM Qwen3-1.7B。
4.1 在 FastAPI 中实现 API Key 中间件
第一步,在 FastAPI 中写一个依赖函数,用于校验请求头里的 Bearer Token。不把校验逻辑散落在每个路由里面,而是写成一个通用依赖,挂到需要保护的路由上。代码如下:
from fastapi import Depends, HTTPException, Security from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer import hashlib import hmac # 假设 valid_keys 是从数据库或内存加载的 {key_hash: role} valid_keys = { "a1b2c3...哈希值...": "admin", "d4e5f6...哈希值...": "dev", } bearer_scheme = HTTPBearer(auto_error=False) def hash_key(key: str) -> str: return hashlib.sha256(key.encode("utf-8")).hexdigest() def verify_api_key( credentials: HTTPAuthorizationCredentials = Security(bearer_scheme), ) -> str: if credentials is None: raise HTTPException(status_code=401, detail="Missing API Key") key_hash = hash_key(credentials.credentials) for stored_hash, role in valid_keys.items(): if hmac.compare_digest(stored_hash, key_hash): return role raise HTTPException(status_code=401, detail="Invalid API Key")然后在你需要保护的路由上挂上这个依赖:
from fastapi import FastAPI, Depends app = FastAPI() @app.post("/v1/chat/completions") def chat_completions( role: str = Depends(verify_api_key), ): # 将请求转发给 vLLM,或者直接调用本地模型 return {"role": role, "message": "ok"}实际项目中,密钥哈希通常会存在数据库或 Redis 里,这里用字典演示是为了让逻辑更清晰。注意valid_keys的 key 是哈希值,不是明文——数据库泄露也不怕。
这里要专门提醒一个容易犯的错:不要把 API Key 校验逻辑写在路由函数内部,而是用Depends这样的依赖注入机制。原因很简单,依赖注入能保证所有入口都经过同一套校验逻辑;写在路由内部,一旦有个新同事开发了新接口忘了复制粘贴校验代码,就等于开了个漏洞。
4.2 限流与配额
鉴权做完只是“门禁”部分,还需要控制“流量”。没有限流,哪怕密钥管理做得再好,一个内部人员拿了 key 开个死循环,也能把你的 Qwen3-1.7B 打满。模型推理是资源密集型操作,一次请求占用的显存和算力远超普通 Web 请求。
一个简单的按密钥限流,可以用 Redis 的滑动窗口计数器来做。伪代码如下:
import time import redis r = redis.Redis(host="localhost", port=6379, db=0) def check_rate_limit(key_hash: str, limit: int, window: int) -> bool: now = int(time.time()) current_window = now // window key = f"rate_limit:{key_hash}:{current_window}" count = r.incr(key) if count == 1: r.expire(key, window + 1) return count <= limit这个函数会在 Redis 里维护一个当前时间窗口内的计数器,超过阈值就返回 False。你可以在依赖里加上这个检查,超限就返回 HTTP 429。对模型服务来说,通常限制的不是“每秒多少次请求”,而是“每分钟多少次”或者“每天多少次”。因为大模型单次推理耗时较长,每秒几十次的调用已经能说明问题了。
配额方面,我建议把限流参数做成每个密钥可配置的。比如某业务方买的是 100 次/分钟的调用额度,另一个测试账号只需要 10 次/分钟。把这个信息存在密钥记录里,而不是全局写死一个数值。开发时为了方便可以先用slowapi这样的 Python 库做单机限流,生产环境再用 Redis 做分布式限流。
4.3 Nginx TLS 终结与防扫描配置
App 应用层的鉴权逻辑再完善,也得面对裸奔的公网端口问题。Nginx 在这里干两件事:一是把 443 端口进来的 HTTPS 请求转发给 FastAPI,二是承担一部分简单的 IP 级限制。
Nginx 配置如下:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s; server { listen 443 ssl; server_name your.domain.com; ssl_certificate /etc/nginx/ssl/server.crt; ssl_certificate_key /etc/nginx/ssl/server.key; location /v1/ { limit_req zone=api_limit burst=10 nodelay; proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里的关键是limit_req_zone的 IP 限流。就算有人拿到了你的密钥但不在 IP 白名单内,他也只能在极低的频率下尝试。加上安全组本身只放行特定 IP,攻击者拿密钥也废了。
TLS 终结放在 Nginx 而不是 FastAPI 里,好处是你不需要在 Python 代码里管理证书、不需要处理 HTTPS 协议细节。SSL 证书如果暂时没有正式域名,可以用自签证书顶一阵,但客户端那边记得关闭证书校验,或者导入你的自签 CA。正式对外服务还是建议用正规证书,Let's Encrypt 免费且好用。
4.4 客户端调用方式
部署完成后,客户端调用就是标准的 OpenAI 兼容格式。用 curl 测试:
curl https://your.domain.com/v1/chat/completions \ -H "Authorization: Bearer sk-qwen3-你的密钥" \ -H "Content-Type: application/json" \ -d '{ "model": "Qwen3-1.7B", "messages": [{"role": "user", "content": "你好"}] }'用官方 OpenAI SDK 的话,只需要把api_key填成你分配的密钥,把base_url指到你的 Nginx 地址:
from openai import OpenAI client = OpenAI( api_key="sk-qwen3-你的密钥", base_url="https://your.domain.com/v1" ) response = client.chat.completions.create( model="Qwen3-1.7B", messages=[{"role": "user", "content": "你好"}] ) print(response.choices[0].message.content)注意,使用第三方 SDK 时,如果你在系统环境变量里设置了OPENAI_API_KEY,SDK 会优先使用它,容易导致发到你自己服务器的请求带上了错误的密钥。我一般建议调用方在代码里显式传参,或者用.env区分不同服务的密钥。
5. 常见问题与排查实录
理论和代码都讲完了,这一节是真正腥风血雨的实战经验。我把几个高频问题逐一列出来,这些问题我都在生产环境里踩过或者帮别人排查过。
5.1 密钥泄露后的急诊流程
密钥泄露这件事,一旦发生,第一个动作不是查日志,不是猜测泄露原因,而是立即吊销这个密钥。我见过的比较多的情况有两种:一是开发者把密钥贴到了公开仓库或聊天群里;二是客户端打包时没有做好字符串混淆,密钥静态写在包里被反编译提取。
急诊流程应该按顺序执行:
- 立即吊销对应密钥,禁止继续通过校验。
- 检查最近的访问日志,找出使用该密钥的最近几十条请求来源 IP 和调用内容。
- 判定有没有恶意调用——如果只是演示代码发出去的 key,调用量通常很少;如果出现大量境外 IP 的并发调用,说明已经被刷了。
- 按需封禁来源 IP 段,收紧安全组规则。
- 生成新的密钥给合法调用方,并设置短期宽限期让客户端平滑切换。
这个流程里最容易出错的地方是第 2 步——如果日志里没有记录密钥 ID 或调用 IP,你根本追溯不了。所以平时就一定要把日志的审计字段做全。我自己的日志里至少保留key_id、client_ip、request_path、timestamp这几个字段,缺一不可。
5.2 访问日志里全是明文密钥
这个坑很有迷惑性。很多人配置了 Nginx 的默认日志格式,然后发现access.log里把Authorization: Bearer sk-qwen3-xxxxx完整记录下来了。日志一旦被同步到日志平台、被备份到对象存储,就等于密钥又泄露了一遍。一些日志采集工具还会对这些日志做全文索引,泄露范围更大。
解决办法有两种。第一种是改 Nginx 的log_format,不要记录$http_authorization头。第二种是就算要记录请求头,也得做脱敏——只保留Bearer前缀和密钥的前四位,其他用星号替代。FastAPI 的日志同理,不要用logger.info(request.headers)这种粗暴写法,里面带什么就记什么。
5.3 健康检查被限流误杀
这个问题的代码长这样:“为什么我的监控系统时不时收到 429 告警,但我根本没怎么调用模型?”
排查到最后会发现,监控系统有自己的健康检查机制,它每隔几十秒就会去探一个健康检查路径,探得平繁之后,把 Nginx 层或者应用层限流额度吃光了,导致正常调用被误杀。解决思路是:在限流规则里把健康检查路径排除掉。比如/healthz这个路径单独放行,不参与密钥校验和限流。如果你用的是 Nginx 的limit_req,记得不要把它加到健康检查路径的 location 上。
但这个排查过程很费时间,因为 429 的返回体里如果只有Too Many Requests,没有标明是哪个限流维度触发,你根本不知道是 IP 限流还是密钥限流。我的建议是在限流拒绝时返回一个包含原因的 JSON 错误体,比如{"code": "rate_limited_key", "message": "key weekly quota exceeded"},这样排错能快很多。
5.4 提示注入与鉴权的关系
这里要提醒一句:API 密钥管理和访问权限控制解决的是“谁能访问你的模型”这个问题,但它不会解决“用户发送的内容是否恶意”这个问题。提示注入攻击、恶意 prompt 绕过系统约束、批量生成违规内容——这些都属于模型内容安全范畴,需要独立应对。
比如你给 Qwen3-1.7B 配了一个系统提示词让它扮演客服机器人,但有人精心构造了一段 prompt 把系统提示覆盖掉。密钥不会拦这个,权限也不会。鉴权只能保证来的人是有身份的,不能保证来的人不干坏事。如果模型服务会对公网开放,我强烈建议在应用层加一个输入侧的内容过滤,或者接入内容安全服务,对用户的输入和模型输出都做一道检测。不要指望一个 API Key 能解决所有安全问题。
下面用表格小结一下常见问题的速查:
| 问题 | 现象 | 可能原因 | 处理方式 |
|---|---|---|---|
| 429 频繁 | 正常请求被限制 | 限流阈值太低或健康检查抢占额度 | 排除健康检查路径,按 key 细化额度 |
| 401 突然变多 | 客户端全部接入失败 | 密钥已轮换但客户端未更新 | 轮换时设置宽限期,保留旧 key |
| 日志泄露密钥 | 日志平台里能看到明文 Key | 日志格式记录了 Authorization 头 | 改 log_format 或脱敏输出 |
| 陌生 IP 调用 | 模型响应异常、token 消耗飙升 | 密钥泄露或未校验密钥 | 吊销密钥、收紧安全组、查日志定位 |
还有一条经验:密钥校验和网络层防护不是二选一,而是必须叠加。密钥只是让你“有凭证”,网络安全组、监听地址、限流规则共同决定“凭证背后的人能不能到达你的服务器”。我自己在所有需要公网访问的模型服务上都做了“安全组白名单 + Nginx TLS + FastAPI 密钥校验”三层结构。三层都过,请求才能触达 vLLM。
最后再分享一个细节:给 Qwen3-1.7B 这类开源模型部署服务时,很多人的心态是“这是测试项目、暂时用用、以后再说”,于是跳过所有安全配置。但“以后再说”通常变成“再也没有以后”。我见过不止一个团队因为一次接口裸奔被人刷走大量 token 而被迫提前结束实验项目。安全配置的确会多花一两个小时,但比起事后收拾烂摊子,这笔时间投入划算太多了。