长假前有个前同事发我一堆401日志说自己写的Python接口被人刷了一晚上。我打开他的服务目录扫了一圈十分钟就点出了问题调试模式开着、数据库密码直接写在settings.py里、所有POST接口基本是裸奔的任何一个请求都能往里写数据连token都不校验。这不是个例我看过太多跑在内网甚至公网上的Python服务功能是真的好用安全防御却约等于没有。这次写第九篇聊一聊我一直在用的Python服务安全基线10条。不追求让你一夜变安全专家但照着这10条做能把绝大多数脚本扫描和低级攻击挡在门外。1. 为什么你的Python接口正在裸奔1.1 裸奔的三种典型场景先说个扎心的事实很多Python接口不是被攻破的是根本没设防。第一种场景是内网服务裸奔。团队把FastAPI、Flask往服务器上一扔监听8080觉得内网很安全结果一个钓鱼邮件就让攻击者进了内网然后顺着端口扫描直接打到了你的接口上。更别提有些人为了方便顺手把服务绑在0.0.0.0上公网一访问就是一个活靶子。第二种场景更常见密钥溜进了Git。调试的时候图省事把API Key、数据库密码写进代码顺手提交到了Git仓库。Git历史是删不干净的只要你推上去过这个密钥就等于已经公开解密脚本在网络上一搜一堆。我看到过大厂内部的Python项目在私有GitLab里照样能搜到几十个带密钥的提交基本等于把数据库和第三方服务的钥匙挂在大门口。第三种场景是认证接口无限重试。登录接口、找回密码接口、短信验证码接口往往是最容易被爆破的。没有限流、没有锁定策略、没有验证码攻击者拿一个弱密码字典一个晚上就能跑十万次。很多接口不设防的根源就一句话能用就行安全以后再说。可“以后”通常永远不会来。1.2 安全基线不是满分标准而是最低配置我强调一下“基线”这两个字。安全基线不是让你去做零信任、做WAF、做SOC那套完整体系它只是一个最低配置门槛。就像开车必须系安全带不等于你永远不会出事故但能大幅降低伤亡概率。对这10条基线我的判断标准很简单如果你今天把接口暴露到公网这10条没做到出事的概率是极高的。Python服务尤其需要这类基线。原因很现实Python开发效率太高了从写一个接口到上线生产可能只要半天安全配置往往还停留在“本地demo”状态。再加上大量中小团队没有专职安全运维开发同学自己管服务器、自己写接口、自己发版安全完全靠个人自觉。所以我把这10条整理成一份可以直接对着干的清单每次上线前自查一遍能省掉后面无数个被刷、被拖库、被薅羊毛的夜晚。2. 先看全景Python服务安全基线10条清单2.1 10条基线速览先给你一张全景表后面再逐条拆解。这张表不是给你背的是给你贴在上线检查单里的。编号基线项主要防什么落地位置1强制身份认证未授权调用、匿名访问应用层FastAPI/Flask依赖2最小权限鉴权越权操作、横向移动应用层RBAC/Scope机制3令牌与密钥生命周期管理密钥泄漏、令牌被盗用应用层配置中心4全链路TLS加密中间人窃听、数据篡改网关/反向代理5输入校验与服务端白名单注入攻击、畸形参数应用层Pydantic/Schema6敏感数据不落日志、不返前端数据泄露、隐私外泄日志层序列化层7CORS与安全响应头跨域滥用、浏览器侧攻击中间件/网关8限流与超时控制暴力破解、接口被刷、服务宕机网关应用层9安全日志与审计安全事件无法溯源应用层日志系统10依赖漏洞扫描与定期巡检已知CVE、供应链攻击CI/CD日常运维你可能会觉得“就这都是老生常谈”。没错安全本来就是大量无聊的重复工作。真正导致漏洞的往往不是不知道这些点而是没人把它们固化成流程和代码。2.2 别一次性铺开按优先级分三批落地这10条别想着一天全做完不现实也没有必要。我建议分三批推进每一批完成后先上线运行一段时间稳定了再做下一批。第一批是认证、鉴权、密钥管理也就是第1到第3条。这一批解决的是“谁在调用接口、能不能调用、调用的是不是他能碰的数据”。这批不做后面做再多都是白搭攻击者甚至不需要走到数据层就已经可以随便访问你的业务功能了。第二批是第4到第6条传输加密、输入校验、敏感数据保护。这套解决的是“数据在传输和业务处理过程中会不会被偷看、被篡改、被带出去”。这批属于数据链路的核心防线也是很多合规审计最看重的地方。第三批是第7到第10条响应头加固、限流、日志审计、依赖扫描。这批解决的是“短期的稳定运行和长期的可维护性”。它们不像前两批那样立刻可见效果但真出事时限流能救你服务器日志能帮你溯源依赖扫描能让你避免带着已知漏洞上线。3. 认证与访问控制前三道闸门3.1 第1条强制身份认证拒绝“匿名访问”认证是所有接口安全的第一道闸门。很多人会问我这个接口就是给内部系统调用的能不能用IP白名单我建议你马上改变这个习惯。IP白名单在云原生环境里极不可靠容器出口IP随时变内网IP也可能被伪造或跳板访问。更好的做法是给每个调用方发一个凭证哪怕是最简单的API Key都比“看IP认人”靠谱得多。在FastAPI里最简单的做法是利用全局依赖让所有路由自动经过认证。代码大致是这样from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer security HTTPBearer(auto_errorFalse) async def require_user(credentialsDepends(security)): if credentials is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detailmissing token, headers{WWW-Authenticate: Bearer}, ) user verify_token(credentials.credentials) if user is None: raise HTTPException( status_codestatus.HTTP_401_UNAUTHORIZED, detailinvalid token, headers{WWW-Authenticate: Bearer}, ) return user # 全局生效 app FastAPI(dependencies[Depends(require_user)]) # 登录、健康检查等白名单路径可以单独排除 app.get(/health, dependencies[]) async def health(): return {status: ok}这里有个小细节认证失败一定要返回401而不是403。401代表“你没登录或凭证无效”403代表“你登录了但没权限”。很多开发同学混用把该返回401的接口返回403会让前端和调用方难以区分问题也影响安全审计的可读性。另外推荐用标准的Authorization: Bearer头不要自创X-Token之类的Header。标准头的兼容性好Nginx、网关、监控组件都认识它而自创Header往往要在网关层单独适配。3.2 第2条最小权限鉴权别把所有用户当管理员有了认证还不够认证只能确认“你是谁”不能决定“你能做什么”。很多Python服务的用户体系里只有一个is_admin字段非管理员就是普通用户功能权限完全靠前端隐藏按钮。但前端隐藏只是视觉上的隐藏接口本身如果全都能调攻击者拿普通账号绕开前端照样能调删除接口。最小权限原则的意思是每个用户只拥有完成自己工作所需的最小权限集合。比如普通用户能创建订单、查看自己的订单但不能删除订单运营同学能看报表但不能修改配置管理员才能改系统参数。在代码里我用一个专门的依赖来检查权限from fastapi import HTTPException, status def require_permission(perm: str): def checker(user: dict Depends(require_user)): if perm not in user.get(permissions, []): raise HTTPException( status_codestatus.HTTP_403_FORBIDDEN, detailfpermission denied: {perm}, ) return user return checker # 使用 app.delete(/api/orders/{order_id}) async def delete_order( order_id: str, user: dict Depends(require_permission(order:delete)), ): ...这里的permissions可以来自用户角色也可以来自Scope具体看你的架构。关键是不要把判断逻辑散落在每个接口里封装成依赖后新接口默认就继承了这个规则漏配权限的风险会小很多。3.3 第3条令牌与密钥生命周期管理很多团队把JWT做好了就往上一丢token永不过期签名密钥永远不变。这等于给攻击者留了一扇永远打不开的门。JWT本身是个好东西但生命周期必须管住。我的习惯是access token短期有效15分钟到30分钟即可refresh token长期有效比如7天并且refresh token要存在服务端支持吊销。签名密钥也要定期轮换。每次轮换时用kid标识当前使用哪把密钥做验签旧密钥可以保留一个缓冲区让已签发的token能继续用一段时间避免密钥一换所有用户被强制下线。示例实现可以这样理解# 使用两个密钥current_key 用于签发和验签previous_key 仅用于验签 def verify_token(token: str): for key in [current_key, previous_key]: try: payload jwt.decode(token, key, algorithms[HS256]) return payload except Exception: continue return None密钥本身的管控比token更重要。千万别把密钥写死在代码里开发环境、测试环境、生产环境必须用不同的密钥通过环境变量注入。生产环境的密钥建议放专门的密钥管理服务至少在服务器上用权限受控的配置文件存放并且确保任何密钥都不会出现在Git历史、日志、错误监控系统里。4. 数据传输与边界防护中间五道防线4.1 第4条全链路TLS加密很多人以为“我Python应用里加了SSL证书”就行了。实际上Python应用本身一般并不直接处理TLS而是由前置的Nginx、Caddy或者云负载均衡器终止TLS。放在前面的这一层决定了用户从浏览器到服务器过程中数据是不是明文传输。HTTP明文意味着任何一个网络节点都能抓包看到请求体和响应体包括登录密码、账号信息、业务数据。Nginx层面的标准配置大概是这样的server { listen 443 ssl http2; server_name api.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; add_header Strict-Transport-Security max-age31536000; includeSubDomains always; location / { 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; proxy_set_header X-Forwarded-Proto $scheme; } } # 强制HTTP跳转HTTPS server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; }证书链不完整是上线时最常见的坑。不少人在配置证书时只放了叶子证书没有把中间证书拼进fullchain结果移动端、部分老客户端会校验失败。我建议配置完以后用openssl命令检查证书链是否完整也可以去第三方检测工具看一眼评级。另外记得设置自动化续期别让证书悄悄过期接口访问大面积失败被业务方投诉。4.2 第5条输入校验与服务端白名单输入校验是拦住恶意请求的第一道程序闸门。很多注入类攻击本质都是“输入”没有经过严格校验就进入了下游逻辑。Python生态里最方便的工具是Pydantic声明一个模型类型、长度、格式、取值范围全都能约束。from pydantic import BaseModel, Field, HttpUrl class OrderCreate(BaseModel): order_no: str Field(..., min_length8, max_length32, patternr^[A-Z0-9]$) quantity: int Field(..., ge1, le1000) callback_url: HttpUrl | None None这里的核心原则是不要信任前端已经帮你校验过服务端必须自己再做一遍。比如order_no定义了正则白名单只允许大写字母和数字那么包含SQL关键字、XSS payload的请求在模型校验阶段就直接被Pydantic挡掉了。对于枚举值比如状态字段用Literal限定取值对于文本字段设置最大长度防止超大请求体打爆内存。还有一个我踩过的坑是路径参数也要校验。很多人只校验了请求体忽略了路径里的id、name结果攻击者在路径里注入特殊字符同样能造成问题。SQL查询一定要参数化永远不要把字符串拼接直接丢给数据库执行。4.3 第6条敏感数据不落日志、不返前端日志和接口响应是两个默认的泄密口。先说日志。很多Python框架默认会打印请求头和请求体如果你不加处理用户的token、密码、身份证号、手机号就可能原样出现在日志文件里进而被日志采集系统索引最后被搜到。我建议在日志系统里加一层脱敏过滤器import logging import re class SensitiveDataFilter(logging.Filter): SENSITIVE_PATTERN re.compile( r?(password|token|secret|api_key|authorization)?\s*[:]\s*?[^,\s]?, re.IGNORECASE, ) def filter(self, record): msg record.getMessage() msg self.SENSITIVE_PATTERN.sub(r\1***, msg) record.msg msg return True logging.getLogger().addFilter(SensitiveDataFilter())再说到返回前端。直接把数据库模型返回给前端很容易顺带把内部字段泄露出去。比如用户表里有个password_hash的字段如果response_model没定义好序列化时可能就漏出去了。我的建议是每个接口单独定义返回模型只暴露必要字段。FastAPI的response_model机制就是干这个的加上Pydantic的SecretStr能让敏感字段在序列化时自动打码。4.4 第7条CORS与安全响应头CORS问题是前后端分离架构里最常见的配置坑。很多人图省事直接allow_origins[*]加allow_credentialsTrue浏览器直接就拒绝了带cookie的跨域请求而且这种配置还容易引发恶意站点跨域调用你接口的风险。app.add_middleware( CORSMiddleware, allow_origins[https://admin.example.com, https://app.example.com], allow_methods[GET, POST], allow_headers[Authorization], allow_credentialsTrue, )这里关键点是CORS永远不要用通配符配合凭据。你宁可把域名列表写明确一点也不要用“*”偷懒。同时要理解CORS的作用边界CORS是浏览器侧的跨域控制它不能代替认证和鉴权攻击者用Postman这类工具根本不受CORS限制。除了CORS还有一组基础安全响应头值得加上X-Content-Type-Options: nosniff防止浏览器猜测文件类型X-Frame-Options: DENY防止页面被嵌套到钓鱼站点Content-Security-Policy限制当前页面能加载的资源来源Referrer-Policy: no-referrer防止请求头携带敏感来源信息。这些东西加起来成本很低但能明显提高浏览器侧的安全性。4.5 第8条限流与超时控制限流是接口的保险丝。登录接口不限流等于把密码爆破的大门敞开业务接口不限流一个误操作、一个被劫持的脚本就能把数据库拖垮。我最推荐的方案是Nginx层和应用层双层限流。Nginx擅长连接级别的控制比如单IP每秒请求数应用层更适合业务级别的控制比如“每个用户每分钟最多创建20个订单”。在FastAPI里用slowapi可以很快搞定from slowapi import Limiter from slowapi.util import get_remote_address limiter Limiter(key_funcget_remote_address) app.state.limiter limiter app.post(/api/orders) limiter.limit(20/minute) async def create_order(request: Request, order: OrderCreate, userDepends(require_user)): ...限流不只是限制次数超时控制同样重要。服务自身要对慢客户端说不不要让一个慢连接占住worker不释放调用下游外部服务时也要设超时时间防止一个慢第三方接口把整个请求链路拖死。数据库连接池、外部API调用的read timeout、write timeout这些参数都要在代码里明确配置而不是依赖默认值。5. 可观测性与持续加固最后两条5.1 第9条安全日志与审计安全日志的价值在事后排查时才能体现出来。很多团队平时不觉得日志重要出了安全问题只能从一堆无结构文本里瞎翻。我的建议是所有日志采用结构化格式至少包含这些字段请求ID、用户标识、客户端IP、HTTP方法、路径、状态码、耗时、错误信息。如果能把关键安全事件单独打一个事件类型后续直接在日志平台里筛选就更方便了。审计日志要特别记录几类行为登录成功与失败、令牌校验失败、权限不足被拒绝、修改或删除核心数据、批量导出数据。这些事件是安全事件的主要信号源。401大量出现可能是有人在爆破或token失效403大量出现可能是有人在探测越权接口批量导出行为异常可能是数据被拖走的前兆。配上监控告警比如5分钟内401超过100次就告警你可以在攻击初段就介入而不是等业务方反馈“服务挂了”才发现。5.2 第10条依赖漏洞扫描与定期巡检Python项目的依赖链一年比一年长任何一个包有已知CVE都可能变成漏洞入口。我见过不少项目还在用两年前有大漏洞的旧版本框架原因就是“升级怕出问题”。怕出问题是对的但不能因为怕就不升级。至少要做到能扫出漏洞并且能对漏洞分级排序。现在常用的工具有pip-audit和bandit。pip-audit扫描Python依赖包的已知安全漏洞bandit做Python源码的静态安全问题扫描。上线前在CI里各跑一遍很实惠pip install pip-audit bandit pip-audit bandit -r app/ -f json -o bandit_report.jsonscanning结果的处理有个原则如果漏洞类型是可通过请求触发的比如反序列化、命令执行、认证绕过必须优先处理能升级就升级不能升级就要加缓解措施。如果漏洞只影响离线处理场景可以排期处理但要记录在案。依赖锁定不要用那种“什么都锁死”的粗暴做法锁版本的同时把安全更新作为例行任务排进迭代计划这样既稳又安全。6. 实操落地给一个FastAPI接口套上完整安全基线6.1 依赖和配置文件前面讲了那么多条不落地都等于零。我用一个FastAPI项目演示完整的安全基线路。依赖文件如下fastapi0.111.0 uvicorn[standard]0.30.1 python-jose[cryptography]3.3.0 slowapi0.1.9 pydantic-settings2.2.1配置统一走环境变量不让任何密钥进代码仓库。这里用pydantic-settings读取.env文件from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): app_name: str secure-api debug: bool False jwt_secret: str access_token_expire_minutes: int 30 model_config SettingsConfigDict(env_file.env, env_file_encodingutf-8) settings Settings().env文件加入.gitignore本地测试时手动创建。生产环境的JWT_SECRET通过部署平台的环境变量注入不要再放文件里。6.2 认证中间件与鉴权依赖认证部分用HTTPBearer标准头配合JWT签发和校验。签发token的简化实现如下from datetime import datetime, timedelta, timezone from jose import jwt def create_access_token(user_id: str) - str: expire datetime.now(timezone.utc) timedelta(minutessettings.access_token_expire_minutes) payload {sub: user_id, exp: expire, type: access} return jwt.encode(payload, settings.jwt_secret, algorithmHS256) def verify_token(token: str) - dict | None: try: payload jwt.decode(token, settings.jwt_secret, algorithms[HS256]) if payload.get(type) ! access: return None return payload except Exception: return None配合前面的require_user依赖全局路由默认都需要token。这样任何新接口一上线就是安全状态不会出现“开发时忘了加认证”这种低级事故。登录接口要放到白名单里否则没法拿到token用API Route或者依赖排除都可以核心是白名单越短越好并且每隔一段时间review一次。6.3 输入校验、限流与安全响应头路由上把三者组合起来。输入校验用Pydantic模型限流用slowapi响应头用中间件统一加from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded limiter Limiter(key_funcget_remote_address) app FastAPI() app.state.limiter limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.middleware(http) async def security_headers(request: Request, call_next): response await call_next(request) response.headers[X-Content-Type-Options] nosniff response.headers[X-Frame-Options] DENY response.headers[Content-Security-Policy] default-src none response.headers[Referrer-Policy] no-referrer response.headers[Cache-Control] no-store return response app.post(/api/orders, response_modelOrderOut) limiter.limit(20/minute) async def create_order(request: Request, order: OrderCreate, userDepends(require_user)): ...这里有个容易踩的坑加响应头一定要放在代理层和应用层同时考虑。如果应用层加了但Nginx层因为某种原因覆盖了以Nginx为准反过来也一样。我的习惯是响应头尽量在Nginx层统一加应用层只加那些跟业务逻辑相关的动态头避免重复和管理混乱。6.4 用curl做一轮基线自检上线前在终端跑一遍自检是最直观的验收方式。三句话能覆盖认证、证书、响应头三个维度curl -i https://api.example.com/api/orders不带token时期望返回401而不是200。如果返回200说明认证依赖没有全局生效马上修。curl -I https://api.example.com/health检查响应头。看有没有Strict-Transport-Security、X-Content-Type-Options那几个安全头同时看Server头有没有泄露Nginx版本和Python框架版本。版本号越少越好很多攻击者就是根据版本号找已知漏洞的。curl -i -H Authorization: Bearer $TOKEN -H Content-Type: application/json \ -d {order_no:ABC123456,quantity:2} https://api.example.com/api/orders期望正常返回200同时拿到正确的response_model字段不应包含内部字段。如果返回422说明Pydantic校验正常工作如果返回429说明限流配置生效了。7. 常见问题与排查技巧实录7.1 明明上了HTTPS为什么还会收到告警许多同学在Nginx上配了证书但告警还是不断过来。最常见的三个原因证书链不完整、TLS协议版本过低、HSTS缺失。证书链不完整时部分客户端无法完成校验于是自动降级或者放弃访问。用openssl检查是最快的openssl s_client -connect api.example.com:443 -servername api.example.com看输出里的证书链是否根证书、中间证书、叶子证书都齐了。如果没有中间证书把证书文件换成包含中间证书的fullchain。TLS版本方面至少要把TLSv1.0和TLSv1.1禁掉最低用TLSv1.2推荐只开TLSv1.2和TLSv1.3。HSTS缺失的话浏览器第一次访问可能还是走了HTTP连接被中间人截走加上add_header Strict-Transport-Security那行配置再重启Nginx就好。7.2 接口被刷之后限流没有生效限流配置了但还是被刷先看限流key取的是什么。在Nginx后面如果应用直接拿remote_addr做key所有请求都显示成Nginx内网IP等于限流失效。正确的做法是在代理层传递X-Forwarded-For或X-Real-IP同时让应用层读取真实客户端IP。FastAPI里如果用了slowapi的get_remote_address要对代理头做正确配置否则它拿到的永远是网关IP。另一个常见问题是多进程部署下限流计数器存在每个worker进程自己的内存里。假设你的服务起了4个worker每个worker都允许100次/分钟那实际限制了400次/分钟。对严格限流场景要把计数器放到共享存储比如Redisslowapi的storage_uri参数指向Redis就能解决。业务集群的探活接口、健康检查接口记得要从限流规则里排除否则节点一扩容健康检查把自己限流限死了服务直接处于不健康状态运维排查半天找不到原因。7.3 令牌失效、密钥泄漏如何处理JWT设计上有个痛点token签发了没法主动回收。所以处理方案是分层短期token靠过期时间来约束refresh token存服务端发现异常里可以吊销。如果确定JWT_SECRET泄漏了立即做两件事换新密钥旧密钥只保留一个很短的验签缓冲区或者直接不用撤销所有refresh token让所有用户重新登录。在实际排查会话异常时先查日志里有没有大量401再查有没有某个用户的token被反复使用。这里我建议给token加一个业务层的固定字段比如用户当前登录会话ID存Redis里做比对。一旦用户修改密码或者管理员踢人会话ID一换旧token就自动失效这样比单纯依赖JWT的exp有效得多。7.4 上线前最后一道检查习惯我自己长期保留一个习惯每次发布前在终端跑三条基线检查。第一条是搜硬编码密钥跑一个简单命令把代码里出现password、token、secret、api_key字样并赋值的地方全部过一遍排查是否进了代码库。第二条是跑pip-audit确认依赖没有新的高危CVE。第三条是拿不带token的curl打一遍核心接口确认返回的是401而不是200。这条习惯坚持下来之后我基本没有再被“上线后半小时发现接口裸奔”这类问题折磨过。安全基线的价值不在于某一条有多么精巧而在于它们变成一个不依赖个人记忆的固定流程。你可以把这三条命令写进发布脚本里跑不过就不让发版效果最好。我这几年维护Python服务的心得就是别等安全团队来查你先把这10条基线当成自己的上线规矩比什么防护都靠谱。