上篇博文

重写了下,理清了思路。顺便也是因为其验证换了,所以原来的处理就失效了

一、实现思路

工信部 ICP 备案查询的核心流程分为两步:

  1. 获取验证图片:向工信部接口请求滑块验证所需的大图 / 小图资源,同时获取鉴权所需的 Token、UUID 等参数;
  2. 滑块验证 + 查询备案:用户完成滑块验证后,将验证坐标提交后端,后端携带验证信息调用备案查询接口,最终返回备案数据。

系统整体架构分为:

  • 前端:负责用户输入、滑块验证交互、请求发送与结果展示(HTML+JavaScript);
  • 后端:负责对接工信部接口、处理鉴权逻辑、封装查询方法(PHP)。

非核心的页面样式、布局(如index.html中的 UI 排版)仅作为 Demo 验证,本文不做过多赘述,重点讲解核心业务逻辑。

二、后端核心逻辑(PHP)

后端核心代码集中在icpbeian.php(核心类)和api.php(接口层),前者封装了对接工信部接口的所有逻辑,后者作为前端与核心类的桥梁。

2.1 核心类Webquery

该类封装了 Cookie 获取、Token 生成、验证图片获取、滑块验证、备案查询等核心方法,是整个系统的核心。

2.1.1 初始化与基础配置
class Webquery
{
    // 备案查询的参数模板(不同serviceType对应不同查询类型)
    private $typj = [
        0 => '{"pageNum": "", "pageSize": "", "unitName": "", "serviceType": 1}',
        1 => '{"pageNum": "", "pageSize": "", "unitName": "", "serviceType": 6}',
        2 => '{"pageNum": "", "pageSize": "", "unitName": "", "serviceType": 7}',
        3 => '{"pageNum": "", "pageSize": "", "unitName": "", "serviceType": 8}'
    ];
    // 基础请求头(模拟浏览器)
    private $cookieHeaders = [
        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...'
    ];
    // 工信部核心接口地址
    private $home = 'https://beian.miit.gov.cn/';
    private $url = 'https://hlwicpfwc.miit.gov.cn/icpproject_query/api/auth';
    private $getCheckImage = 'https://hlwicpfwc.miit.gov.cn/icpproject_query/api/image/getCheckImagePoint';
    private $checkImage = 'https://hlwicpfwc.miit.gov.cn/icpproject_query/api/image/checkImage';
    private $queryByCondition = 'https://hlwicpfwc.miit.gov.cn/icpproject_query/api/icpAbbreviateInfo/queryByCondition';
    
    // 构造方法初始化curl(忽略SSL验证,适配工信部接口)
    public function __construct()
    {
        $this->session = curl_init();
        curl_setopt($this->session, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->session, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($this->session, CURLOPT_SSL_VERIFYHOST, false);
    }
}
2.1.2 鉴权前置:Cookie 与 Token 获取

工信部接口需要先获取__jsluid_s Cookie 和 Token 才能正常请求,这是对接的第一个关键节点:

// 获取__jsluid_s Cookie
private function getCookie()
{
    curl_setopt($this->session, CURLOPT_URL, $this->home);
    curl_setopt($this->session, CURLOPT_HTTPHEADER, $this->formatHeaders($this->cookieHeaders));
    curl_setopt($this->session, CURLOPT_HEADER, true);
    curl_setopt($this->session, CURLOPT_NOBODY, true);

    $response = curl_exec($this->session);
    preg_match('/__jsluid_s=([0-9a-z]{32})/', $response, $matches);
    return $matches[1] ?? '';
}

// 获取Token(基于时间戳+md5生成authKey)
private function getToken()
{
    $timeStamp = round(microtime(true) * 1000);
    $authSecret = 'testtest' . $timeStamp;
    $authKey = md5($authSecret);

    $cookie = $this->getCookie();
    $this->baseHeader['Cookie'] = '__jsluid_s=' . $cookie;
    
    curl_setopt($this->session, CURLOPT_URL, $this->url);
    curl_setopt($this->session, CURLOPT_POST, true);
    curl_setopt($this->session, CURLOPT_POSTFIELDS, ['authKey' => $authKey, 'timeStamp' => $timeStamp]);
    $response = curl_exec($this->session);
    $data = json_decode($response, true);
    return $data['params']['bussiness'] ?? '';
}
2.1.3 获取滑块验证图片

Token 获取成功后,请求工信部接口获取滑块验证的大图 / 小图,并返回前端所需的鉴权参数:

public function checkImg()
{
    $this->token = $this->getToken();
    try {
        // 生成客户端唯一标识clientUid
        $data = $this->getClientUid();
        $clientUid = json_decode($data, true)['clientUid'];

        $headers = $this->baseHeader;
        $headers['Content-Length'] = strlen($data);
        $headers['Token'] = $this->token;
        $headers['Content-Type'] = 'application/json';

        // 请求验证图片接口
        curl_setopt($this->session, CURLOPT_URL, $this->getCheckImage);
        curl_setopt($this->session, CURLOPT_POST, true);
        curl_setopt($this->session, CURLOPT_POSTFIELDS, $data);
        curl_setopt($this->session, CURLOPT_HTTPHEADER, $this->formatHeaders($headers));

        $response = curl_exec($this->session);
        $res = json_decode($response, true);

        // 提取核心参数(返回给前端)
        return [
            "big_image" => $res['params']['bigImage'] ?? '',
            "small_image" => $res['params']['smallImage'] ?? '',
            "p_uuid" => $res['params']['uuid'] ?? '',
            "token" => $this->token,
            "secretKey" => $res['params']['secretKey'] ?? '',
            "clientUid" => $clientUid,
            "height" => $res['params']['height'] ?? 0,
            'cookie' => $this->baseHeader2['Cookie'] ?? '',
            'success' => true
        ];
    } catch (\Exception $e) {
        return false;
    }
}
2.1.4 滑块验证 + 备案查询

用户完成滑块拖动后,前端提交验证坐标,后端携带坐标调用验证接口,验证通过后查询备案信息:

public function icp_auth($offsetX, $p_uuid, $token, $secretKey, $clientUid, $domain, $cookie_c)
{
    $client = new Client([
        'verify' => false,
        'headers' => [
            'User-Agent' => 'Mozilla/5.0 ...',
            'Cookie' => $cookie_c,
            'Token' => $token
        ]
    ]);
    try {
        // 1. 提交滑块验证坐标
        $verifyData = [
            "key" => $p_uuid,
            "value" => (string)intval($offsetX)
        ];
        $verifyResponse = $client->post($this->checkImage, ['json' => $verifyData]);
        $resData = json_decode($verifyResponse->getBody(), true);
        if (!$resData['success']) {
            return ['code' => 201, 'error' => '验证码验证失败'];
        }

        // 2. 验证通过后,调用备案查询接口
        $sign = $resData["params"] ?? '';
        $info = json_decode($this->typj[0], true);
        $info['unitName'] = $domain;
        $infoJson = json_encode($info, JSON_UNESCAPED_UNICODE);
        
        $queryResponse = $client->post($this->queryByCondition, [
            'body' => $infoJson,
            'headers' => [
                'Content-Length' => strlen($infoJson),
                'Sign' => $sign,
                'Token' => $token,
                'Uuid' => $p_uuid
            ]
        ]);

        return json_decode($queryResponse->getBody(), true);
    } catch (GuzzleException $e) {
        return ["code" => 122, "msg" => "查询失败: " . $e->getMessage()];
    }
}

2.2 接口层

作为前端与核心类的桥梁,接收前端请求并分发处理(分 step1/step2):

require_once '../vendor/autoload.php';
require('icpbeian.php');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $step = $_POST['step'] ?? 1;
    $domain = $_POST['domain'] ?? '';
    $myicp = new Webquery();
    
    // step1:获取验证图片
    if ($step == 1) {
        if (empty($domain)) {
            echo json_encode(["code" => 101, "msg" => "参数错误,请指定domain参数"], JSON_UNESCAPED_UNICODE);
            exit;
        }
        $result = $myicp->icp_search($domain); // 内部调用checkImg()
        echo json_encode($result, JSON_UNESCAPED_UNICODE);
    } 
    // step2:滑块验证+查询备案
    elseif ($step == 2) {
        $sliderOffset = $_POST['sliderOffset'] ?? null;
        $p_uuid = $_POST['p_uuid'] ?? null;
        $token = $_POST['token'] ?? null;
        // 校验参数后调用icp_auth()
        $result = $myicp->icp_auth($sliderOffset, $p_uuid, $token, $secretKey, $clientUid, $domain, $cookie_c);
        echo json_encode($result, JSON_UNESCAPED_UNICODE);
    }
}

三、前端核心交互(JavaScript)

前端核心逻辑在icpget.js,重点是滑块验证的实现与请求流程的封装。

3.1 初始化查询流程

用户输入域名后,发起第一步请求,获取验证图片并展示滑块验证界面:

function queryDomain() {
    const domain = document.getElementById('domain').value.trim();
    if (!domain) {
        showError('请输入要查询的域名');
        return;
    }
    // 展示加载状态,隐藏其他状态
    document.getElementById('resultContainer').classList.remove('hidden');
    document.getElementById('loadingState').classList.remove('hidden');
    
    // 调用step1接口,获取验证图片
    fetch('yhicp/api.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: `domain=${encodeURIComponent(domain)}&step=1`
    })
    .then(response => response.json())
    .then(data => {
        if (data.success && data.big_image && data.small_image) {
            showImages(data); // 初始化验证图片和滑块
        } else {
            showError(data.msg || '未查询到该域名的备案信息');
        }
    })
    .catch(error => {
        showError('查询失败,请稍后再试');
    });
}

3.2 滑块验证实现

这是前端最核心的交互逻辑,实现滑块拖动、坐标计算、验证提交:

function initSlider() {
    var slideBtn = document.getElementById('slideBtn');
    var slideTrack = document.getElementById('slideTrack');
    var smallImage = document.getElementById('smallImage');
    var isDragging = false;
    var startClientX = 0;
    var startLeft = 0;
    var currentLeft = 0;
    // 计算最大可拖动距离(大图宽度-小图宽度)
    var maxOffset = bigImage.naturalWidth - smallImage.naturalWidth;
    var trackMaxOffset = slideTrack.offsetWidth - slideBtn.offsetWidth;

    // 鼠标按下/触摸开始事件
    slideBtn.addEventListener('mousedown', onStart);
    slideBtn.addEventListener('touchstart', onStart, { passive: false });
    // 鼠标移动/触摸移动事件
    document.addEventListener('mousemove', onMove);
    document.addEventListener('touchmove', onMove, { passive: false });
    // 鼠标松开/触摸结束事件
    document.addEventListener('mouseup', onEnd);
    document.addEventListener('touchend', onEnd);

    function onStart(e) {
        e.preventDefault();
        isDragging = true;
        startClientX = e.type.indexOf('touch') >= 0 ? e.touches[0].clientX : e.clientX;
        startLeft = currentLeft;
    }

    function onMove(e) {
        if (!isDragging) return;
        e.preventDefault();
        var clientX = e.type.indexOf('touch') >= 0 ? e.touches[0].clientX : e.clientX;
        var dx = clientX - startClientX;
        var newTrackLeft = startLeft + dx;
        // 限制拖动范围(0 ~ trackMaxOffset)
        newTrackLeft = Math.max(0, Math.min(newTrackLeft, trackMaxOffset));
        // 计算小图的偏移量(滑块拖动距离与验证图偏移的比例转换)
        var ratio = maxOffset / trackMaxOffset;
        var imageLeft = Math.round(newTrackLeft * ratio);
        
        // 更新滑块、填充条、小图的位置
        currentLeft = newTrackLeft;
        slideBtn.style.left = newTrackLeft + 'px';
        slideFill.style.width = (newTrackLeft + btnWidth / 2) + 'px';
        smallImage.style.left = imageLeft + 'px';
    }

    function onEnd(e) {
        if (!isDragging) return;
        isDragging = false;
        // 提交滑块偏移量进行验证
        document.getElementById('sliderOffset').value = parseInt(smallImage.style.left) || 0;
        submitSlide(); // 调用step2接口
    }
}

3.3 提交滑块验证结果

将滑块偏移量、鉴权参数提交后端,验证通过后展示备案信息:

function submitSlide() {
    var formData = new FormData(document.getElementById('coordinateForm'));
    formData.append('step', 2);
    document.getElementById('loadingState').classList.remove('hidden');
    
    fetch('yhicp/api.php', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        if (data.params && data.params.list && data.params.list.length > 0) {
            showResult(data.params.list[0]); // 展示备案信息
        } else {
            showError(data.msg || '验证失败,请刷新页面重试');
        }
    })
    .catch(error => {
        showError('查询失败,请稍后再试');
    });
}

四、总结

  1. 接口鉴权适配:工信部接口依赖__jsluid_s Cookie、Token、UUID 等参数,需严格模拟浏览器请求头和参数生成规则;
  2. 滑块验证逻辑:前端需实现拖动范围限制、滑块与验证图的坐标比例转换,确保偏移量精准;
  3. 跨端兼容:滑块验证同时支持鼠标和触摸事件,适配移动端与 PC 端;
  4. 异常处理:前后端均需处理网络异常、验证失败、参数缺失等场景,提升用户体验

五、项目仓库

完整代码已开源至 Gitee,可直接下载部署验证Gitee仓库https://gitee.com/bei-mio/miit-icp-query

六、扩展与优化方向

  1. 增加验证码自动识别(如接入第三方打码平台),实现免手动滑块验证;
  2. 优化前端交互,增加加载动画、错误提示的精细化展示;
  3. 增加备案信息缓存,减少重复请求工信部接口;
  4. 适配更多工信部接口参数,支持企业 / 个人等多类型备案查询
Logo

一站式 AI 云服务平台

更多推荐