作为程序员一定要保持良好的睡眠,才能好编程

IP地址应用场景及ip转换详细地址采取RPC调用

发布时间:2019-04-09


ip转换成详细地址,在项目中经常使用到。


IP地址应用场景

访问来源统计

小流量线上验证

黑白名单


以上是一些应用场景。


我们要把ip转换成详细地址,如何转换? 借助api来实现。


ipip.net 



ip.png



这是ipip提供的免费api:


https://www.ipip.net/product/client.html

ip2.png



聚合数据


https://www.juhe.cn/docs/api/id/1


ip3.png



有了以上两家提供的ip转换,这两家做的都是非常不错的,每天都有免费限额。


我这里使用ipip.net 这家企业的ip服务,每天使用限额是 1000 次,对于一个小型的项目来说,是够用的了。




程序开发


为了开发微服务,采用RPC的形式进行开发转换。


为了更大程度的调用,采用Rpc远程过程调用的方式进行开发部署。


项目采用 yaf + yar 这两个框架。



1、注册接口

IpController.php

defined('APP_PATH') OR exit('No direct script access allowed');

class IpController extends BaseController {

  public function indexAction() {
    $serivce = new Yar_Server(new IpService());
    $serivce->handle();
  }
}


2、ip转换程序实现


IpService.php

defined('APP_PATH') OR exit('No direct script access allowed');

class IpService extends RpcService {


  public function _init() {
    parent::_init(); // TODO: Change the autogenerated stub
  }

  /**
   * 获取ip 地址相关信息
   * @param string $ip ip地址
   * @param int $type 获取类型 1通过调用接口  2 通过url解析dom 没有限制 推荐2
   * @return array|bool
   */
  public function ip($rpcData) {
    $ip = $rpcData['ip'];
    $type = isset($rpcData['type']) && in_array($rpcData['type'], [1, 2]) ? $rpcData['type'] : 1;

    $methodRules = [
      'ip' => 'require|ip'
    ];

    $data = [
      'ip' => $ip
    ];

    $validate_message = [
      'ip' => 'ip不能为空或ip格式不正确'
    ];

    $validate = $this->validate($methodRules, $data, $validate_message);
    if (!$validate['result'])
      return $validate;

    $result = $this->ipModel->getIpInfo($ip);
    //将发送状态返回给用户
    return $result;
  }

}


父类 RpcService.php

<?php
/**
 * Created by PhpStorm.
 */

defined('APP_PATH') OR exit('No direct script access allowed');

class RpcService extends BaseService {

  protected $checkSignResult = TRUE;

  public $secrectData;

  public $secrect;

  protected $allowIp = ['*']; //允许所有的ip访问,可以指定一个完整ip


  public function _init() {
    parent::_init(); // TODO: Change the autogenerated stub

    $this->checkSignResult && $this->_checkIp();
    $this->checkSignResult && $this->_check();
  }

  protected function validate($rules, $data, $message = []) {
    $result = validate($rules, $data, $message);
    if ($result === TRUE) {
      return ['result' => TRUE];
    } else {
      return $result;
    }
  }

  /**
   * 排序字符
   * @param $data
   * @return string
   */
  private final function _data2str($data) {
    $str = '';
    ksort($data);
    foreach ($data as $key => $val) {
      if (is_array($val)) {
        $str .= '&' . $this->_data2str($val);
      } else {
        $str .= $key . '=' . $val . '&';
      }
    }
    return rtrim($str, '&');
  }

  /**
   * sign 生成
   * @param $data
   * @return string
   */
  public final function _sign($data) {
    //由于客户端本地使用了__call 这个方法,加密的字符串是 array([0]=>data); 因此下面也按照这个样子加密。保持一致
    return md5($this->secrect . '_' . $this->_data2str([$data]));
  }


  private function _checkIp() {
    if (count($this->allowIp) == 1 && ($this->allowIp)[0] === '*') {

      debugMessage('RPC ip 验证通过   允许所有IP访问');
      return;
    } else {
      $clientIP = getClientIP();
      if (in_array($clientIP, $this->allowIp)) {
        debugMessage('RPC ip 验证通过 客户端访问ip是:' . $clientIP);
        return;
      } else
        $this->checkSignResult = FALSE;
    }
  }

  protected function signErr() {
    return rpcdata(400, 'sign签名验证错误');
  }

  /**
   * 验证sign是否合法
   */
  private function _check() {
    //只有post时候才请求

    if (isset($_SERVER['HTTP_SIGN_NOCHECK']) && $_SERVER['HTTP_SIGN_NOCHECK'] == 1)
      return TRUE;

    if (isPost()) {
      //我是针对于rpc服务的,返回后系统就不在执行了。
      if (isset($_GET['secrect']) && $_GET['secrect'] != '') {
        $secrectData = unserialize(AESDecrypt($_GET['secrect'], COOKIE_KEY, TRUE));

        if (!$secrectData) {
          $this->checkSignResult = FALSE;
          debugMessage('AES 解密失败');
          return FALSE;
        }
        debugMessage('AES 解密成功');

        //得到用户受保护的字符串
        isset($secrectData['secrect']) && $this->secrect = $secrectData['secrect'];

        $this->secrectData = $secrectData;

        debugMessage('secrectData:' . jsonencode($this->secrectData));

        $loginUser = LOGIN_USER;
        if (isset($secrectData['userid']) && isset($loginUser[$secrectData['userid']])) {
          //判断用户和密码是否符合,判断时间是否允许
          $checkUser = $loginUser[$secrectData['userid']];

          if ($checkUser['username'] == $secrectData['username'] && $checkUser['password'] == $secrectData['password']) {

            if (time() - $secrectData['expiretime'] < MESSAGE_EXPIERTIME) {
              debugMessage('checkSignResult 验证通过');
              return;
            }
          }
        }

      }
      $this->checkSignResult = FALSE;
    }
  }

}



3、调用接口数据返回

<?php
/**
 * 获取ip详细信息 模型
 */
defined('APP_PATH') OR exit('No direct script access allowed');


class IpModel extends BaseModel {


  public function _init() {
    parent::_init(); // TODO: Change the autogenerated stub
    /**
     * https://www.ipip.net/support/api.html
     *
     * 免费接口(限速每天1000次,仅供测试)
     * 如若需要可以使用收费版本
     */
    $this->_host = 'http://freeapi.ipip.net/';
  }

  /**
   * 发送前准备
   * @param string $url
   * @param array|string $data
   * @return array|string
   */
  public function fetchBefore($url, $data) {

    return parent::fetchBefore($url, $data);
  }

  /**
   * 返回时的处理
   * @param string $url
   * @param array|bool $data
   * @return mixed
   */
  public function fetchAfter($url, $data) {

    return parent::fetchAfter($url, $data);
  }

  /**
   * 返回成功后,处理部分业务逻辑
   * @param $data
   * @return array
   */
  public function fetchFinish($data) {
    $result = [];

    if ($data == 'not found') {
      return [];
    } else if (strpos($data, '[') !== FALSE && strpos($data, ']')) {

      $d = explode(',', str_replace(['[', ']', '"', "\n"], '', $data));
      //            国家      省份        城市     详情     供应商
      $dataName = ['china', 'province', 'city', 'info', 'operator'];

      if (is_array($d)) {
        $data = array_combine($dataName, $d);
        debugMessage("ip解析成功:" . var_export($data, TRUE));
      }
    }
    return $data;
  }

  /**
   * 根据ip获取相关信息
   * @param $ip
   * @return mixed
   */
  public function getIpInfo($ip) {
    $result = $this->send($this->_host . $ip, [], 'get');
    $ip = ip_long($ip);
    $this->insert(['ip' => $ip, 'method' => 'getIpInfo_interface', 'ip_info' => jsonencode($result)]);
    return $result;
  }




}


通过上面的几步,服务器端的代码已经注册完毕。



客户端是如何与服务器端通讯的呢?


我在客户端也进行了一定量的封装。例如:如图所示





客户端与服务器端如何鉴权呢?

通过将重要的参数 封装成一个数组,序列化   、AES加密   通过 url 中的一个参数  secrect  进行密文传送。


这是一个非常重要的信息,如果没有此信息 则认为是 无授权用户,不能访问的。


服务器获取到密文后 ,AES 解密后  反序列化 得到数组,与服务器端进行验证。 验证一致,则认为 是有效用户。