Skip to content

为什么有些视频在别的地区看不了?IP 属地做版权分区的原理(Node.js 网关实现) ​

留学生打开爱奇艺,弹出一行字:「因版权限制,该影片暂不支持在当前区域播放」。换个账号没用,挂个加速器有时能看、有时还是不行,海外华人群里天天有人问「为什么」。回答之前得先明白一件事:不是视频平台故意为难你,是版权合同里写了「本片仅在特定地区授权」,平台不拦你,版权方会找它麻烦。

版权分区(地理围栏)是视频、音乐、游戏内容平台的常规操作,判断依据就是请求来源的 IP 归属地。判定链不复杂:请求进来 → 拿 IP 查归属地 → 匹配授权区域 → 不在授权区就返回「地区限制」。这篇文章把这条链拆开讲,附一段 Node.js 网关实现,重点说清楚误判是怎么来的、平台侧怎么兜底。

一、版权分区到底发生了什么 ​

一部剧的版权是按地区拆着卖的:大陆独播、港台独播、东南亚独播、欧美流媒体拿全球(除大陆)……发行商把每个地区的独家播放权分开授权,价格不同、合同条款不同。平台买了哪几个地区,就在哪几个地区开放播放。

平台怎么知道访问者在哪个地区?看出口 IP 的归属地。请求打进来,目前台拿到的是用户的公网 IP,查一下归属地,对照授权地区表,命中就放行,不命中就拦。整条链路里唯一的技术活就是「IP 归属地查得准不准」——这也是误判的来源。

二、为什么经常判错 ​

IP 归属地本身是「网络出口位置」,不是「人所在位置」,两回事。常见错位有四种:

  • CDN / 代理回源:用户走企业代理或机场,出口 IP 落在别的城市,跟着判错;
  • 手机漫游:人在国外,手机还连着国内运营商的漫游出口,IP 归属地显示国内,本该拦截的放了;
  • 运营商 DHCP 漂移:家用宽带的出口 IP 可能在邻省节点,省份边界用户经常被误判;
  • 机房出口:公司在某地有服务器,员工统一从机房出口上网,整个办公室被当成那个城市的人。

所以正规平台的版权判定不会只查一次:首查 IP 归属地,拿不到明确结论(归属地在授权区边缘、或 IP 是机房代理)就降级放行或走人工申诉。把「查得严」和「体验好」平衡好,靠的是兜底策略。

三、Node.js 网关实现 ​

判定放网关层而不是每个业务接口里,一次判定全局生效,换授权区域只改配置。Express 中间件写法:

javascript
const express = require('express');
const app = express();

// 授权区域表:节目 ID -> 允许的国家/地区(country_code 列表)
const LICENSES = {
  'drama-001': ['CN'],                 // 仅中国大陆
  'drama-002': ['CN', 'HK', 'MO'],     // 大中华区
  'music-001': ['CN', 'SG', 'MY'],     // 大陆 + 东南亚部分
};

// IP 归属地缓存,避免重复打接口
const CACHE = new Map();
let lastCall = 0;

async function queryGeo(ip) {
  if (CACHE.has(ip)) return CACHE.get(ip);
  const now = Date.now();
  if (now - lastCall < 1100) {          // 免费版 60 次/分钟,留余量
    await new Promise(r => setTimeout(r, 1100 - (now - lastCall)));
  }
  try {
    const res = await fetch(`https://ip9.com.cn/get?ip=${ip}`);
    const d = await res.json();
    lastCall = Date.now();
    if (d.ret !== 200) return null;
    CACHE.set(ip, d.data);
    return d.data;
  } catch {
    return null;
  }
}

// 版权地区判定中间件:/play/:id 播放类接口统一走这里
async function geoGuard(req, res, next) {
  const id = req.params.id;
  const allowed = LICENSES[id];
  if (!allowed) return next();          // 无版权限制的节目直接放行

  const ip = req.headers['x-forwarded-for']?.split(',')[0].trim()
      || req.socket.remoteAddress;
  const geo = await queryGeo(ip);

  if (!geo) {
    // 查询失败(非法 IP / 限速):安全策略默认拒绝,避免把没授权的地区放进来
    return res.status(403).json({ code: 403, msg: `因版权限制,该影片暂不支持在当前区域播放` });
  }
  if (allowed.includes(geo.country_code)) return next();

  // 边缘兜底:归属地在授权区临界省份 + 非代理特征,降级放行一次并记录
  const risky = (geo.big_area === '华东' || geo.area_code === '31');
  if (risky && !isProxyLike(ip)) {
    console.log(`[geo] 边缘放行 ${ip} -> ${geo.prov} / ${geo.city},节目 ${id}`);
    return next();
  }
  return res.status(403).json({
    code: 403,
    msg: `因版权限制,该影片暂不支持在当前区域播放(判断依据:IP 归属地 ${geo.country}/${geo.prov})`,
  });
}

// 简单代理特征:机房/企业类型由 VIP 接口的 ip_type 提供,此处用端口特征兜底
function isProxyLike(ip) {
  return CACHE.get(ip)?.ip_type === 'IDC'; // 有 ip_type 字段(VIP)时生效
}

app.get('/play/:id', geoGuard, (req, res) => {
  res.json({ url: `https://cdn.example.com/stream/${req.params.id}.m3u8` });
});

app.listen(3000);

两个细节值得说:一是提示语把「判断依据」也回了客户端(IP 归属地 美国/加州),用户看到至少知道为什么被拦,投诉量会少很多;二是查询失败默认拒绝而不是放行——版权场景宁可误伤,不能把没授权的地区漏进去。

四、落地注意事项 ​

  • 判定必须带兜底:纯 IP 判定误伤率摆在那(漫游、代理、边界省份),平台要留人工申诉通道,申诉入口在提示页上直接给;
  • 授权表配置化:LICENSES 这种写死在代码里只适合演示,线上应该放配置中心,版权合同每月都有变动,改代码上线太慢;
  • 别碰「解锁」:用户侧有各种解锁工具,平台能做的只有把判定做严、把误判兜底做好。写绕过方案既违服务条款也不合法,本文不做展开;
  • 隐私合规:版权判定只需要国家/地区维度的结果,后端不要存用户 IP 明细,日志里记节目 ID + 判定结果即可。

总结 ​

「为什么看不了」的答案很简单:你的出口 IP 归属地不在版权授权区。链路就是请求 IP → 归属地查询 → 授权表匹配 → 拒绝或放行,Node.js 网关里一个中间件就能实现。IP 归属地查询用 https://ip9.com.cn/get?ip=xxx(免费版返回国家/省市/运营商,够版权分区用;判代理特征可以用 VIP 版的 ip_type),记得加缓存和限速。判定兜底比判定本身更重要:提示语带判断依据、留申诉通道、查询失败默认拒绝,这三件事做好,版权方的合同守住了,用户的投诉也压得住。官网:https://www.ip9.com.cn