🔐 通用 - 签名接口

ThinkAdmin 提供完善的签名接口机制,支持安全的 API 接口调用和数据验证。

🚀 主要功能

  • 安全签名: 基于 MD5 的接口数据签名机制
  • 时间验证: 支持时间戳验证,防止重放攻击
  • 随机字符串: 使用随机字符串增强安全性
  • 调试模式: 支持调试模式,方便开发测试
  • 分页支持: 支持列表分页功能
  • 文件传输: 支持 base64 文件传输

📋 请求字段说明

必需字段

  • appid: 接口请求认证账号,需要保持两端一致
  • time: 接口请求时间(时差不能超过30秒),使用时间戳,单位为秒
  • nostr: 接口请求随机字符串,可以使用随机函数生成
  • data: 接口请求数据(数据需转为JSON格式),需以原格式传输
  • sign: 接口数据签名,按特定规则生成

签名规则

  • 字段顺序: 依次为 appiddatatimeappkeynostr
  • 连接方式: 五个字段以 # 连接
  • 加密方式: 进行 MD5 编码获取签名
  • 私钥保护: appkey 为接口私钥,仅用于签名且不参与接口数据请求

⚙️ 特殊说明

文件传输

  • 传输方式: 使用 base64 传输文件
  • 数据格式: 需要去掉数据头部
  • 大文件处理: 建议直接对接云存储
  • 性能考虑: 大文件传输可能影响性能

调试模式

  • 调试开关: 当接口的 debugtrue
  • 简化请求: 不需要传签名内容
  • 普通表单: 只需要以普通表单传原接口的 data 内容
  • 格式要求: 非JSON格式,方便调试编写文档
  • 开发便利: 简化开发阶段的接口调试

列表分页

  • 分页参数: 使用 get 方式传 page 参数
  • 签名排除: page 不需要参与签名
  • 数据格式: 传在 data 的 JSON 中会无效
  • 参数不符: 因为数据格式与 Page 参数不符
  • 性能优化: 分页参数独立处理提升性能

安全机制

  • 时间验证: 时间戳验证防止重放攻击
  • 随机字符串: 使用随机字符串增强安全性
  • 签名验证: 严格的签名验证机制
  • 私钥保护: 私钥不参与接口数据请求

🔧 实际应用案例

前端客户端请求案例

基础请求示例

// 1. 加载 MD5 插件
require(['md5'], function(md5) {
    // 2. 配置接口参数
    const config = {
        appid: 'your_app_id',      // 接口账号
        appkey: 'your_app_key',    // 接口密钥
        apiUrl: 'https://your-domain.com/api/data/getUserInfo'  // 接口地址
    };
    
    // 3. 准备请求数据
    const requestData = {
        user_id: 123,
        page: 1,
        limit: 10
    };
    
    // 4. 生成签名
    function generateSign(appid, data, appkey) {
        const nostr = Math.random().toString(36).substr(2, 15);
        const time = Math.ceil(Date.now() / 1000);
        const json = JSON.stringify(data);
        const signString = [appid, json, time, appkey, nostr].join('#');
        const sign = md5(signString);
        
        return {
            appid: appid,
            time: time,
            nostr: nostr,
            data: json,
            sign: sign
        };
    }
    
    // 5. 发起请求
    const signData = generateSign(config.appid, requestData, config.appkey);
    
    $.ajax({
        url: config.apiUrl,
        type: 'POST',
        data: signData,
        dataType: 'json',
        success: function(response) {
            if (response.code === 1) {
                console.log('请求成功:', response.data);
                // 处理成功响应
            } else {
                console.error('请求失败:', response.info);
                // 处理错误响应
            }
        },
        error: function(xhr, status, error) {
            console.error('请求异常:', error);
        }
    });
});

封装成通用方法

// 封装签名接口请求方法
class ApiClient {
    constructor(appid, appkey, baseUrl) {
        this.appid = appid;
        this.appkey = appkey;
        this.baseUrl = baseUrl;
    }
    
    // 生成签名
    generateSign(data) {
        const nostr = Math.random().toString(36).substr(2, 15);
        const time = Math.ceil(Date.now() / 1000);
        const json = JSON.stringify(data);
        const signString = [this.appid, json, time, this.appkey, nostr].join('#');
        const sign = md5(signString);
        
        return {
            appid: this.appid,
            time: time,
            nostr: nostr,
            data: json,
            sign: sign
        };
    }
    
    // 发起请求
    request(endpoint, data = {}) {
        return new Promise((resolve, reject) => {
            const signData = this.generateSign(data);
            
            $.ajax({
                url: this.baseUrl + endpoint,
                type: 'POST',
                data: signData,
                dataType: 'json',
                success: function(response) {
                    if (response.code === 1) {
                        resolve(response.data);
                    } else {
                        reject(new Error(response.info));
                    }
                },
                error: function(xhr, status, error) {
                    reject(new Error(error));
                }
            });
        });
    }
}

// 使用示例
const api = new ApiClient('your_app_id', 'your_app_key', 'https://your-domain.com/api/');

// 获取用户列表
api.request('/data/getUserList', { page: 1, limit: 10 })
    .then(data => {
        console.log('用户列表:', data);
    })
    .catch(error => {
        console.error('请求失败:', error.message);
    });

// 获取用户详情
api.request('/data/getUserInfo', { user_id: 123 })
    .then(data => {
        console.log('用户详情:', data);
    })
    .catch(error => {
        console.error('请求失败:', error.message);
    });

调试模式示例

// 调试模式下的简化请求
function debugRequest(endpoint, data) {
    // 调试模式下直接发送数据,不需要签名
    $.ajax({
        url: 'https://your-domain.com/api' + endpoint,
        type: 'POST',
        data: data,  // 直接发送原始数据
        dataType: 'json',
        success: function(response) {
            console.log('调试请求成功:', response);
        },
        error: function(xhr, status, error) {
            console.error('调试请求失败:', error);
        }
    });
}

// 使用调试模式
debugRequest('/data/getUserList', {
    page: 1,
    limit: 10,
    debug: true  // 开启调试模式
});

后端客户端请求案例

use think\admin\service\InterfaceService;

// 实例接口服务接口
$service = InterfaceService::instance();
$service->getway(GETWAY); // 设置接口请求网关
$service->setAuth(APPID, APPKEY); // 设置接口认证账号
$result = $service->doRequest('API_URL', DATA); // 发起接口请求
print_r($result);

服务端接口控制器案例

namespace app\data\controller\api;

use think\admin\Controller;
use think\admin\service\InterfaceService;

/**
 * 接口授权基础控制器
 * Class Auth
 * @package app\data\controller\api
 */
class Auth extends Controller
{
    /**
     * 当前请求数据
     * @var array
     */
    protected $data;

    /**
     * 接口ID
     * @var string
     */
    protected $appid = '接口账号';

    /**
     * 接口密钥
     * @var string
     */
    protected $appkey = '接口密钥';

    /**
     * 接口服务对象
     * @var InterfaceService
     */
    protected $interface;

    /**
     * 接口授权初始化
     */
    protected function initialize()
    {
        // 实例接口对象
        $this->interface = InterfaceService::instance();
        // 获取接口数据
        $this->interface->setAuth($this->appid, $this->appkey);
        $this->data = $this->interface->getData();
    }

    /**
     * 响应错误消息
     * @param mixed $info
     * @param mixed $data
     * @param mixed $code
     */
    public function error($info, $data = '{-null-}', $code = 0): void
    {
        $this->interface->error($info, $data, $code);
    }

    /**
     * 响应成功消息
     * @param mixed $info
     * @param mixed $data
     * @param mixed $code
     */
    public function success($info, $data = '{-null-}', $code = 1): void
    {
        $this->interface->success($info, $data, $code);
    }

    /**
     * 响应高级数据
     * @param array $data
     */
    protected function response(array $data = []): void
    {
        $this->interface->baseSuccess('接口调用成功!', $data);
    }
}

如果需要使用数据库来管理接口账号,需要调整下初始化方法

// 接口实例初始化
$this->interface = InterfaceService::instance();

// 从数据库中账号检查 ( 用自己的数据表 )
$map = $this->_vali(['appid.require' => '参数APPID不能为空!']);
$this->user = $this->app->db->name('AppUser')->where($map)->find();

if (empty($this->user)) $this->error('接口账号不存在!');
if (empty($this->user['status'])) $this->error('接口账号已被禁用!');
if (!empty($this->user['is_deleted'])) $this->error('接口账号已被移除!');

// 接口数据初始化
$this->interface->setAuth($this->user['appid'], $this->user['appkey']);
[$this->data, $this->appid] = [$this->interface->getData(), $this->interface->getAppid()];

处理业务时,可以对接口提交过来的数据进行二次验证(需要继承基础控制器)

// 接收并验证参数是否符合要求,失败返回业务错误内容
$data = $this->_vali([...], $this->data, [$this,'error']);

// 使用 $this->_query 动态传参数查询,与后台控制器操作无差别(只需要指定数据)
$query = $this->_query($this->table, $this->data);
$query->like(...)->equal(...)->in(...); // 绑定查询条件
$this->success("操作成功", $query->page(true,false));
最近更新:
Contributors: 邹景立