
如果你没用过大模型接口的账单你可能无法理解为什么鉴权这件事在大模型场景下是第一优先级而不是一个普通的“安全加分项”。我见过太多团队辛辛苦苦把一个对话机器人接进了业务系统上线第二天账单上多出一排从未见过的调用记录——几千个请求来自同一个IP段每次输入都是几K的prompt模型在帮别人生成文案、分析数据。这就是典型的“裸奔接口被扫到”。大模型接口和普通REST接口最大的区别在于普通接口被刷最多是流量成本和数据库压力大模型接口被刷每一次调用都在烧真金白银的token费用而且输入输出越长烧得越快。一个没有鉴权的/completions接口相当于把钱包挂在公网上谁都能来取钱。这也是为什么阿里、腾讯这类大厂在2026年的面试题里反复问这个问题大模型接口为什么必须先做鉴权面试官其实不是在考你“要不要鉴权”而是在看你能不能把这件事讲出层次——成本、安全、合规、架构你能不能建立起一套完整的防守体系。这篇文章我就从这几个维度拆开讲穿插一些我自己在实际项目里踩过的坑和最终落地的方案。1. 把“必须”讲透一次裸奔接口的真实代价1.1 成本放大效应鉴权漏洞等于给攻击者开了无限额度大模型接口的计费逻辑和传统接口完全不一样。传统接口无论怎么刷成本天花板基本是带宽和数据库连接大模型接口的每一次请求都需要消耗GPU推理资源按照输入输出token数计费。这意味着攻击者不需要拿到什么敏感数据只需要不断向你的接口发送prompt你的账户余额就会肉眼可见地消失。我做一个直观的算术题。假设你接的是某个商业大模型API单次对话平均消耗1500个token折合人民币大约3毛钱。攻击者用100并发持续打你一个小时就是36万次请求账单上多出10万块钱级别消耗。如果这个接口还支持长文本输入攻击者故意塞几万字的长文档单次请求成本直接翻几十倍那这个数字就更吓人了。这就是大模型接口必须做鉴权的第一层逻辑鉴权在这里不只是安全控制更是成本控制的第一道闸门。没有鉴权你的模型服务就成了公共资源池任何人都能白嫖你的算力而账单由你出。1.2 数据泄露与合规风险模型是唯一会“记住”业务的组件成本只是最直观的损失更隐蔽的是数据安全风险。大模型接口通常带有上下文管理和会话记忆能力很多系统会把用户的历史对话、业务数据作为上下文传给模型。如果接口没有鉴权攻击者可以通过构造请求读取其他用户的会话记录甚至通过提示词注入绕过系统预设的指令套出系统提示词里隐藏的业务逻辑和数据。我原来在做客服机器人时就遇到过一次内部安全演练因为没有对会话ID做归属校验一个普通用户只要改一下请求里的sessionId就能续接另一个用户的对话框前几轮聊了什么一目了然。这还只是业务数据如果你们的prompt里嵌入了数据库结构、内部知识库信息被套走的后果更严重。从合规角度讲很多模型服务商的使用条款本身都强制要求你做好身份认证防止滥用。如果因为接口裸奔导致模型被用于生成违规内容责任方就是调用方。这个层面的风险已经不是账单问题而是影响业务存续的问题。1.3 面试官视角这道题到底在考察什么能力把这题放到面试场景里看面试官问“为什么必须先做鉴权”通常不是要一个“防止别人盗刷”的简单答案。我后来跟一些大厂的朋友聊过他们比较认可的拆解方式是第一层安全常识知不知道公开的模型接口等于公开的提款机。第二层架构思维能不能主动设计出身份识别、授权控制、配额管理、审计追踪这条链路而不是只挂在某个过滤器里简单拼一个token。第三层成本意识有没有从计费模型出发理解到鉴权在AI场景里和成本控制强绑定。第四层生产落地经验能不能说出真实项目里鉴权绕过的攻击路线和防御方案而不是背概念。所以下文的展开我会从设计逻辑到具体实现再到攻击绕过和面试追问把每一层都走一遍。2. 看清鉴权全链路从“你是谁”到“你能用多少”2.1 先分清两个身份应用身份和用户身份设计大模型接口鉴权的第一步不是急着写代码而是想清楚这个接口到底有谁会来调。通常一个成熟的大模型应用接口调用方分两类一类是机器对机器的内部服务调用比如你的推荐系统要调用大模型生成摘要另一类是端上用户触发的请求比如用户在网页里点了一下“AI总结”。这两类请求对应的鉴权主体是不一样的。服务间调用需要识别应用身份核心凭证是AppKey/API Key代表“哪个应用在调”。端上用户请求需要识别用户身份核心凭证是用户Token代表“哪个用户在调”同时你可能还要通过应用身份确认请求来自你自己的后端而不是第三方恶意脚本。我见过很多失败的设计就是把这两个身份混在一起。有的团队把API Key直接发给前端让浏览器持有它来调大模型接口结果就是API Key被扒出来任何人都能以你的应用身份调用服务。正确的方式是用户Token用于标识用户服务端再用自己的API Key去调用大模型前端永远接触不到那个真正计费的Key。2.2 API Key的签发、下发与轮换策略API Key的生命周期管理是鉴权设计的重头戏。分享一套我在生产环境中实践过、相对完整的方案创建Key时至少要保存这几个字段Key的明文只在创建那一刻返回给申请方数据库里存哈希值Key的权限范围比如可调用的模型列表、最大并发数、QPS上限有效期限按业务需要设30天、90天或自定义。下发渠道上我建议遵守一条铁律服务端到服务端的Key走环境变量或配置中心绝对不能打进前端代码、小程序代码或移动端包里。如果业务确实需要端上直连模型服务也要用短期Token换取服务端签名的方式让端上拿到的凭证失效期只有几分钟而不是永久有效的Key。轮换策略上每90天强制轮换一次属于比较常见的节奏。更稳妥的做法是同时保留新旧两个Key给调用方一个迁移窗口避免一把Key过期导致全链路中断。之前我们做灰度迁移时就是因为没有做双Key并行期某个定时任务在切换点突然开始报401排查了半天才意识到是Key轮换没有做过渡。2.3 鉴权、授权、配额三者的边界很多人在聊鉴权时会把鉴权和授权、配额混在一起面试时一展开就露馅。这其实是三个不同层面的东西鉴权Authentication解决的是“你是谁”的问题通过API Key、用户Token确认调用方身份。授权Authorization解决的是“你能做什么”的问题比如某个Key只能调用文本模型不能调用图像模型只能读取不能写入。配额Quota解决的是“你能用多少”的问题包括每分钟请求数、每天Token消耗上限、余额不足时直接拒绝。大模型接口里配额这块尤其重要。鉴权通过并不代表可以无限调用一个合法的API Key如果被人拿到或者业务代码出现死循环照样能把预算打爆。所以在鉴权链路之后必须再挂一道配额校验。我的经验是配额校验要分两级一是接口入口处的粗粒度限流按Key维度限QPS二是模型调用前的细粒度余额校验比如实时扣减前的配额预占。只有这两级都过了才真正把请求转发给模型服务。3. 从Spring Boot到SSE、Netty WebSocket的落地鉴权实现3.1 普通HTTP接口拦截器 双重校验 防重放对于普通的HTTP接口比如典型的POST /api/v1/chat/completions最基础的做法是用Spring Boot HandlerInterceptor统一拦截校验Header里的API Key。拦截器本身逻辑不复杂但有几个细节值得注意。先看一段我平时常用的拦截器骨架Component public class ApiKeyInterceptor implements HandlerInterceptor { private final ApiKeyStore apiKeyStore; public ApiKeyInterceptor(ApiKeyStore apiKeyStore) { this.apiKeyStore apiKeyStore; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String apiKey request.getHeader(X-API-Key); if (apiKey null || apiKey.isBlank()) { writeError(response, 401, missing api key); return false; } StoredApiKey key apiKeyStore.verify(apiKey); if (key null || key.isDisabled() || key.isExpired()) { writeError(response, 401, invalid api key); return false; } request.setAttribute(apiKeyContext, key); return true; } }这段代码看起来简单但要注意下面这几点第一校验不能只判断Key“非空”必须验证Key是否有效、是否被禁用、是否过期。我见过真实系统因为只写了if (apiKey null)导致攻击者传一个Authorization: Bearer这种空头也会被当成有效请求放进去。第二verify方法里不要每次打数据库。高频接口场景下API Key校验是热点路径至少要加一层本地缓存或Redis缓存不然高峰期数据库连接会被拖死。第三单靠API Key还不够因为API Key是静态的抓包后可以反复重放。所以更安全的做法是叠加一个签名机制客户端用持有的Secret对请求参数和时间戳做HMAC签名服务端校验签名有效性和时间窗口。签名过滤器的核心逻辑大致是这样Component public class SignatureFilter extends OncePerRequestFilter { private static final long VALID_WINDOW_SECONDS 60L; private final String hmacSecret; private final StringRedisTemplate redis; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String timestamp request.getHeader(X-Timestamp); String nonce request.getHeader(X-Nonce); String signature request.getHeader(X-Signature); if (timestamp null || nonce null || signature null) { writeError(response, 401, missing signature params); return; } long ts Long.parseLong(timestamp); if (Math.abs(System.currentTimeMillis() / 1000 - ts) VALID_WINDOW_SECONDS) { writeError(response, 401, timestamp expired); return; } if (Boolean.TRUE.equals(redis.hasKey(nonce: nonce))) { writeError(response, 401, replay attack); return; } String body streamToString(request.getInputStream()); String serverSign hmacSha256(hmacSecret, timestamp \n nonce \n body); if (!MessageDigest.isEqual( serverSign.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8))) { writeError(response, 401, signature mismatch); return; } redis.opsForValue().set(nonce: nonce, 1, Duration.ofSeconds(VALID_WINDOW_SECONDS * 2)); chain.doFilter(request, response); } }签名机制背后的思路是时间戳防止过期请求nonce防止同一请求被重复提交签名保证请求体没有被篡改。这里有个细节nonce必须持久化到Redis等共享存储里并设置过期时间如果只放在本地内存里集群部署时不同节点各记各的重放攻击依然拦不住。3.2 SSE长连接场景鉴权时机在建立连接的那一瞬间也要考虑连接生命周期大模型接口里SSEServer-Sent Events是非常常见的一种形态因为大模型生成文本是流式输出需要边生成边推送。SSE的鉴权有个天然矛盾浏览器原生的EventSource API不支持自定义Header很多团队图省事就把token挂在URL Query参数上比如/chat-stream?tokenxxx。这个做法的问题非常明显URL参数会出现在Nginx access log、浏览器历史记录、各种代理日志里token等于明文泄露。而且SSE连接一旦建立往往会持续几十秒甚至几分钟如果走的是短期token连接还没结束token就过期了这时候如果把连接直接断掉用户的体验就毁了。我实践的方案是这样如果是浏览器场景前端不能自定义Header就改成先通过普通HTTP接口换取一个一次性connectToken这个token只允许用于建立SSE连接有效期设30秒到60秒然后SSE请求带着?connectTokenxxx建立连接。服务端校验connectToken通过后建立连接连接建立后即使connectToken过期也不影响当前连接。更稳妥的做法是服务端在连接期间周期性校验用户主体的有效性。比如用Netty的Channel或者Servlet的AsyncContext保持连接时可以维护一个连接上下文定期检查用户token在黑名单里是否被拉黑。一旦用户被踢下线或权限被收回服务端主动断开对应的SSE流。这个细节在大模型场景很重要流式输出时间很长如果只校验连接建立那一刻用户中途被吊销了权限模型还会继续往这个连接里推数据。3.3 Netty WebSocket握手期鉴权只是开始连接生命周期管理才是重点Netty WebSocket做鉴权和SSE有类似的地方但复杂度更高。WebSocket的连接同样会持续很久而且它是全双工的客户端和服务端可以双向持续发消息所以鉴权需要分两个阶段。第一个阶段是握手期。HandshakeInterceptor里从请求头或Query参数里取出token校验通过才让握手成功。这里最常见的错误是只校验token存在不校验有效性或者把token放在Sec-WebSocket-Protocol里用。Sec-WebSocket-Protocol虽然能自定义子协议但它不是为安全设计的很多网关代理会改写这个头导致鉴权信息丢失。第二个阶段是连接生命周期管理。握手成功只是开始连接建立后还要做三件事心检测。服务端定时发送Ping帧如果客户端连续几次没有Pong说明连接已经失效主动关闭。token定期复检。不要假设一个连接永远有效定期从连接上下文里取用户身份检查用户是否仍然有效如果用户在别的设备上改了密码或被管理员封禁这条连接要能及时感知。连接级权限控制。WebSocket连接建立后客户端可以发送不同类型的消息比如“发起对话”“获取历史记录”“停止生成”。这些消息不能共用同一个握手期鉴权结果每条业务消息最好还要做一次操作级授权至少校验一下这个用户是否有权执行该操作。我之前用Netty封装过一个简单的鉴权骨架大致流程是ServerBootstrap bootstrap new ServerBootstrap() .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new HttpServerCodec()); ch.pipeline().addLast(new HttpObjectAggregator(65536)); ch.pipeline().addLast(new WebSocketServerProtocolHandler(/ws/llm)); ch.pipeline().addLast(new AuthHandshakeHandler()); // 握手期鉴权 ch.pipeline().addLast(new LlmMessageHandler()); // 业务消息处理连接生命周期校验 } });AuthHandshakeHandler里完成token校验后把用户身份写入Channel的attr中LlmMessageHandler处理每条消息时先从attr里拿用户身份再叠加操作级权限校验。这套机制比握手校验一把梭要严谨得多也是面试时能拿出来讲的亮点。4. 鉴权绕过的真实攻击面这些坑面试官一追一个准4.1 客户端拿不到完整签名不等于安全很多人会有一个错觉我的接口是给自家前端用的别人拿不到源码所以不需要太复杂的鉴权。这个想法在大模型场景下很危险。且不说前端代码可以反编译、小程序包可以解包、H5请求可以用Charles直接抓包单说“伪前端”这种攻击方式攻击者不看你的前端代码而是直接模仿你的前端发请求。如果接口的鉴权逻辑只是前端隐藏了一个API Key攻击者抓包抓到了就能直接重放请求。这也是为什么静态API Key只能作为低等级凭证更可靠的方案是端上通过用户登录态换取动态签名每次签名都跟时间戳、随机数、业务参数绑定。4.2 绕过网关直连后端安全边界塌陷的经典案例还有一个很容易被忽视的路径就是“绕过入口直连后端”。很多团队的鉴权只做在API网关上后端服务为了调试方便开了非安全端口、内网端口或者没有做网络策略隔离。攻击者只要扫描到后端服务的真实IP和端口就能绕过网关直接打到模型服务的原始接口。我之前遇到过一次安全测试就是这么打进去的。网关层鉴权做得滴水不漏但某个模型服务Pod因为要接内部监控额外暴露了4379端口并且没有鉴权。测试人员扫到之后直接往这个端口发prompt照样出结果。从那之后我们给所有模型服务加了严格的网络层ACL只允许从网关网段访问服务本身监听的端口全部改成只在集群内部可达。这背后是一条通用的安全原则鉴权不能只靠某一层网络边界、接入层、业务层都要有纵深防御。面试官很喜欢顺着这个话题追问“如果网关被绕过了怎么办”所以你要提前准备这一层的答案。4.3 从“非空校验”到“签名可预测”一张表看懂常见绕过姿势下面把我在实际项目里遇到过或者测试过的绕过姿势做一个汇总面试时能复述出根因和防御手段会非常有说服力。绕过方式根因防御手段直接访问后端服务端口安全边界只存在于网关后端无网络隔离网络ACL、防火墙规则仅允许网关网段访问伪造X-Forwarded-For为内网IP业务层信任客户端可控的请求头信任网关卡重写的Header入口处清除客户端原始头路径大小写、双斜杠、URL编码变体网关路由与后端路由解析不一致网关转发前统一规范化URI路径Authorization头传空字符串或“Bearer null”代码只做了“非空判断”而非“有效校验”严格解析Token并校验签名和过期时间时间戳范围过大且无nonce无法拦截重放请求时间窗口限制建议60秒内 Redis nonce去重签名用弱算法或密钥硬编码签名可预测或可直接提取HMAC-SHA256密钥放配置中心/KMSAPI Key放在前端代码或小程序包里静态密钥天然泄露端上只用短期Token服务端持真实API Key同一连接内越权访问他人会话鉴权只做在连接建立时未做消息级授权每条业务消息重新校验资源和用户归属这张表里的每一行都可以作为面试中“你做过什么安全加固”的展开点。尤其通信封装比较薄弱、从没考虑过重放攻击的团队往往在别人用抓包工具重放了几次请求之后才发现自己的服务跟裸奔没什么区别。5. 面试官追问Top 5答完这些这道题才算真的会了5.1 Token有效期到底设多少这个问题没有标准答案但可以分层回答。短期Token建议5到15分钟长期TokenRefresh Token建议7到30天并且要支持续期。对大模型接口来说SSE和WebSocket这种长连接场景尤其要独立处理连接的准入token有效期要短比如30秒到60秒连接内允许长时间存活但必须支持服务端主动踢出。从风险角度说token有效期越短越安全但也不能短到用户打几个字就要重新登录所以需要区分使用场景。5.2 JWT无状态 vs Session有状态怎么选大模型接口的鉴权我建议核心用有状态的设计或者至少保留一份服务端可查的会话状态。原因很简单模型调用涉及成本和合规出了事情必须能把某个token立即吊销。纯JWT一旦签发在过期前是无法主动失效的虽然也可以用黑名单机制弥补但那等于又引入了状态存储。我的做法是混合模式网关层用JWT做轻量级身份传递业务层再配合Redis里的会话状态做实时吊销和配额判断。这样既有无状态扩展的好处又能做到随时踢人、随时封禁。5.3 网关层鉴权与业务层鉴权怎么分工网关层负责粗粒度的身份确认比如这个请求带没带有效token、API Key是否存在、有没有过基本的限流。业务层负责细粒度的授权比如这个用户有没有权限访问某个会话、有没有权限调用某个模型、剩余配额够不够。两者不是二选一而是缺一不可。如果只做网关层业务数据归属校验容易出现漏洞如果只做业务层网关容易被打满恶意请求会消耗大量业务资源。5.4 审计日志到底要记哪些字段大模型接口的审计日志比普通接口多几个关键字段用户ID、API Key标识、调用的模型名、输入token数、输出token数、接口耗时、HTTP状态码、错误码。之所以要记输入输出token数是为了跟计费对账如果某天账单异常审计日志能帮你定位到是哪个Key、哪个用户、在哪个时间点发了什么请求导致消耗暴涨。没有这套日志出了问题只能干瞪眼。5.5 你手写过的最不安全的鉴权实现是什么这道题是压力面里常见的变体考察的是你有没有真实踩过坑。你可以大方承认早期写过把API Key写死在前端代码里的版本也可以承认做过仅仅校验“Header不为空”就算通过的过滤器。关键是讲完坑之后要立刻给出你的改进方案让面试官看到你的成长逻辑和复盘能力。我个人实际项目里印象最深的教训是早期为了图省事在Spring Boot里给模型接口做了一个全局HandlerInterceptor所有请求用一个统一的静态Token做校验结果Token被别人通过浏览器调试工具扒出来一夜之间被刷了几百块钱的模型调用。后来我改成“用户Token 应用API Key 时间戳签名 配额限流”的四层组合再也没出过类似问题。所以回到最初的问题大模型接口为什么必须先做鉴权因为大模型服务的每一次调用都在烧钱每一次越权都可能泄露业务数据每一次绕过都意味着合规事故的隐患。鉴权不是安全团队给你贴的标签而是大模型应用从Demo走向生产必须跨过的第一道门槛。你在设计和实现鉴权链路时想得越细后续在成本控制、故障排查、合规审计上就越省心。