公开接口防盗:防止数据接口被滥用的6种实战方案
你的网站有个数据查询接口,本来只是方便自己调用,结果有天发现别人把这接口包了一层,直接嵌到他自己服务里了。
流量蹭蹭往上涨,服务器负载飙升,账单也跟着涨。这种情况怎么办?
接口防盗这事儿,核心就是让"只有我的前端能调这个接口"。
听起来简单,但单纯靠一个 Referer 校验根本防不住,稍微懂点技术的人都能绕过去。
为什么接口会被盗用
公开接口被盗有几个常见场景:
- 前端直接调用 - 接口 URL 和参数格式都暴露在浏览器请求里,一抓包全看到了
- 无鉴权机制 - 接口没有任何身份验证,谁拿到 URL 谁就能用
- 防护措施形同虚设 - Referer 能伪造,IP 限制不够细,签名算法被逆向
说白了,只要接口返回的数据有价值,就一定会有人惦记。
方案一:Referer 校验(入门级)
Referer 是浏览器自动带上的请求头,标识这个请求是从哪个页面发起的。
最简单的防盗方式,就是检查 Referer 是不是你的域名。
func checkReferer(r *http.Request) bool {
referer := r.Header.Get("Referer")
// 只允许来自自己网站的请求
if strings.HasPrefix(referer, "https://yoursite.com") {
return true
}
return false
}但 Referer 能伪造。
别人用代码发请求时手动加个 Referer 头,这层防护就废了。
所以这招只能挡住小白,真想盗接口的人根本不 care。
方案二:签名验证(常规手段)
签名验证是把请求参数和一个密钥拼起来,算个哈希值,服务端收到请求后再算一遍,对上了才认。
签名生成流程
- 前端把请求参数按字母排序拼成字符串
- 加上时间戳和密钥
- 算 SHA256 哈希值作为签名
- 把签名和时间戳一起发给后端
const crypto = require('crypto');
/**
* 生成签名(和 Go 版本逻辑完全一致)
* @param {Object} params - 参数对象
* @param {string} secret - 密钥
* @returns {string} sha256 签名字符串
*/
function generateSign(params, secret) {
// 1. 获取参数 key 并排序
const keys = Object.keys(params).sort();
// 2. 拼接排序后的参数
let str = '';
for (const key of keys) {
str += `${key}=${params[key]}&`;
}
// 3. 拼接密钥
str += `key=${secret}`;
// 4. sha256 加密
return crypto.createHash('sha256').update(str).digest('hex');
}
/**
* 验证签名
* @param {Object} params - 参数对象
* @param {string} sign - 待验证的签名
* @param {string} secret - 密钥
* @returns {boolean} 验证结果
*/
function verifySign(params, sign, secret) {
const expectedSign = generateSign(params, secret);
return sign === expectedSign;
}
// 导出给外部使用
module.exports = {
generateSign,
verifySign
};前端调用时:
const {
generateSign,
verifySign
} = require('./sign');
// 测试参数
const params = {
username: 'test',
age: '18',
timestamp: '1712345678'
};
const secret = 'mySecretKey123';
// 生成签名
const sign = generateSign(params, secret);
console.log('生成签名:', sign);
// 验证签名
const isValid = verifySign(params, sign, secret);
console.log('验证结果:', isValid); // true
后端验证:
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 提取参数
params := map[string]string{
"userId": r.FormValue("userId"),
"type": r.FormValue("type"),
"ts": r.FormValue("ts"),
}
sign := r.FormValue("sign")
// 验证时间戳(防重放攻击)
ts, _ := strconv.ParseInt(params["ts"], 10, 64)
if time.Now().Unix()-ts > 300 { // 5分钟过期
http.Error(w, "request expired", http.StatusForbidden)
return
}
// 验证签名
if !verifySign(params, sign, "your-secret-key") {
http.Error(w, "invalid signature", http.StatusForbidden)
return
}
// 处理业务逻辑
// ...
}这招的问题是:如果前端代码里的密钥暴露了(JS 代码能被看到),别人照样能算出正确签名。
所以密钥最好放在你自己的中间层服务里,而不是直接写在前端。
方案三:Token 鉴权(推荐)
Token 鉴权就是用户先登录拿到一个令牌,后续请求都带上这个令牌。
服务端验证令牌有效性,无效就拒绝。
import (
"github.com/golang-jwt/jwt/v5"
"time"
)
var jwtSecret = []byte("your-jwt-secret")
// 生成 Token
func generateToken(userId string) (string, error) {
claims := jwt.MapClaims{
"userId": userId,
"exp": time.Now().Add(time.Hour * 24).Unix(), // 24小时过期
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// 验证 Token
func verifyToken(tokenString string) (*jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return &claims, nil
}
return nil, fmt.Errorf("invalid token")
}
// 中间件:校验 Token
func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
http.Error(w, "missing token", http.StatusUnauthorized)
return
}
// 去掉 "Bearer " 前缀
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
claims, err := verifyToken(tokenString)
if err != nil {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
// 把用户信息存到上下文(可选)
// ctx := context.WithValue(r.Context(), "userId", (*claims)["userId"])
// r = r.WithContext(ctx)
next(w, r)
}
}使用:
http.HandleFunc("/api/data", authMiddleware(handleDataRequest))Token 方案的好处是:就算别人拿到接口地址,没有有效 Token 也调不通。
但登录逻辑要做好,别让人轻易注册个账号就能无限薅数据。
方案四:IP 限流(防刷量)
即使有签名和 Token,也挡不住有人注册一堆账号来刷接口。
这时候就需要限流。
常见限流策略:
- 按 IP 限流 - 同一个 IP 每分钟最多调 100 次
- 按用户限流 - 同一个用户每小时最多调 1000 次
- 全局限流 - 整个接口每秒最多处理 500 个请求
可以用 Redis 实现计数器限流:
import (
"context"
"github.com/redis/go-redis/v9"
"time"
)
var rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
// IP 限流:每分钟最多 100 次
func rateLimitByIP(ip string) bool {
ctx := context.Background()
key := fmt.Sprintf("rate_limit:ip:%s", ip)
count, err := rdb.Incr(ctx, key).Result()
if err != nil {
return false
}
if count == 1 {
rdb.Expire(ctx, key, time.Minute)
}
return count <= 100
}
// 限流中间件
func rateLimitMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
if !rateLimitByIP(ip) {
http.Error(w, "rate limit exceeded", http.StatusTooManyRequests)
return
}
next(w, r)
}
}但纯 IP 限流也有问题:别人用代理池或者 VPN,换个 IP 就能继续刷。
所以限流最好结合 Token,按用户维度来限。
方案五:CSRF防御之token认证
CSRF(Cross-site request forgery),中文名称:跨站请求伪造。 攻击者盗用你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。 所以用 CSRF 防御来防止接口被盗刷也是完成也可以的,
CSRF Token的防护策略分为三个步骤:
-
- 将CSRF Token输出到页面中 用户进入页面时,服务端需为其生成唯一 Token。该 Token 通过加密算法生成,通常由随机字符串 + 时间戳组合构成。
出于安全考量,Token不能存放于 Cookie中,否则易被攻击者盗用冒用。最优方案是将 Token 存储在服务端 Session 里。
后续每次页面加载时,通过 JS 遍历整页 DOM 树,自动为页面中所有 a 标签、form 表单 自动追加 Token 参数,可防护大部分常规请求。
但该方式仅对页面初始静态 DOM生效,页面加载后动态生成的 HTML 元素无法自动植入 Token,需要开发人员在业务编码中手动拼接添加。
-
- 页面提交的请求携带这个Token 可以通过请求参数携带,也可以通过请求头携带
-
- 服务器验证 Token 是否正确 客户端获取 Token 后,再次向服务端发起请求提交 Token 时,服务端需对 Token 做有效性校验。 验证逻辑为:先对 Token 进行解密,校验加密字符串是否匹配,同时核对时间戳是否在有效期内;只有加密字符串一致、且时间未过期,才判定当前 Token 合法有效。
这样对方如果想要盗你的接口,也得先模拟访问你的页面拿 token,成本大幅上升。
方案六:动态参数加密(对抗逆向)
有些人会逆向你的前端代码,把签名算法扒出来自己用。
怎么办?让算法动态化。
比如签名密钥不固定,而是定期从服务端拉取:
// 服务端定期轮换密钥
var currentSecret = "secret-v1"
var secretVersion = 1
// 前端先请求当前密钥版本
func getSecretVersion(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"version": %d}`, secretVersion)
}
// 根据版本返回对应密钥(这个接口要做 Token 鉴权)
func getSecret(w http.ResponseWriter, r *http.Request) {
version := r.URL.Query().Get("version")
// 这里可以从数据库读取历史密钥,支持旧版本客户端
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"secret": "%s"}`, currentSecret)
}前端每次请求前先拉最新密钥版本,用最新密钥签名。这样即使别人逆向了代码,密钥过几天就换了,又得重新逆向。
更狠一点的做法是用 WebAssembly 把签名算法编译成二进制,增加逆向难度。但说实话,真有人下这个功夫,也能破解。只是提高了成本而已。
方案七:设置 CORS(跨域限制)
CORS(跨域资源共享)可以限制哪些域名可以访问你的接口。在响应头里设置:
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 只允许你自己的域名
w.Header().Set("Access-Control-Allow-Origin", "https://yoursite.com")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// 处理预检请求
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next(w, r)
}
}但 CORS 只对浏览器有效。别人用代码直接发 HTTP 请求,根本不 care CORS。
所以这招只是辅助手段,不能单独依赖。
组合拳:多层防护
单一方案都有弱点,最靠谱的是叠加多层防护:
- 前端层 - CORS + Referer + CSRF 防御校验,挡住浏览器里的简单盗用
- 签名层 - 动态密钥签名验证,增加逆向成本
- 鉴权层 - Token 验证,没登录不给用
- 限流层 - IP + 用户双重限流,防止暴力刷接口
- 监控层 - 记录异常请求(比如签名频繁失败的 IP),及时封禁
举个例子:正常用户走浏览器访问,CORS 和 Referer 没问题,Token 也有效,签名验证通过,请求频率正常,数据返回。
盗用者用代码调接口:没有有效 Token,直接被拦在鉴权层。就算他注册了账号拿到 Token,频繁请求会触发限流。如果他逆向了签名算法,定期轮换密钥也能让他的工具失效。
常见问题
Q1. 为什么不直接设置接口不公开?
有些业务场景必须公开接口,比如网站是没有登录注册功能的,那么很多数据接口是公开的 API
这时候只能通过技术手段控制滥用,而不是彻底关闭接口。
Q2. 签名算法被破解了怎么办?
定期轮换密钥,同时监控异常请求。如果发现某个 IP 或账号频繁请求失败后又突然成功,可能是破解了签名,直接封禁。
Q3. 限流会不会误伤正常用户?
会。所以限流阈值要根据实际业务调整。可以给普通用户一个基础额度(比如每分钟 10 次),给付费用户或认证用户更高额度。
Q4. Token 过期了怎么办?
用 Refresh Token 机制。用户登录时返回两个 Token:Access Token(短期有效,比如 1 小时)和 Refresh Token(长期有效,比如 30 天)。Access Token 过期后,用 Refresh Token 换新的,不需要重新登录。
Q5. CORS 能防住所有盗用吗?
不能。CORS 只对浏览器有效,别人用 curl、Python、Go 等直接发请求,完全绕过 CORS。所以 CORS 只是辅助,不能作为主要防护手段。
总结
接口防盗没有银弹,但叠加多层防护能大幅提高盗用成本。
Referer 和 CORS 挡小白,签名验证和 Token 鉴权挡技术手,限流挡暴力刷,监控挡持久攻击。
最关键的是:别把密钥硬编码在前端,别让接口完全无鉴权。
如果你的接口数据特别值钱(比如付费 API),考虑加上计费系统,让盗用者每次调用都得付费,自然就不划算了。
如果大家对接口防盗还有疑问,或者遇到过更复杂的盗用场景,欢迎在评论区交流~~~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/interface-theft-protection/
备用原文链接: https://blog.fiveyoboy.com/articles/interface-theft-protection/