laravel 微信域名拦截检测
Laravel

前言

因工作要实现微信域名是否被拦截工具,发现网上有好多付费的检测接口,

所以想自己简单的实现下这个功能。最后的效果:www.phpojbk.com/tool


功能实现的思路

获取接口传过来的域名,使用微信提供短链接接口,把域名生成短连接,有人

可能有疑问为什么要把域名生成微信短链接,因为我们都是通过微信浏览器访

问域名才能知道域名是否被拦截,如下图:


普通的浏览器访问域名是不会向微信的服务器发送请求,也就不会出现上图情况,我们要

通过普通的浏览器访问域名,检测是否被微信屏蔽,就要通过访问微信短连接,所以可以

使用GuzzleHttp 发送微信短链接请求,请求会访问微信服务器,最后通过返回的内容来判

断域名是否被微信拦截。


​开始实现功能

我们可以先把要使用的配置和资源库准备好,

首先去微信公众平台申请一个测试号:测试号申请

拿到app_id和app_secret,后面微信短链接要使用。

我们后面实现功能有使用redis,先安装好php的redis扩展,

通过composer安装laravel中使用的redis包:composer require predis/predis

接着composer安装发送http请求的包:composer require guzzlehttp/guzzle


创建ToolController 

首先使用artisan命令创建一个控制器 :php artisan make:controller  Api\ToolController 

<?php

namespace App\Http\Controllers\Api;
use App\Http\Controllers\ApiController;
use App\Service\WeChatService;
use Illuminate\Http\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;

class ToolController extends ApiController { /** * 微信拦截检测 */ public function wechat(Request $request) { $this->validate($request, [ 'domain' => 'required' ], [ 'domain.required' => '域名不能为空' ]); $domain = $request->domain; $fresh = $request->fresh; if (!$this->checkFormat($domain)) { throw new HttpException(422, '域名格式不合法'); } $service = new WeChatService(); $intercept = $service->check($domain, $fresh); return response()->json(['data' => compact('domain', 'intercept')]); } /** * 域名格式检查 */ public function checkFormat($domain) { if (preg_match("/^(([\x{4e00}-\x{9fa5}]|[a-zA-Z0-9-])+(\.[a-z]{2,5})?\.)+([a-z]|[\x{4e00}-\x{9fa5}]){2,10}$/ui", $domain)) { // 去掉-开头的域名 if (substr($domain, 0, 1) != '-' && stripos($domain, '--') === FALSE) { return true; } } return false; } }

创建ApiController

创建一个ToolController继承的基类控制器:php artisan make:controller  ApiController

这个控制器主要是做字段的验证,并抛出字段错误异常。

<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request; use Symfony\Component\HttpKernel\Exception\HttpException; class ApiController extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/**
* api validate 不通过没有具体信息修正
*/
public function validate(Request $request, array $rules, array $messages = [], array $customAttributes = [])
{
$validator = $this->getValidationFactory()->make($request->all(), $rules, $messages, $customAttributes);
if ($validator->fails()) {
throw new HttpException(420,$validator->errors()->first());
}
}
/**
* Throw the failed validation exception.
*/
protected function throwValidationException(\Illuminate\Http\Request $request, $validator) {
throw new HttpException(420,$validator->errors());
}
}

修改Handler

app\Exceptions\Handler.php,重构全局异常抛出

<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\HttpException;
class Handler extends ExceptionHandler
{
    /**
     * A list of the exception types that are not reported.
     *
     * @var array
     */
    protected $dontReport = [
        //
    ];
    /**
     * A list of the inputs that are never flashed for validation exceptions.
     *
     * @var array
     */
    protected $dontFlash = [
        'password',
        'password_confirmation',
    ];
    /**
     * Report or log an exception.
     *
     * This is a great spot to send exceptions to Sentry, Bugsnag, etc.
     *
     * @param  \Exception $exception
     * @return void
     */
    public function report(Exception $exception)
    {
        parent::report($exception);
    }
    /**
     * Render an exception into an HTTP response.
     *
     * @param \Illuminate\Http\Request $request
     * @param Exception $exception
     * @return $this|\Symfony\Component\HttpFoundation\Response
     */
    public function render($request, Exception $exception)
    {
        if ($exception instanceof HttpException) {
            $message = $exception->getMessage();
            $status_code = $exception->getStatusCode();
            $data = compact('message', 'status_code');
            if (config('app.debug')) {
                $trace = $exception->getTraceAsString();
                $data = array_merge($data, compact('trace'));
            }
            return response()->json($data)->setStatusCode($status_code);
        } else {
            return parent::render($request, $exception);
        }
    }
}


创建WechatService

然后创建一个微信域名拦截的业务类:php artisan make:service  WechatService

<?php
namespace App\Service;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
/**
* 微信拦截检测

*/
class WeChatService extends BaseService
{
private $config;
public function __construct()
{
//获取测试号配置
$this->config = [
'app_id' => '', //微信测试的app_id
'app_secret' => '' //微信测试的app_secret
];
}
/**
* 微信域名检测
*/
public function check($domain, $fresh = false, $try = 1)
{
if ((!$intercept = Redis::get('intercept:wecaht:' . $domain)) || $fresh) {
//优先短链接检测
$intercept = $this->checkViaShortUrl($domain, null);
if ($intercept == 0 && $try < 2) {
//失败重试
return $this->check($domain, $fresh, ++$try);
} elseif ($intercept) {
//查询成功,检测结果缓存24小时
Redis::setex('intercept:wecaht:' . $domain, 24 * 60 * 60, $intercept);
}
}
return $intercept;
}
/**
* 微信域名检测
* @return int intercept:0查询失败 1正常 2拦截
*/
private function checkViaShortUrl($domain)
{
$intercept = 0;
$short_url = '';
try {
//获取AccessToken
$access_token = $this->getAccessToken();
//生成短链接
$short_url = $this->getShortUrl($access_token, $domain, $proxy);
//短链接检测
if ($res = $this->checkShortUrl($short_url)) {
$intercept = 1;
} else {
$intercept = 2;
}
Log::info("微信拦截查询成功[域名:$domain][短链接:$short_url]:" . $intercept);
} catch (\Exception $exception) {
Log::info("微信拦截查询失败[域名:$domain][短链接:$short_url]:" . $exception->getMessage());
}
return $intercept;
}
/**
* 获取Access_Token
*/
private function getAccessToken()
{
$app_id = $this->config['app_id'];
if (!$access_token = Redis::get('wechat:access_token:' . $app_id)) {
$client = new Client();
$url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $this->config['app_id'] . '&secret=' . $this->config['app_secret'];
$response = $client->request('GET', $url, [
'connect_timeout' => 3,
'timeout' => 3,
]);
$contents = $response->getBody()->getContents();
$data = json_decode($contents, true);
$access_token = $data['access_token'];
Redis::setex('wechat:access_token:' . $app_id, 7100, $access_token);//微信access_token有效期为7200s
}
return $access_token;
}

/**
* 获取短链接

*/
private function getShortUrl($access_token, $domain)
{
if (!$short_url = Redis::get('short_url:' . $domain)) {
$client = new Client();
$url = "https://api.weixin.qq.com/cgi-bin/shorturl?access_token=$access_token";
$options = [
'json' => [
'action' => 'long2short',
'long_url' => 'http://' . $domain,
],
'connect_timeout' => 3,
'timeout' => 3,
];
$response = $client->request('POST', $url, $options);
$contents = $response->getBody()->getContents();
//AccessToken非法
if (strpos($contents, 'access_token is invalid') !== false) {
Redis::del('wechat:access_token:' . $this->config['app_id']);
throw new \Exception('access_token is invalid');
} else {
$data = json_decode($contents, true);
$short_url = $data['short_url'];
//接口频次限制
$this->setInterfaceLimit();
//短链接缓存90
Redis::setex('short_url:' . $domain, 24 * 60 * 60 * 90, $short_url);
}
}
return $short_url;
}
/**
* 短链接拦截检测
*/
private function checkShortUrl($url)
{
try {
$client = new Client();
$response = $client->request('GET', $url, [
'connect_timeout' => 2,
'timeout' => 2,
]);
$contents = $response->getBody()->getContents();
if (strpos($contents, '已停止访问该网页')) {
return false;
} else {
return true;
}
} catch (\Exception $exception) {
//网站无法访问 = 未拦截
return true;
}
}
/**
* 接口频次限制
* 公众号短链接接口:1000次/
*/
private function setInterfaceLimit()
{
$app_id = $this->config['app_id'];
if (!$count = Redis::get('wechat:account:' . $app_id)) {
//当天有效
$expire = mktime(23, 59, 59) - mktime(date('H'), date('i'), date('s'));
Redis::setex('wechat:account:' . $app_id, $expire, 1);
} else {
Redis::incr('wechat:account:' . $app_id);
}
}
/**
* 微信第三方检测
*/
private function checkViaAdopt($domain)
{
$intercept = 0;
try {
$intercept = $this->checkViaWeiXinClup($domain);
Log::info("微信拦截查询成功[域名:{$domain}]:" . $intercept);
} catch (\Exception $exception) {
Log::info("微信拦截查询失败[域名:{$domain}]:" . $exception->getMessage());
if ($exception->getMessage() == 'Exceeding times') {
try {
$intercept = $this->checkViaDingXsd($domain, $intercept);
Log::info("微信拦截查询成功[域名:{$domain}]:" . $intercept);
} catch (\Exception $e) {
Log::info("微信拦截查询失败[域名:{$domain}]:" . $exception->getMessage());
}
}
}
return $intercept;
}
/**
* weixinclup.com 微信拦截检测
* @param $domain

*/
private function checkViaWeiXinClup($domain)
{
$client = new Client();
$options = [
'connect_timeout' => 5,
'timeout' => 5,
'headers' => ['X-Requested-With' => 'XMLHttpRequest'],
'form_params' => ['url' => $domain]
];

$url = 'http://api.weixinclup.com/index/checkurl.html';
$response = $client->request('POST', $url, $options);
$contents = $response->getBody()->getContents();
$res = json_decode($contents, true);
if ($res && $res['errcode'] === 0) {
$intercept = 2;
} elseif ($res && $res['errcode'] === 1) {
$intercept = 1;
} else {
throw new \Exception('Exceeding times');
}
return $intercept;
}
/**
* dingxsd.com 微信拦截检测
*/
private function checkViaDingXsd($domain)
{
$client = new Client();
$options = [
'connect_timeout' => 5,
'timeout' => 5,
];

$url = 'http://weixin.dingxsd.com/index.php?s=/index/ck_blacklist&domain=' . $domain;
$response = $client->request('GET', $url, $options);
$contents = $response->getBody()->getContents();
$res = json_decode($contents, true);
if ($res && $res['status'] === 0) {
$intercept = 2;
} elseif ($res && $res['status'] === 1) {
$intercept = 1;
} else {
throw new \Exception('Exceeding times');
}
return $intercept;
}
}

最后

最后创建一条在routes/web.php文件路由:Route::post('/api/tools/wechat','Api\ToolController@wechat');

这篇博客只是提供一个思路,zj也可以用更好的方式实现。


转载请附上博客链接:https://www.phpojbk.com/news/14


Responses