目录

公开接口防盗:防止数据接口被滥用的6种实战方案

你的网站有个数据查询接口,本来只是方便自己调用,结果有天发现别人把这接口包了一层,直接嵌到他自己服务里了。

流量蹭蹭往上涨,服务器负载飙升,账单也跟着涨。这种情况怎么办?

接口防盗这事儿,核心就是让"只有我的前端能调这个接口"。

听起来简单,但单纯靠一个 Referer 校验根本防不住,稍微懂点技术的人都能绕过去。

/img/interface-theft-protection/0101.png
接口防盗六层防护体系

为什么接口会被盗用

公开接口被盗有几个常见场景:

  • 前端直接调用 - 接口 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。

方案二:签名验证(常规手段)

/img/interface-theft-protection/0102.png
API签名验证流程

签名验证是把请求参数和一个密钥拼起来,算个哈希值,服务端收到请求后再算一遍,对上了才认。

签名生成流程

  1. 前端把请求参数按字母排序拼成字符串
  2. 加上时间戳和密钥
  3. 算 SHA256 哈希值作为签名
  4. 把签名和时间戳一起发给后端
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,按用户维度来限。

/img/interface-theft-protection/0103.png
限流策略对比

方案五:CSRF防御之token认证

CSRF(Cross-site request forgery),中文名称:跨站请求伪造。 攻击者盗用你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。 所以用 CSRF 防御来防止接口被盗刷也是完成也可以的,

CSRF Token的防护策略分为三个步骤:

    1. 将CSRF Token输出到页面中 用户进入页面时,服务端需为其生成唯一 Token。该 Token 通过加密算法生成,通常由随机字符串 + 时间戳组合构成。

    出于安全考量,Token不能存放于 Cookie中,否则易被攻击者盗用冒用。最优方案是将 Token 存储在服务端 Session 里。

    后续每次页面加载时,通过 JS 遍历整页 DOM 树,自动为页面中所有 a 标签、form 表单 自动追加 Token 参数,可防护大部分常规请求。

    但该方式仅对页面初始静态 DOM生效,页面加载后动态生成的 HTML 元素无法自动植入 Token,需要开发人员在业务编码中手动拼接添加。

    1. 页面提交的请求携带这个Token 可以通过请求参数携带,也可以通过请求头携带
    1. 服务器验证 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。

所以这招只是辅助手段,不能单独依赖。

组合拳:多层防护

单一方案都有弱点,最靠谱的是叠加多层防护:

  1. 前端层 - CORS + Referer + CSRF 防御校验,挡住浏览器里的简单盗用
  2. 签名层 - 动态密钥签名验证,增加逆向成本
  3. 鉴权层 - Token 验证,没登录不给用
  4. 限流层 - IP + 用户双重限流,防止暴力刷接口
  5. 监控层 - 记录异常请求(比如签名频繁失败的 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/