React+FastAPI无访客认证场景下的API访问控制方案咨询
这个问题我之前帮朋友搭开放访客平台的时候刚好踩过坑——开放给未登录用户的API确实容易被恶意爬取或者滥用,但又不能加登录门槛影响用户体验,得在「开放易用」和「基础安全」之间找平衡。给你几个落地性强的方案,你可以根据自己的业务风险等级来选:
一、基础防护:CORS配置 + Referer头校验(适合低风险公开场景)
这是最容易上手的方案,先把最基础的“非前端域名的请求”挡在门外:
- 严格配置CORS
在FastAPI里只允许你的React生产域名跨域请求,别图省事用通配符*:
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() # 只添加你的React域名,开发环境的本地地址可以暂时保留 origins = [ "https://your-react-app.com", "http://localhost:3000", ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], )
这样浏览器层面就会拦截其他域名发起的跨域请求,能挡掉大部分随便用Postman直接测的小白或者简易自动爬虫工具。
- 补充Referer头校验
虽然Referer可以被伪造,但能过滤掉很多没刻意做伪装的请求。你可以写一个FastAPI依赖项来统一校验:
from fastapi import Header, HTTPException, Depends async def validate_referer(referer: str = Header(None)): allowed_domains = ["your-react-app.com", "localhost:3000"] if not referer: raise HTTPException(status_code=403, detail="无效的请求来源") # 检查Referer是否包含允许的前端域名 if not any(domain in referer for domain in allowed_domains): raise HTTPException(status_code=403, detail="禁止的请求来源") return referer # 在需要保护的路由上挂载这个依赖 @app.get("/public-list") async def get_public_data(referer = Depends(validate_referer)): return {"data": "公开内容列表"}
这个方案优点是零额外开发成本,缺点是防不住刻意伪造Referer的请求,适合只是展示静态公开内容、没有资源消耗的场景。
二、进阶防护:临时访客令牌(无需用户登录)
给每个首次访问的前端分配一个临时访客令牌,用来做请求身份标识,同时配合限流,能有效降低单IP滥用的风险:
- 前端初始化时获取令牌
React项目在根组件挂载时,自动向后端请求令牌并存在localStorage里:
import { useEffect } from 'react'; function App() { useEffect(() => { const getVisitorToken = async () => { const storedToken = localStorage.getItem('visitor_token'); if (!storedToken) { const res = await fetch('https://your-backend.com/get-visitor-token'); const data = await res.json(); localStorage.setItem('visitor_token', data.token); } }; getVisitorToken(); }, []); return ( <div className="App"> {/* 你的页面内容 */} </div> ); } export default App;
- 后端生成并校验令牌
用Redis存储令牌(设置过期时间,比如24小时),同时搭配限流工具控制请求频率:
import redis import secrets from fastapi import Header, HTTPException, Depends from slowapi import Limiter, _rate_limit_exceeded_handler from slowapi.util import get_remote_address from slowapi.errors import RateLimitExceeded # 初始化Redis和限流工具 r = redis.Redis(host='localhost', port=6379, db=0) limiter = Limiter(key_func=get_remote_address) app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # 生成访客令牌的接口 @app.get("/get-visitor-token") @limiter.limit("10/minute") # 限制每个IP每分钟最多拿10个令牌 async def get_visitor_token(): token = secrets.token_hex(16) # 设置令牌24小时后过期 r.setex(f"visitor:{token}", 86400, "valid") return {"token": token} # 令牌校验的依赖项 async def validate_visitor_token(x_visitor_token: str = Header(None)): if not x_visitor_token or not r.exists(f"visitor:{x_visitor_token}"): raise HTTPException(status_code=403, detail="无效的访客令牌") return x_visitor_token # 受保护的接口示例 @app.get("/protected-public-data") @limiter.limit("50/hour") # 限制每个令牌每小时最多50次请求 async def get_protected_data(token = Depends(validate_visitor_token)): return {"data": "带基础防护的公开内容"}
这个方案能有效防止单IP的批量滥用,令牌过期机制也能避免长期占用资源,缺点是如果令牌被泄露,别人还是能模拟请求,但已经能挡掉大部分自动爬虫了。
三、中高风险场景:请求签名机制
如果你的API涉及到资源消耗(比如生成动态内容、复杂数据库查询),可以加请求签名,让只有知道约定规则的前端才能生成有效请求:
- 前后端约定签名规则
比如用时间戳 + 请求参数 + 共享密钥生成HMAC-SHA256签名,前端请求时在Header里带上X-Timestamp和X-Signature:
// 前端生成签名的工具函数(注意:密钥要存在环境变量里,绝对不能硬编码在代码中!) import crypto from 'crypto-js'; const generateSignature = (params, timestamp) => { const secret = process.env.REACT_APP_API_SECRET; // 固定拼接规则,后端要和前端完全一致 const signStr = `${timestamp}&${JSON.stringify(params)}`; return crypto.HmacSHA256(signStr, secret).toString(crypto.enc.Hex); }; // 带签名的请求示例 async function fetchComplexData(params) { const timestamp = Date.now().toString(); const signature = generateSignature(params, timestamp); const token = localStorage.getItem('visitor_token'); const res = await fetch('https://your-backend.com/complex-data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Visitor-Token': token, 'X-Timestamp': timestamp, 'X-Signature': signature }, body: JSON.stringify(params) }); return res.json(); }
- 后端校验签名
import hmac import hashlib import json from fastapi import Header, HTTPException, Body, Depends from datetime import datetime # 和前端一致的密钥,存在后端环境变量里 SECRET_KEY = b"your-shared-secret" async def validate_signature( x_timestamp: str = Header(None), x_signature: str = Header(None), params: dict = Body(None) ): if not x_timestamp or not x_signature: raise HTTPException(status_code=403, detail="缺少签名参数") # 校验时间戳,防止重放攻击(只允许5分钟内的请求) current_ts = int(datetime.now().timestamp() * 1000) if abs(int(x_timestamp) - current_ts) > 300 * 1000: raise HTTPException(status_code=403, detail="请求已过期") # 生成对比签名,注意参数排序要和前端一致 sign_str = f"{x_timestamp}&{json.dumps(params, sort_keys=True)}".encode('utf-8') expected_signature = hmac.new(SECRET_KEY, sign_str, hashlib.sha256).hexdigest() # 用安全的对比方法,避免时序攻击 if not hmac.compare_digest(x_signature, expected_signature): raise HTTPException(status_code=403, detail="无效的签名")
这个方案能防大部分伪造请求,因为不知道共享密钥就生成不了正确的签名。注意前端的密钥要通过构建工具注入环境变量,不要直接写在代码里,同时可以配合代码混淆降低泄露风险。
四、兜底方案:速率限制(必须加!)
不管用上面哪种方案,都要给API加速率限制,就算被突破了校验,也不会把后端打崩。FastAPI用slowapi库很容易实现,前面的例子里已经用到了,核心是根据接口的资源消耗情况,设置合理的请求频率,比如普通接口每分钟100次,复杂接口每小时50次。
最后总结一下
其实没有100%安全的开放API,核心是根据你的业务风险来组合方案:
- 如果只是展示静态公开内容:CORS配置 + Referer校验 + 基础限流就够了
- 如果有轻度资源消耗的接口:临时访客令牌 + 限流
- 如果是中高风险的核心接口:请求签名 + 临时令牌 + 严格限流
- 如果怕机器人批量请求:可以加轻量人机验证(比如滑块验证码),访客第一次触发核心操作时验证,通过后再发有效令牌,这样能挡掉大部分自动化工具。
另外一定要记住:前端的任何校验都只能做“友好提示”,后端必须对所有请求参数做严格校验,不要信任前端传过来的任何数据!




