掘金 后端 ( ) • 2024-04-26 01:54

我们在访问一些 Web3 网站的时候,会经常碰到网页显示我们的地区不在服务范围之内。

比如 unisat 的官网、secwarex 的官网等等。

这期视频不是教大家如何绕开这种限制,相信以大家的聪明才智绕开这种限制轻而易举。

这期视频主要是讲解一下如何实现对 IP 的检测、识别与屏蔽。

为什么要做 IP 检测?

首先我们要明白为什么要做 IP 检测这件事?

因为 Web3 并不是一个在全世界都受欢迎的行业,有些国家和地区对 Web3 并不喜欢,甚至是一种厌恶或者排斥的态度。

有一些业务在一些国家和地区是不可以运营的,否则会触犯到法律。

所以一些项目方会把某些国家和地区的 IP 禁用掉,这样就可以规避法律风险。

规避法律风险是主要的目的,除此之外我们还可以做一些功能,比如针对某个地区的用户提供特殊的内容,比如一些活动之类的。并且可以自动根据 IP 来切换用户所在区域的语言。或者是提供当地的新闻之类的个性化服务。

IP 的格式

IP 是 Internet Protocol 的缩写,也就是互联网协议。

IP 的概念其实很好理解,世界上每一台设备连接到互联网都会有一个唯一的 IP。

IP 目前有两个版本,分别是 IPv4 和 IPv6。

IPv4 是目前使用最广泛的版本,它是由 32 位的二进制组成。通常又分为 4 组,每组占据 1 个字节,也就是 8 位。我们会用 10 进制数来表示 2 进制的字节。

它的格式是这样的: 192.168.1.1。因为 8 位的二进制的范围是 0 到 255,所以每一组最多是三位数,最小是 0,最大是 255。

其实我们不难发现,组成 IPv4 的地址个数是有限的,目前一共只有 43 亿个唯一的 IPv4 地址,实际上全球的设备数量已经超过了 43 亿个。

IPv6 是为了应对 IPv4 地址耗尽而设计的新一代 IP 地址。IPv6 地址由八组四位十六进制数表示。另一个和 IPv4 的区别是,IPv6 的格式不同,它是通过冒号(:)对每一组进行分隔的。比如: 2001:0db8:85a3:0000:0000:8a2e:0370:7334

IPv6 理论上可以提供 2 的 128 次方个唯一地址,完全可以满足全球范围内长期的网络设备增长需求。

通过 IP 识别区域的方式

一般来说,通过 IP 识别区域的方式有三种。

第一种是直接通过专门做站点防护的服务商,在网络层面来做。比较出名的有 Cloudflare,还有 AWS 这类服务商也会提供这种服务。

第二种是使用在线的第三方 IP 服务商。它们通常会提供 API 接口,我们把 IP 传递到它们的接口,就可以得到用户的区域。比较知名的服务商有 MaxMind、ipstack 和 ipinfo.io 等。

第三种就是自建 IP 地理定位服务。我们可以选择购买或者使用开源的地理位置数据库,建立自己的服务来查询 IP 数据。这种方法可以更好的控制数据隐私和响应时间。

技术实现

这里我选用开源的 geoip lite 这个项目来完成自建 IP 地理定位的功能。

然后我会把它应用到 Nodejs 中比较流行的 Web 框架 Nestjs 中,并把它做成一个中间件。

首先来创建 nestjs 项目。

nest new geoip

然后通过 npm run start:dev 来启动项目。

然后访问:http://0.0.0.0:3000/,可以看到 Hello World! 的字样。

然后安装 geoip lite

npm install geoip-lite

我们再继续新建一个中间件,我们的核心逻辑都会放在这个中间件中。

import {
  Injectable,
  NestMiddleware,
  ServiceUnavailableException,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as geoip from 'geoip-lite';

@Injectable()
export class GeoIpMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction): void {
    // 从请求中获取 IP 地址
    const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
    // 如果 IP 地址是字符串类型,使用 geoip-lite 模块获取 IP 地址的地理位置信息
    if (typeof ip === 'string') {
      const geo = geoip.lookup(ip);
      console.debug('geo info:', ip, geo);
      // 如果地理位置信息中的国家是中国,则抛出 ServiceUnavailableException 异常
      if (geo && geo.country === 'CN') {
        throw new ServiceUnavailableException();
      }
    } else {
      // 如果 IP 地址不是字符串类型,抛出 ServiceUnavailableException 异常
      throw new ServiceUnavailableException();
    }

    next();
  }
}

然后把中间件添加到 app.module.ts 中,这样才会生效。

import { MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GeoIpMiddleware } from './geoip.middleware';

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(GeoIpMiddleware)
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

现在我们就可以访问服务来测试结果了。

不过我们不能在本地进行测试,因为本地访问 IP 总是 127.0.0.1,无法获取到真正的地理位置。

所以我把项目部署到服务器上进行测试。

可以看到,我开启代理之后,地理位置检测到是香港,正常访问。

然后我把代理关闭,地理位置检测到的是大陆,所以请求返回了 503,服务不可用。