在 Yii 2 中,基于 SAM-IT/yii2-urlsigner 实现安全的 URL 签名和验证

1、微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权、创建微博的微连接的网页应用的用户),Rap 文档,如图1

图1

2、由于渠道发布接口为底层服务,因此,所有接口皆允许游客访问,进而导致通过此网址,任务微博帐号皆可以创建对应的微博的微连接的网页应用的用户,现阶段,准备基于 yii2-urlsigner 实现安全的 URL 签名和验证,以防止此种情况的出现

3、在 Rancher 中的环境变量配置

 CHANNEL_PUB_API_CFG_NGINX_API_SERVER_NAME=wjdev2.chinamcloud.com # Nginx 服务器名称(接口域名、建议入方向仅支持内网)
 CHANNEL_PUB_API_CFG_NGINX_API_LISTEN=80 # Nginx 服务器监听端口(接口域名、建议入方向仅支持内网)
 CHANNEL_PUB_API_CFG_NGINX_API_SCHEME=https # Nginx 服务器 URI 方案的协议(接口域名、建议入方向仅支持内网、范围:[http, https, 空字符串])
 CHANNEL_PUB_API_CFG_NGINX_AUTH_SERVER_NAME=wjdev2.chinamcloud.com # Nginx 服务器名称(授权域名、建议入方向可支持外网)
 CHANNEL_PUB_API_CFG_NGINX_AUTH_LISTEN=81 # Nginx 服务器监听端口(授权域名、建议入方向可支持外网)
 CHANNEL_PUB_API_CFG_NGINX_AUTH_SCHEME=https # Nginx 服务器 URI 方案的协议(授权域名、建议入方向可支持外网、范围:[http, https, 空字符串])
 CHANNEL_PUB_API_CFG_AUTH_HOST_INFO=https://wjdev2.chinamcloud.com:8662 // 渠道发布接口授权的 HOME URL
 CHANNEL_PUB_API_CFG_AUTH_BASE_URL= // 渠道发布接口授权的 BASE URL

4、基于 Composer 安装 SAM-IT/yii2-urlsigner,网址:https://github.com/SAM-IT/yii2-urlsigner ,执行命令

PS E:\wwwroot\channel-pub-api> composer require --prefer-dist SAM-IT/yii2-urlsigner
Using version ^2.0 for sam-it/yii2-urlsigner
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing sam-it/yii2-urlsigner (v2.0.0): Downloading (100%)
  - Installing intervention/image (2.4.2): Downloading (100%)
intervention/image suggests installing intervention/imagecache (Caching extension for the Intervention Image library)
Writing lock file
Generating autoload files

5、实现接口:获取微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权、创建微博的微连接的网页应用的用户)的链接,编辑 \weibo\rests\oauth2\AuthorizeAction.php

<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
namespace weibo\rests\oauth2;

use Yii;
use weibo\models\Channel;
use weibo\models\ChannelType;
use weibo\models\WeiboWeiboConnectWebAppUserCreateParam;
use weibo\services\ChannelService;
use weibo\services\ChannelTypeService;
use weibo\services\WeiboWeiboConnectWebAppService;
use SamIT\Yii2\UrlSigner\UrlSigner;
use yii\base\Model;
use yii\helpers\Url;
use yii\web\NotFoundHttpException;
use yii\web\UnprocessableEntityHttpException;

/**
 * 获取微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权、创建微博的微连接的网页应用的用户)的链接
 *
 * 1、请求参数列表
 * (1)source:必填,来源,xContent:内容库;vms:视频管理系统;scms:内容管理系统;spider:自媒体;channel-pub-api:渠道发布接口
 * (2)source_uuid:必填,来源ID(UUID)
 * (3)user_name:必填,用户名称
 * (4)permission:必填,权限,1:同步;2:发布;3:同步与发布
 * (5)status:可选,状态,0:禁用;1:启用,默认:1
 * (6)redirect_uri:可选,微博的微连接的网页应用授权后重定向的回调链接,其值需要 Base64 编码,默认:渠道发布接口授权成功提示页面的网址
 *
 * 2、输入数据验证规则
 * (1)存在性:基于代码,weibo:微博查询资源(渠道),如果不存在,则返回失败
 * (2)比对(status !== 1):判断状态(渠道),如果未启用,则返回失败
 * (3)存在性:基于代码,weibo_weibo_connect_web:微博的微连接的网页应用查询资源(渠道的类型),如果不存在,则返回失败
 * (4)比对(status !== 1):判断状态(渠道的类型),如果未启用,则返回失败
 * (5)存在性:基于 App ID 查询资源(微博的微连接的网页应用),如果不存在,则返回失败
 * (6)比对(status !== 1):判断状态(微博的微连接的网页应用),如果未启用,则返回失败
 * (7)必填:source、source_uuid、user_name、permission
 * (8)默认值(1):status
 * (9)默认值(渠道发布接口授权成功提示页面的网址):redirect_uri
 * (10)网址(Base64 解码):redirect_uri
 *
 * 3、操作数据
 * (1)URL 签名
 * (2)基于渠道发布接口授权配置,返回授权链接
 *
 * For more details and usage information on AuthorizeAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */class AuthorizeAction extends Action
{
    /**
     * @var string the scenario to be assigned to the new model before it is validated and saved.
     */    public $scenario = Model::SCENARIO_DEFAULT;
    /**
     * @var string the name of the view action. This property is need to create the URL when the model is successfully created.
     */    public $viewAction = 'view';

    /**
     * Authorizes a new model.
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \Throwable
     */    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

        // 基于代码查找状态为启用的数据模型(渠道)
        $channelEnabledItem = ChannelService::findModelEnabledByCode(Channel::CODE_WEIBO);

        // 基于代码查找状态为启用的数据模型(渠道的类型)
        $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_WEIBO_WEIBO_CONNECT_WEB);

        // 基于 App ID 查找状态为启用的数据模型(微博的微连接的网页应用)
        $weiboWeiboConnectWebAppEnabledItem = WeiboWeiboConnectWebAppService::findModelEnabledByAppId(Yii::$app->params['weiboAuth']['weiboConnectWebApp']['appId']);

        $request = Yii::$app->request;
        $get = $request->get();
        $base64DecodeRedirectUri = base64_decode($request->get('redirect_uri', base64_encode(Yii::$app->params['auth']['hostInfo'] . Yii::$app->params['auth']['baseUrl'])), true);

        // Base64 解码失败
        if ($base64DecodeRedirectUri === false) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', 244006)));
        }

        $get['redirect_uri'] = $base64DecodeRedirectUri;

        /* 微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权)参数 */        $weiboWeiboConnectWebAppUserCreateParam = new WeiboWeiboConnectWebAppUserCreateParam();
        // 把请求数据填充到模型中
        if (!$weiboWeiboConnectWebAppUserCreateParam->load($get, '')) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', 244003)));
        }
        // 验证模型
        if (!$weiboWeiboConnectWebAppUserCreateParam->validate()) {
            $weiboWeiboConnectWebAppUserCreateParamResult = self::handleValidateError($weiboWeiboConnectWebAppUserCreateParam);
            if ($weiboWeiboConnectWebAppUserCreateParamResult['status'] === false) {
                throw new UnprocessableEntityHttpException($weiboWeiboConnectWebAppUserCreateParamResult['message']);
            }
        }

        $pathInfo = '/weibo-oauth2/authorize';
        $base64EncodeRedirectUri = base64_encode($base64DecodeRedirectUri);

        // URL 签名
        $urlSigner = new UrlSigner([
            'secret' => Yii::$app->params['urlSigner']['secret']
        ]);
        $route = [
            $pathInfo,
            'group_id' => $get['group_id'],
            'source' => $get['source'],
            'source_uuid' => $get['source_uuid'],
            'user_name' => $get['user_name'],
            'permission' => $get['permission'],
            'status' => $weiboWeiboConnectWebAppUserCreateParam->status,
            'redirect_uri' => $base64EncodeRedirectUri,
        ];

        // 是否允许添加额外参数:否,将到期时间设置为 1 分钟
        $urlSigner->signParams($route, false, (new \DateTime())->add(new \DateInterval('PT' . Yii::$app->params['urlSigner']['timeOut'] . 'M')));

        // 包含 host info 的整个 URL
        $scheme = empty(Yii::$app->params['nginxAuthScheme']) ? true : Yii::$app->params['nginxAuthScheme'];
        $route[0] = Yii::$app->params['auth']['hostInfo'] . Yii::$app->params['auth']['baseUrl'] . $pathInfo;
        $data['url'] = Url::to($route, $scheme);

        return ['code' => 10000, 'message' => Yii::t('success', '144008'), 'data' => $data];
    }
}

6、接口响应如下:

{
    "code": 10000,
    "message": "获取微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权、创建微博的微连接的网页应用的用户)的链接成功",
    "data": {
        "url": "http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A"
    }
}

7、编辑 \frontend\controllers\WeiboOauth2Controller.php,即 微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权、创建微博的微连接的网页应用的用户)

<?php
/**
 * Created by PhpStorm.
 * User: Qiang Wang
 * Date: 2018/12/20
 * Time: 15:57
 */
namespace frontend\controllers;

use Yii;
use frontend\models\Channel;
use frontend\models\ChannelType;
use frontend\models\ChannelAppSource;
use frontend\models\WeiboWeiboConnectWebAppUserCreateParam;
use frontend\models\WeiboWeiboConnectWebAppUser;
use frontend\models\redis\WeiboWeiboConnectWebAppUserAccessToken as RedisWeiboWeiboConnectWebAppUserAccessToken;
use frontend\services\ChannelService;
use frontend\services\ChannelTypeService;
use frontend\services\WeiboWeiboConnectWebAppService;
use frontend\services\ChannelAppSourceService;
use frontend\services\WeiboWeiboConnectWebAppUserService;
use frontend\services\WeiboWeiboConnectWebAppAccessTokenService;
use SamIT\Yii2\UrlSigner\UrlSigner;
use SamIT\Yii2\UrlSigner\HmacFilter;
use yii\base\DynamicModel;
use yii\base\Model;
use yii\web\Controller;
use yii\web\Response;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
use yii\web\ServerErrorHttpException;
use yii\web\NotFoundHttpException;
use yii\web\UnprocessableEntityHttpException;

/**
 * Class WeiboOauth2Controller
 * @package frontend\controllers
 *
 * 微博的微连接的网页应用授权  {auth_url}/weibo-oauth2
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */class WeiboOauth2Controller extends Controller
{
    /**
     * {@inheritdoc}
     */    public function actions()
    {
        return [
            'error' => [
                'class' => 'yii\web\ErrorAction',
            ],
        ];
    }

    public function behaviors()
    {
        return [
            'hmacFilter' => [
                'class' => HmacFilter::class,
                'signer' => new UrlSigner([
                    'secret' => Yii::$app->params['urlSigner']['secret']
                ]),
            ],
        ];
    }

    /**
     * 处理模型错误
     * @param object $model 模型
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 214003, // 返回码
     *     'message' => '数据验证失败:权限不能为空。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */    public static function handleValidateError($model)
    {
        if ($model->hasErrors()) {
            $response = Yii::$app->getResponse();
            $response->setStatusCode(422, 'Data Validation Failed.');
            $firstError = '';
            foreach ($model->getFirstErrors() as $message) {
                $firstError = $message;
                break;
            }
            return ['status' => false, 'code' => 214003, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '214003'), ['first_error' => $firstError]))];
        } elseif (!$model->hasErrors()) {
            throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
        }
    }

    /**
     * 处理模型填充与验证
     * @param object $model 模型
     * @param array $requestParams 请求参数
     * @return array
     * 格式如下:
     *
     * [
     *     'status' => true, // 成功
     * ]
     *
     * [
     *     'status' => false, // 失败
     *     'code' => 214002, // 返回码
     *     'message' => '数据验证失败:权限不能为空。', // 说明
     * ]
     *
     * @throws ServerErrorHttpException
     */    public static function handleLoadAndValidate($model, $requestParams)
    {
        // 把请求数据填充到模型中
        if (!$model->load($requestParams)) {
            return ['status' => false, 'code' => 214002, 'message' => Yii::t('error', '214002')];
        }
        // 验证模型
        if (!$model->validate()) {
            return self::handleValidateError($model);
        }

        return ['status' => true];
    }

    /**
     * 引导用户进入授权页面登录同意授权(302 跳转至平台的请求授权页面)
     *
     * 1、请求参数列表
     * (1)source:必填,来源,xContent:内容库;vms:视频管理系统;scms:内容管理系统;spider:自媒体;channel-pub-api:渠道发布接口
     * (2)source_uuid:必填,来源ID(UUID)
     * (3)user_name:必填,用户名称
     * (4)permission:必填,权限,1:同步;2:发布;3:同步与发布
     * (5)status:可选,状态,0:禁用;1:启用,默认:1
     * (6)redirect_uri:必填,微博的微连接的网页应用授权后重定向的回调链接,其值需要 Base64 编码
     *
     * 2、输入数据验证规则
     * (1)存在性:基于代码,weibo:微博查询资源(渠道),如果不存在,则返回失败
     * (2)比对(status !== 1):判断状态(渠道),如果未启用,则返回失败
     * (3)存在性:基于代码,weibo_weibo_connect_web:微博的微连接的网页应用查询资源(渠道的类型),如果不存在,则返回失败
     * (4)比对(status !== 1):判断状态(渠道的类型),如果未启用,则返回失败
     * (5)存在性:基于 App ID 查询资源(微博的微连接的网页应用),如果不存在,则返回失败
     * (6)比对(status !== 1):判断状态(微博的微连接的网页应用),如果未启用,则返回失败
     * (7)必填:source、source_uuid、user_name、permission、redirect_uri
     * (8)默认值(1):status
     * (9)网址(Base64 解码):redirect_uri
     * (10)Base64 编码:user_name
     *
     * 3、操作数据
     * (1)URL 签名
     * (2)302 跳转至 https://api.weibo.com/oauth2/authorize?response_type=code&client_id=3815687113&redirect_uri=http%3A%2F%2Fwww.channel-pub-api-localhost.chinamcloud.com%2Fweibo-oauth2%2Faccess-token%3Fgroup_id%3Dspider%26source%3Dspider%26source_uuid%3D825e6d5e36468cc4bf536799ce3565cf%26user_name%3D%E5%8D%8E%E6%A0%96%E4%BA%91%26permission%3D3%26status%3D1%26redirect_uri%3DaHR0cDovL3d3dy56bXQuY29t
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \Throwable
     */    public function actionAuthorize()
    {
        // 基于代码查找状态为启用的数据模型(渠道)
        $channelEnabledItem = ChannelService::findModelEnabledByCode(Channel::CODE_WEIBO);

        // 基于代码查找状态为启用的数据模型(渠道的类型)
        $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_WEIBO_WEIBO_CONNECT_WEB);

        // 基于 App ID 查找状态为启用的数据模型(微博的微连接的网页应用)
        $weiboWeiboConnectWebAppEnabledItem = WeiboWeiboConnectWebAppService::findModelEnabledByAppId(Yii::$app->params['weiboAuth']['weiboConnectWebApp']['appId']);

        $request = Yii::$app->request;
        $get = $request->get();
        $base64DecodeRedirectUri = base64_decode($request->get('redirect_uri'), true);

        /* 判断请求参数中租户ID是否存在 */        if (empty($get['group_id'])) {
            throw new UnprocessableEntityHttpException(Yii::t('error', '214004'), 214004);
        }

        // Base64 解码失败
        if ($base64DecodeRedirectUri === false) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', 214001)));
        }

        $get['redirect_uri'] = $base64DecodeRedirectUri;

        /* 微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权)参数 */        $weiboWeiboConnectWebAppUserCreateParam = new WeiboWeiboConnectWebAppUserCreateParam();
        // 把请求数据填充到模型中
        if (!$weiboWeiboConnectWebAppUserCreateParam->load($get, '')) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', 214002)));
        }
        // 验证模型
        if (!$weiboWeiboConnectWebAppUserCreateParam->validate()) {
            $weiboWeiboConnectWebAppUserCreateParamResult = self::handleValidateError($weiboWeiboConnectWebAppUserCreateParam);
            if ($weiboWeiboConnectWebAppUserCreateParamResult['status'] === false) {
                throw new UnprocessableEntityHttpException($weiboWeiboConnectWebAppUserCreateParamResult['message']);
            }
        }

        $pathInfo = '/weibo-oauth2/access-token';
        $base64EncodeRedirectUri = base64_encode($base64DecodeRedirectUri);
        $base64EncodeUserName = base64_encode($get['user_name']);

        // URL 签名
        $urlSigner = new UrlSigner([
            'secret' => Yii::$app->params['urlSigner']['secret']
        ]);
        $route = [
            $pathInfo,
            'group_id' => $get['group_id'],
            'source' => $get['source'],
            'source_uuid' => $get['source_uuid'],
            'user_name' => $base64EncodeUserName,
            'permission' => $get['permission'],
            'status' => $weiboWeiboConnectWebAppUserCreateParam->status,
            'redirect_uri' => $base64EncodeRedirectUri,
        ];

        // 是否允许添加额外参数:是,将到期时间设置为 1 分钟
        $urlSigner->signParams($route, true, (new \DateTime())->add(new \DateInterval('PT' . Yii::$app->params['urlSigner']['timeOut'] . 'M')));

        // 包含 host info 的整个 URL,编码 URL 字符串
        $scheme = empty(Yii::$app->params['nginxAuthScheme']) ? true : Yii::$app->params['nginxAuthScheme'];
        $route[0] = Yii::$app->params['auth']['hostInfo'] . Yii::$app->params['auth']['baseUrl'] . $pathInfo;
        $redirectUri = urlencode(Url::to($route, $scheme));

        /* 浏览器 302 跳转:引导用户进入授权页面登录同意授权,获取 code */        Yii::$app->response->redirect(Yii::$app->params['weiboAuth']['hostInfo'] . Yii::$app->params['weiboAuth']['baseUrl'] . '/authorize?response_type=code&client_id=' . Yii::$app->params['weiboAuth']['weiboConnectWebApp']['appId'] . '&redirect_uri=' . $redirectUri);
    }

    /**
     * 通过 Code 换取第三方授权 Access Token,相应数据操作、创建微博的微连接的网页应用的用户
     *
     * 1、请求参数列表
     * (1)source:必填,来源,xContent:内容库;vms:视频管理系统;scms:内容管理系统;spider:自媒体;channel-pub-api:渠道发布接口
     * (2)source_uuid:必填,来源ID(UUID)
     * (3)user_name:必填,用户名称
     * (4)permission:必填,权限,1:同步;2:发布;3:同步与发布
     * (5)status:可选,状态,0:禁用;1:启用,默认:1
     * (6)redirect_uri:必填,微博的微连接的网页应用授权后重定向的回调链接,其值需要 Base64 编码
     * (7)code:必填,用户进入授权页面登录同意授权后所获取到的 Auth Code
     *
     * 2、输入数据验证规则
     * (1)存在性:基于代码,weibo:微博查询资源(渠道),如果不存在,则返回失败
     * (2)比对(status !== 1):判断状态(渠道),如果未启用,则返回失败
     * (3)存在性:基于代码,weibo_weibo_connect_web:微博的微连接的网页应用查询资源(渠道的类型),如果不存在,则返回失败
     * (4)比对(status !== 1):判断状态(渠道的类型),如果未启用,则返回失败
     * (5)存在性:基于 App ID 查询资源(微博的微连接的网页应用),如果不存在,则返回失败
     * (6)比对(status !== 1):判断状态(微博的微连接的网页应用),如果未启用,则返回失败
     * (7)必填:source、source_uuid、user_name、permission、redirect_uri、code
     * (8)默认值(1):status
     * (9)网址(Base64 解码):redirect_uri
     * (10)整数:permission、status
     * (11)字符串(最大长度:32):source、user_name
     * (12)字符串(最大长度:64):source_uuid
     * (13)范围([1, 2, 3]):permission
     * (14)范围([0, 1]):status
     * (15)Base64 解码:user_name
     *
     * 3、操作数据
     * (1)HTTP 请求,通过 Code 换取第三方授权 Access Token
     * (2)基于 授权第三方用户的用户唯一标识 查询微博的微连接的网页应用的用户是否存在,如果已存在,则返回失败
     * (3)创建 MySQL 模型(渠道的应用的来源)
     * (4)创建 MySQL 模型(微博的微连接的网页应用的用户)
     * (5)基于 Access Token 插入/更新数据至微博的微连接的网页应用的用户的访问令牌(Redis)
     * (6)浏览器 302 跳转:微博的微连接的网页应用授权后重定向的回调链接 http://www.zmt.com/?group_id=spider&code=10000&message=微博的微连接的网页应用授权成功&channel_app_source_uuid=c0ec27fe080c11e997a954ee75d2ebc1
     *
     * @throws NotFoundHttpException 如果未找到数据模型,将抛出 404 HTTP 异常
     * @throws UnprocessableEntityHttpException 如果找到数据模型,状态未启用,将抛出 422 HTTP 异常
     * @throws \Throwable
     */    public function actionAccessToken()
    {
        // 基于代码查找状态为启用的数据模型(渠道)
        $channelEnabledItem = ChannelService::findModelEnabledByCode(Channel::CODE_WEIBO);

        // 基于代码查找状态为启用的数据模型(渠道的类型)
        $channelTypeEnabledItem = ChannelTypeService::findModelEnabledByCode(ChannelType::CODE_WEIBO_WEIBO_CONNECT_WEB);

        // 基于 App ID 查找状态为启用的数据模型(微博的微连接的网页应用)
        $weiboWeiboConnectWebAppEnabledItem = WeiboWeiboConnectWebAppService::findModelEnabledByAppId(Yii::$app->params['weiboAuth']['weiboConnectWebApp']['appId']);

        $request = Yii::$app->request;
        $get = $request->get();
        $code = $request->get('code');
        $base64DecodeRedirectUri = base64_decode($request->get('redirect_uri'), true);
        $base64DecodeUserName = base64_decode($request->get('user_name'), true);

        /* 判断请求参数中租户ID是否存在 */        if (empty($get['group_id'])) {
            throw new UnprocessableEntityHttpException(Yii::t('error', '214004'), 214004);
        }

        // Base64 解码失败
        if ($base64DecodeRedirectUri === false) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', 214001)));
        }

        // Base64 解码失败
        if ($base64DecodeUserName === false) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', 214005)));
        }

        $get['redirect_uri'] = $base64DecodeRedirectUri;
        $get['user_name'] = $base64DecodeUserName;

        /* 通过 Code 换取第三方授权 Access Token,相应数据操作参数 */        $weiboWeiboConnectWebAppUserCreateParam = new WeiboWeiboConnectWebAppUserCreateParam();
        // 把请求数据填充到模型中
        if (!$weiboWeiboConnectWebAppUserCreateParam->load($get, '')) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', 214002)));
        }
        // 验证模型
        if (!$weiboWeiboConnectWebAppUserCreateParam->validate()) {
            $weiboWeiboConnectWebAppUserCreateParamResult = self::handleValidateError($weiboWeiboConnectWebAppUserCreateParam);
            if ($weiboWeiboConnectWebAppUserCreateParamResult['status'] === false) {
                throw new UnprocessableEntityHttpException($weiboWeiboConnectWebAppUserCreateParamResult['message']);
            }
        }

        // Code 参数不存在
        if (!isset($code)) {
            $errorCode = $request->get('error_code');
            $errorMsg = $request->get('error_description');
            if (!isset($errorCode)) {
                $errorCode = 0;
            }
            if (!isset($errorMsg)) {
                $errorMsg = '';
            }
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '214006'), ['error_code' => $errorCode, 'error_msg' => $errorMsg])));
        }

        /* 通过 Code 换取第三方授权 Access Token,相应数据操作 */        $weiboWeiboConnectWebAppAccessTokenService = new WeiboWeiboConnectWebAppAccessTokenService();

        /* HTTP 请求,通过 Code 换取第三方授权 Access Token */        $httpAccessTokenData = [
            'grantType' => RedisWeiboWeiboConnectWebAppUserAccessToken::GRANT_TYPE_AUTHORIZATION_CODE,
            'clientId' => Yii::$app->params['weiboAuth']['weiboConnectWebApp']['appId'],
            'clientSecret' => Yii::$app->params['weiboAuth']['weiboConnectWebApp']['appSecret'],
            'redirectUri' => Yii::$app->params['weiboAuth']['weiboConnectWebApp']['authCallbackUrl'],
            'code' => $code,
        ];
        $accessToken = $weiboWeiboConnectWebAppAccessTokenService->httpAccessToken($httpAccessTokenData);
        /*
        $accessToken = [
            'access_token' => '2.00OaBgKGXWOOKE92b7df5350ZJoY9D',
            'remind_in' => '157679999',
            'expires_in' => 157679999,
            'uid' => '5654576218',
            'isRealName' => 'false',
        ];
        */
        /* 实例化多个模型 */
        // 渠道的应用的来源
        $channelAppSource = new ChannelAppSource([
            'scenario' => ChannelAppSource::SCENARIO_CREATE,
        ]);
        // 转换创建微博的微连接的网页应用的用户参数,多模型的填充、验证的实现
        $get[$channelAppSource->formName()]['source'] = $weiboWeiboConnectWebAppUserCreateParam->source;
        $get[$channelAppSource->formName()]['source_uuid'] = $weiboWeiboConnectWebAppUserCreateParam->source_uuid;
        $get[$channelAppSource->formName()]['status'] = $weiboWeiboConnectWebAppUserCreateParam->status;
        $channelAppSourceResult = self::handleLoadAndValidate($channelAppSource, $get);
        if ($channelAppSourceResult['status'] === false) {
            throw new UnprocessableEntityHttpException($channelAppSourceResult['message']);
        }

        // 微博的微连接的网页应用的用户
        /* @var $model \yii\db\ActiveRecord */        $model = new WeiboWeiboConnectWebAppUser([
            'scenario' => WeiboWeiboConnectWebAppUser::SCENARIO_CREATE,
        ]);
        // 转换创建微博的微连接的网页应用的用户参数,多模型的填充、验证的实现
        $get[$model->formName()] = [
            'weibo_weibo_connect_web_app_id' => $weiboWeiboConnectWebAppEnabledItem->id,
            'user_id' => $accessToken['uid'],
            'user_name' => $weiboWeiboConnectWebAppUserCreateParam->user_name,
            'permission' => $weiboWeiboConnectWebAppUserCreateParam->permission,
            'status' => $weiboWeiboConnectWebAppUserCreateParam->status,
        ];
        $modelResult = self::handleLoadAndValidate($model, $get);
        if ($modelResult['status'] === false) {
            throw new UnprocessableEntityHttpException($modelResult['message']);
        }

        /* 操作数据(事务) */        $weiboWeiboConnectWebAppService = new WeiboWeiboConnectWebAppService();
        $weiboWeiboConnectWebAppServiceUserUpdateResult = $weiboWeiboConnectWebAppService->userCreate($channelEnabledItem, $channelTypeEnabledItem, $channelAppSource, $model);
        if ($weiboWeiboConnectWebAppServiceUserUpdateResult['status'] === false) {
            throw new ServerErrorHttpException($weiboWeiboConnectWebAppServiceUserUpdateResult['message'], $weiboWeiboConnectWebAppServiceUserUpdateResult['code']);
        }

        // 基于 Access Token 插入/更新数据至微博的微连接的网页应用的用户的访问令牌(Redis)
        $data = [
            'weiboWeiboConnectWebAppId' => $weiboWeiboConnectWebAppServiceUserUpdateResult['data']['weibo_weibo_connect_web_app_id'],
            'channelAppSourceId' => $weiboWeiboConnectWebAppServiceUserUpdateResult['data']['channel_app_source_id'],
            'channelAppSourceUuid' => $weiboWeiboConnectWebAppServiceUserUpdateResult['data']['channel_app_source_uuid'],
            'weiboWeiboConnectWebAppUserId' => $weiboWeiboConnectWebAppServiceUserUpdateResult['data']['id'],
            'weiboWeiboConnectWebAppUserUuid' => $weiboWeiboConnectWebAppServiceUserUpdateResult['data']['uuid'],
            'grantType' => RedisWeiboWeiboConnectWebAppUserAccessToken::GRANT_TYPE_AUTHORIZATION_CODE,
        ];
        $result = $weiboWeiboConnectWebAppAccessTokenService->saveModel(ArrayHelper::merge($data, $accessToken));
        if ($result['status'] === false) {
            throw new ServerErrorHttpException($result['message'], $result['code']);
        }

        /* 浏览器 302 跳转:微博的微连接的网页应用授权后重定向的回调链接 */        $pos = strpos($get['redirect_uri'], '?');
        if ($pos === false) {
            $redirectUri = $get['redirect_uri'] . '?group_id=' . $get['group_id'] . '&code=10000&message=' . Yii::t('success', '114001') . '&channel_app_source_uuid=' . $weiboWeiboConnectWebAppServiceUserUpdateResult['data']['channel_app_source_uuid'];
        } else {
            $redirectUri = $get['redirect_uri'] . '&group_id=' . $get['group_id'] . '&code=10000&message=' . Yii::t('success', '114001') . '&channel_app_source_uuid=' . $weiboWeiboConnectWebAppServiceUserUpdateResult['data']['channel_app_source_uuid'];
        }

        Yii::$app->response->redirect($redirectUri);
    }

}

8、测试 URL 验证,修改/删除/添加参数,符合预期,如图2

图2


获取到的链接:
http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A

修改参数:permission=1

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=1&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A

Forbidden (#403)
This security code in this URL invalid

修改参数:expires=1547801680

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=1&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801680&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A

Forbidden (#403)
This security code in this URL invalid

修改参数:hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_0

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_0

Forbidden (#403)
This security code in this URL invalid

删除参数:status=1

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A

Forbidden (#403)
This security code in this URL invalid

删除参数:expires=1547801689

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A

Forbidden (#403)
This security code in this URL invalid

添加参数:p=1

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A&p=1

Forbidden (#403)
This security code in this URL invalid

9、测试 URL 验证,删除 URL 签名:expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A,符合预期,如图3

图3


删除参数:expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A
http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t

Forbidden (#403)
This security code for this URL is missing

10、测试 URL 验证,超时 1 分钟后,符合预期,如图4

图4


超时 1 分钟后:
http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547801689&hmac=17h9ZhIxaDLRWmINHOZ-Syh9X-x7VCj9enotVRjbE_A

Forbidden (#403)
This URL has expired

11、测试 URL 验证,从微博跳转回的链接,是否允许添加额外参数:是,code=7c104c4d67fa8b246ca8081248486be1,符合预期,如图5

图5


获取到的链接:
http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547802531&hmac=6S7d_3MtbrpT3KOlwJl_6gkg1HItB0nhy1kH4h7uiyY

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/access-token?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=5Y2O5qCW5LqRMTY1ODM5Nzk2Mg%3D%3D&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547802545&params=group_id%2Csource%2Csource_uuid%2Cuser_name%2Cpermission%2Cstatus%2Credirect_uri%2Cexpires&hmac=R_DEzUDJTpQDkwquF_-pKT_4oJdOxnw3PE_eUaW70jY&code=7c104c4d67fa8b246ca8081248486be1

修改参数:permission=1

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/access-token?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=5Y2O5qCW5LqRMTY1ODM5Nzk2Mg%3D%3D&permission=1&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547802545&params=group_id%2Csource%2Csource_uuid%2Cuser_name%2Cpermission%2Cstatus%2Credirect_uri%2Cexpires&hmac=R_DEzUDJTpQDkwquF_-pKT_4oJdOxnw3PE_eUaW70jY&code=7c104c4d67fa8b246ca8081248486be1

Forbidden (#403)
This security code in this URL invalid

删除参数:status=1

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/access-token?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=5Y2O5qCW5LqRMTY1ODM5Nzk2Mg%3D%3D&permission=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1547802545&params=group_id%2Csource%2Csource_uuid%2Cuser_name%2Cpermission%2Cstatus%2Credirect_uri%2Cexpires&hmac=R_DEzUDJTpQDkwquF_-pKT_4oJdOxnw3PE_eUaW70jY&code=7c104c4d67fa8b246ca8081248486be1

Forbidden (#403)
This security code in this URL invalid

12、URL 签名和验证已经实现,现阶段存在的问题是同一个签名后的网址,可以在 1 分钟内多次打开,仍然存在安全问题,需要确保 1 次签名仅使用 1 次,使用后就失效。在 $urlSigner->signParams() 后,将 hmac 存储至 Redis 中,过期时间为 60 秒,如图6

图6

        // 是否允许添加额外参数:否,将到期时间设置为 1 分钟
        $urlSigner->signParams($route, false, (new \DateTime())->add(new \DateInterval('PT' . Yii::$app->params['urlSigner']['timeOut'] . 'M')));

        // 获取 redis 组件
        $redis = Yii::$app->redis;
        // 将字符串值 $route['hmac'] . '_' . $route['expires'] 关联到 md5($route['hmac']),过期时间为 60 秒
        $redis->set(Yii::$app->params['redisCommand']['keyPrefix'] . md5($route['hmac']), $route['hmac'] . '_' . $route['expires'], 'ex', Yii::$app->params['urlSigner']['timeOut'] * 60);
        // 是否允许添加额外参数:是,将到期时间设置为 1 分钟
        $urlSigner->signParams($route, true, (new \DateTime())->add(new \DateInterval('PT' . Yii::$app->params['urlSigner']['timeOut'] . 'M')));

        // 获取 redis 组件
        $redis = Yii::$app->redis;
        // 将字符串值 $route['hmac'] . '_' . $route['expires'] 关联到 md5($route['hmac']),过期时间为 60 秒
        $redis->set(Yii::$app->params['redisCommand']['keyPrefix'] . md5($route['hmac']), $route['hmac'] . '_' . $route['expires'], 'ex', Yii::$app->params['urlSigner']['timeOut'] * 60);

13、创建过滤器 common\filters\HmacFilter,继承至 SamIT\Yii2\UrlSigner\HmacFilter,在 hmacFilter 过滤器之后执行,如果 redis hmac 存在,则删除 redis hmac,且响应 true,如果 redis hmac 不存在,则响应 false。新建 \common\filters\HmacFilter.php

<?php

namespace common\filters;

use Yii;
use SamIT\Yii2\UrlSigner\InvalidHmacException;

/**
 * 检查 URL 中的有效 HMAC,是否已经被请求过 1 次
 * @package common\fixtures
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */class HmacFilter extends \SamIT\Yii2\UrlSigner\HmacFilter
{
    /**
     * @param \yii\base\Action $action
     * @throws \Exception
     * @return bool
     */    public function beforeAction($action)
    {
        $result = parent::beforeAction($action);

        if ($result === true) {
            // 获取 redis 组件
            $redis = Yii::$app->redis;
            $request = $action->controller->module->get('request');
            $hmac = Yii::$app->params['redisCommand']['keyPrefix'] . md5($request->get('hmac'));
            if ($redis->get($hmac)) {
                $redis->del($hmac);
                return $result;
            } else {
                throw new InvalidHmacException();
            }
        }
    }

}

14、编辑 \frontend\controllers\WeiboOauth2Controller.php,即 微博的微连接的网页应用授权(引导用户进入授权页面登录同意授权、创建微博的微连接的网页应用的用户)

use common\filters\HmacFilter;

15、测试 URL 验证,修改参数,符合预期
获取到的链接,第 1 次验证通过,第 2 次验证失败:

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=2&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1548050429&hmac=jU4Vo4n2Pe04ymy0UHnqB-_hSjZquOHzkKyU0SCcUPU

第 2 次验证失败
Forbidden (#403)
This security code in this URL invalid

修改参数:permission=1

http://www.channel-pub-api-localhost.chinamcloud.com/weibo-oauth2/authorize?group_id=spider&source=spider&source_uuid=825e6d5e36468cc4bf536799ce3565cf&user_name=%E5%8D%8E%E6%A0%96%E4%BA%911658397962&permission=1&status=1&redirect_uri=aHR0cDovL3d3dy56bXQuY29t&expires=1548050429&hmac=jU4Vo4n2Pe04ymy0UHnqB-_hSjZquOHzkKyU0SCcUPU

Forbidden (#403)
This security code in this URL invalid

 

永夜