0


接口管理工具Apifox在前后端分离项目中的实践

前言

最近在做的集团SaaS平台的派车模块,因实际使用中司机无法操作电脑端,所以又开发了派车小程序以方便司机角色去接单、派车和送货签收操作。小程序端直接调用的是后台的派车模块的接口,这就涉及到了前后端分离中的一个痛点-接口的文档维护和接口的联调测试问题。幸好,在这个全民脱贫、码农翻身把歌唱的时代,我们有了比postman更好用的接口管理工具-Apifox

官方👉[点我直达]给出的介绍:

Apifox 是接口管理、开发、测试全流程集成工具,定位 Postman + Swagger + Mock + JMeter。通过一套系统、一份数据,解决多个系统之间的数据同步问题。只要定义好接口文档,接口调试、数据 Mock、接口测试就可以直接使用,无需再次定义;接口文档和接口开发调试使用同一个工具,接口调试完成后即可保证和接口文档定义完全一致。高效、及时、准确!

Apifox在项目中的实践应用

一、后端接口服务的签名验证规则

  1. 调用 JSON 格式为:{"accessKey":,//访问key(由系统分配给用户)"reqSign":xxxxxxxxxxxxxxxxxxxxxxxxxx,//用一定规则生成的签名"timestamp":2022-01-2013:15:15,//请求时间记录"nonce":123456,//小于6位的随机数,用来标识每个被签名的请求// "data":{} //查询参数}
  2. Signature****参数签名生成规则:① 按照请求参数名的字母升序排列非空请求参数(包含AccessKey),使用URL键值对的格式(即 key1=value1&key2=value2…)拼接成字符串stringA;② 在stringA最后拼接上用户密钥(32位UUID)得到字符串stringSignTemp; 对stringSignTemp进行MD5运算,并将得到的字符串所有字符转换为大写,得到Signature 值。返回 JSON 格式为:{"ok":true,//查询是否成功"errorCode":null,//错误码"errors":null,//错误信息“data”:{}//查询结果数据}具体的调用参数和返回结果中 data 的内容各个功能详细描述。验证失败的返回结果是:{"ok":false,"errorCode":-1"errors":”用户验证失败”"data":null}另外错误返回可能还包括:-2:服务过期-3: 未购买指定的服务-4: 内部错误

二、后端权限过滤器AuthFilter

权限过滤器:

packagecom.jieguan.filter;importcom.jieguan.entity.ParamDTO;importcom.jieguan.utils.ServiceLicKit;importcom.yorma.constant.RspCode;importio.zbus.rpc.RpcFilter;importio.zbus.rpc.annotation.FilterDef;importio.zbus.transport.Message;importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Component;importjava.util.HashMap;/**
 * 权限过滤器
 *
 * @author ZHANGCHAO
 * @date 2022/3/31 16:56
 */@Slf4j@Component("authFilter1")@FilterDef("jieguanAuthFilter")publicclassAuthFilterimplementsRpcFilter{@OverridepublicbooleandoFilter(Message request,Message response,Throwable exception){boolean auth =false;ParamDTO param =ServiceLicKit.checkParams(request);if(!param.isOk()){
            response.setStatus(RspCode.REQ_ERR);
            response.setBody(param.getErrorMsg());returnfalse;}//校验 NONCE 防重放if(!ServiceLicKit.verifyNonce(param.getTimeStamp(), param.getNonce())){
            response.setStatus(RspCode.UNAUTH);
            response.setHeaders(newHashMap<>());
            response.setBody("校验NONCE未通过,请求拒绝!");//校验 URI访问控制}elseif(!ServiceLicKit.verifyUri(request, param.getLic())){
            response.setStatus(RspCode.UNAUTH);
            response.setBody("访问受限!");//校验 请求签名 防篡改}elseif(!ServiceLicKit.verifySign(param, request)){
            response.setStatus(RspCode.UNAUTH);
            response.setBody("非法请求!");}else{
            auth =true;}return auth;}}

权限验证处理类:

packagecom.jieguan.utils;importcn.hutool.core.date.DateUnit;importcn.hutool.core.date.DateUtil;importcom.alibaba.fastjson.JSON;importcom.alibaba.fastjson.JSONArray;importcom.alibaba.fastjson.JSONObject;importcom.baomidou.mybatisplus.core.conditions.query.QueryWrapper;importcom.jieguan.config.SpringUtil;importcom.jieguan.entity.Nonce;importcom.jieguan.entity.ParamDTO;importcom.jieguan.entity.ServiceLic;importcom.jieguan.entity.ServiceLicUrl;importcom.jieguan.mapper.NonceMapper;importcom.jieguan.mapper.ServiceLicMapper;importcom.jieguan.mapper.ServiceLicUrlMapper;importcom.yorma.util.FileKit;importcom.yorma.util.MD5Util;importcom.yorma.util.StringUtil;importio.zbus.transport.Message;importlombok.extern.slf4j.Slf4j;importjava.io.File;importjava.util.*;importstaticcn.hutool.core.util.ObjectUtil.isEmpty;importstaticcn.hutool.core.util.ObjectUtil.isNotEmpty;importstaticcn.hutool.core.util.StrUtil.isBlank;importstaticcn.hutool.core.util.StrUtil.isNotBlank;/**
 * AppKeyKit
 *
 * @author 张杰  2021/11/8 10:39
 * @version 1.0
 * @apiNote <pre>
 *   类简介
 * </pre>
 */@Slf4jpublicclassServiceLicKit{publicstaticfinalString TIMESTAMP ="timestamp";publicstaticfinalString NONCE ="nonce";publicstaticfinalString MD5 ="MD5";//摘要算法: SM3/MD5publicstaticfinalString SM3 ="SM3";//摘要算法: SM3/MD5publicstaticfinalint MAX_DELAY =5;// NONCE间隔时间privatestaticfinalString ACCESS_KEY ="accessKey";privatestaticfinalString SECRET_KEY ="secretKey";privatestaticfinalString SIGN ="reqSign";privatestaticfinalString BODY_HASH ="bodyHash";/**
     * 提前验证参数
     *
     * @param request 请求
     * @return com.jieguan.entity.ParamDTO
     * @author ZHANGCHAO
     * @date 2022/1/17 22:56
     */publicstaticParamDTOcheckParams(Message request){ParamDTO param =newParamDTO();String accessKey =ServiceLicKit.getKey(ACCESS_KEY, request);String sign =ServiceLicKit.getKey(SIGN, request);String bodyHash =ServiceLicKit.getKey(BODY_HASH, request);ServiceLic lic =ServiceLicKit.getLicByAccessKey(accessKey);String timeStamp =ServiceLicKit.getKey(ServiceLicKit.TIMESTAMP, request);Long nonce;try{
            nonce =Long.valueOf(ServiceLicKit.getKey(ServiceLicKit.NONCE, request));}catch(Exception e){
            param.setErrorMsg("防重放标识nonce不存在或格式错误!");return param;}if(isBlank(accessKey)){
            param.setErrorMsg("未获取到用户标识AccessKey!");return param;}if(isBlank(sign)){
            param.setErrorMsg("未获取到参数签名sign!");return param;}if(isBlank(timeStamp)){
            param.setErrorMsg("未获取到请求时间戳timestamp!");return param;}if(isEmpty(nonce)){
            param.setErrorMsg("未获取到防重放标识nonce!");return param;}if(isEmpty(lic)){
            param.setErrorMsg("未获取到此用户标识的许可信息!");return param;}
        param.setOk(true).setAccessKey(accessKey).setSign(sign).setBodyHash(bodyHash).setTimeStamp(timeStamp).setNonce(nonce).setLic(lic);
        log.info("[参数检查]最终的Param:"+ param);return param;}/**
     * 查询许可, 根据accessKey
     *
     * @param accessKey
     * @return
     */publicstaticServiceLicgetLicByAccessKey(String accessKey){ServiceLicMapper licMapper =(ServiceLicMapper)SpringUtil.getBean("serviceLicMapper");ServiceLic serviceLic = licMapper.selectOne(newQueryWrapper<ServiceLic>().lambda().eq(ServiceLic::getAccessKey, accessKey).eq(ServiceLic::getIsWhite,true));if(isEmpty(serviceLic)){returnnull;}ServiceLicUrlMapper licUrlMapper =(ServiceLicUrlMapper)SpringUtil.getBean("serviceLicUrlMapper");List<ServiceLicUrl> serviceLicUrls = licUrlMapper.selectList(newQueryWrapper<ServiceLicUrl>().lambda().eq(ServiceLicUrl::getLicId, serviceLic.getId()));Set<String> urlSet =newHashSet<>();if(isNotEmpty(serviceLicUrls)){for(ServiceLicUrl url : serviceLicUrls){
                urlSet.add(url.getLicUrl());}}
        serviceLic.setUrlSet(urlSet);return serviceLic;}/**
     * 取参数或头的属性值(参数优先)
     *
     * @param key
     * @param msg
     * @return
     */publicstaticStringgetKey(String key,Message msg){String val =null;if(StringUtil.isNotEmpty(key)&& msg !=null){
            val = msg.getParam(key)==null? msg.getHeader(key): msg.getParam(key,String.class);}return val;}/**
     * 校验 NONCE
     *
     * @param timeStamp
     * @param nonce
     * @return
     */publicstaticbooleanverifyNonce(String timeStamp,long nonce){long betweenTime =DateUtil.between(DateUtil.parseDateTime(timeStamp),newDate(),DateUnit.MINUTE,false);// 超出5分钟时间范围?if(betweenTime > MAX_DELAY || betweenTime <0){
            log.info("[校验NONCE]超出时间范围,请求拒绝!");returnfalse;}NonceMapper nonceMapper =(NonceMapper)SpringUtil.getBean("nonceMapper");Nonce nonceRecord = nonceMapper.selectOne(newQueryWrapper<Nonce>().lambda().eq(Nonce::getNonce, nonce));if(isNotEmpty(nonceRecord)){
            log.info("[校验NONCE]已存在的NONCE,请求拒绝!");
            nonceRecord.setAttackTimes(isNotEmpty(nonceRecord.getAttackTimes())? nonceRecord.getAttackTimes()+1:1);
            nonceMapper.updateById(nonceRecord);returnfalse;}Nonce nonceNew =newNonce();
        nonceNew.setNonce(nonce).setReqTime(DateUtil.parseDateTime(timeStamp));
        nonceMapper.insert(nonceNew);returntrue;}/**
     * 访问权限验证
     *
     * @param msg
     * @param lic
     * @return
     */publicstaticbooleanverifyUri(Message msg,ServiceLic lic){String uri = msg.getUrl();String queryStr = msg.getQueryString();
        uri = uri.replace("?"+ queryStr,"");returnisNotEmpty(lic.getUrlSet())&& lic.getUrlSet().contains(uri);}/**
     * 验证请求签名
     * 原文= paramStr[&timeValue][&nonceValue][&bodyHashHEXValue]&secretKeyValue]
     * paramStr: 请求参数原文(不含‘?’,保持顺序)
     * bodyHashHEXValue:POST/PUT需要计算 bodyHash值,算法 MD5/SM3, 格式 HEX
     * secretKeyValue: 根据 accessKey 获取服务端记录的 secretKeyValue
     *
     * @param param
     * @param req
     * @return
     */publicstaticbooleanverifySign(ParamDTO param,Message req){boolean rt =false;String src =isBlank(req.getQueryString())?"": req.getQueryString().replaceFirst("&?"+ SIGN +"=[0-9,a-f,A-F]+","");String sign = param.getSign();// 时间戳if(!src.contains(TIMESTAMP +"=")){
            src +="&"+ param.getTimeStamp();}// nonceif(!src.contains(NONCE +"=")){
            src +="&"+ param.getNonce();}// bodyHashif(param.getBodyHash()!=null&&!src.contains(BODY_HASH +"=")){
            src +="&"+ param.getBodyHash();}
        src +="&"+ param.getLic().getSecretKey();// MD5 16byte, SM3 32byteString alg = sign.length()==64? SM3 : MD5;//param.getLic().getAlgorithm();//String localSign =signature(src, alg);String localBodyHash ="";if(isNotBlank(param.getBodyHash())){String sourtJson =getSortJson(req.getBody());
            log.info("sortJson:"+ sourtJson);
            localBodyHash =signature(sourtJson, alg);}
        log.info("[src]:"+ src);if(!localSign.equalsIgnoreCase(sign)){
            log.info("[src]:"+ src);
            log.info("[sign]:"+ sign);
            log.info("[localSign]:"+ localSign);}elseif(isNotBlank(param.getBodyHash())&&!localBodyHash.equalsIgnoreCase(param.getBodyHash())){
            log.info("[bodyHash]:"+ param.getBodyHash());
            log.info("[localBodyHash]:"+ localBodyHash);}else{
            rt =true;}return rt;}/**
     * 对请求签名
     * <p>
     * MD5{参数串|body串|key}
     *
     * @param src
     * @param alg
     * @return
     */publicstaticStringsignature(String src,String alg){// FIXME 原文结构待定String sign =null;if(StringUtil.isNotEmpty(alg)){switch(alg.toUpperCase()){case MD5:
                    sign =MD5Util.MD5Encode(src,"UTF-8");break;case SM3:
                    sign =SM3Digest.hashHex(src,"UTF-8");break;default://不支持的算法
                    log.info("不支持算法:"+ alg);}}return sign;}/**
     * 对单层json进行key字母排序
     *
     * @param json
     * @return
     */publicstaticStringgetSortJson(Object json){if(json instanceofJSONArray|| json instanceofJSONObject){returnJSONObject.toJSONString(getSortMap(json));}elseif(json instanceofString){JSONObject jsonObject;try{
                jsonObject =JSONObject.parseObject((String) json);}catch(Exception e){thrownewRuntimeException("不是 JSON 对象: "+ json);}returnJSONObject.toJSONString(getSortMap(jsonObject));}else{thrownewRuntimeException("不是 JSON 对象: "+ json);}}publicstaticObjectgetSortMap(Object json){SortedMap map =newTreeMap();if(json instanceofJSONArray&&!((JSONArray) json).isEmpty()&&((JSONArray) json).get(0)instanceofJSONObject){JSONArray va =(JSONArray) json;for(int i =0; i < va.size(); i++){
                va.set(i,getSortMap(va.get(i)));}return json;}elseif(json instanceofJSONObject){Iterator<String> iteratorKeys =((JSONObject) json).keySet().iterator();while(iteratorKeys.hasNext()){String key = iteratorKeys.next();Object value =((JSONObject) json).get(key);if(value instanceofJSONObject|| value instanceofJSONArray){
                    map.put(key,getSortMap(value));}elseif(value !=null){
                    map.put(key, value);}}return map;}else{return json;}}}

三、Apifox编写公共脚本用于前置操作,设置接口请求签名sign

公共脚本主要用途是实现

脚本复用

,避免多处重复编写

相同功能的脚本

可以将多处都会用到的

相同功能的脚本

或者

通用的类、方法

,放到公共脚本里,然后所有接口直接引用公共脚本即可使用。

在项目设置里新建一个公共脚本,请求前根据一定规则生成公共的请求头,编写生成签名的代码,可参考官方使用文档,讲的都很详细👉接口签名如何处理:

请添加图片描述

请添加图片描述

脚本代码:

// 设置请求头timestampvar moment =require("moment");var timestamp =moment().format('YYYY-MM-DD HH:mm:ss')
console.log(timestamp)/**
 * 6位随机数
 */functiongetNonce(){let nonce = Math.random().toString().slice(-6);if(nonce.startsWith("0")){
    nonce =getNonce();}return nonce;}var nonce =getNonce();
console.log(nonce)// // 获取 Header 参数对象// var headers = pm.request.headers;// // 获取 key 为 field1 的 header 参数的值// var accessKey = pm.variables.replaceIn(headers.get("accessKey"));// console.log(accessKey)// 存放所有需要用来签名的参数let param ={};// 加入 query 参数let queryParams = pm.request.url.query;

queryParams.each(item=>{// if (item.value !== '') { // 非空参数值的参数才参与签名
  param[item.key]= item.value;// }});// 取 keylet keys =[];for(let key in param){// 注意这里,要剔除掉 sign 参数本身if(key !=='sign'){
        keys.push(key);}}// 转成键值对let paramPair =[];for(let i =0, len = keys.length; i < len; i++){let k = keys[i];
    paramPair.push(k +'='+encodeURIComponent(param[k]))// urlencode 编码}

paramPair.push(timestamp);
paramPair.push(nonce);
paramPair.push("cjf9hbd4rln75a58o3tc");// 最后加上 key// paramPair.push("key=" + key);// 拼接let stringSignTemp = paramPair.join('&');if(queryParams ==null|| queryParams ==''){
  stringSignTemp ="&"+ stringSignTemp;}
console.log(stringSignTemp);let sign = CryptoJS.MD5(stringSignTemp).toString();
console.log(sign);// 方案一:直接修改接口请求的 query 参数,注入 sign,无需使用环境变量。// 参考文档:https://www.apifox.cn/help/app/scripts/examples/request-handle/// queryParams.upsert({//     key: 'sign',//     value: sign,// });// 方案二:写入环境变量,此方案需要在接口里设置参数引用环境变量// 设置全局变量
pm.globals.set("reqSign", sign);
pm.globals.set("timestamp", timestamp);
pm.globals.set("nonce", nonce);

四、调用接口,验证权限

请添加图片描述

通过Apifox调用接口,成功返回数据,可以在控制台查看调用时发送的参数等信息:

请添加图片描述

请添加图片描述

另外分享一个MD5加密的脚本:

请添加图片描述

let password = pm.request.url.query.get('password');
console.log('原密码:'+ password);let newPwd = CryptoJS.MD5(password).toString();
console.log('MD5加密后:'+ newPwd);
pm.request.url.query.upsert({key:"password",value: newPwd,});

总结

作为一款国人开发的工具,Apifox已经很优秀了,起码比postman“智能化”不少,但是很烦网络上铺天盖地的广告软文,只有凭自己的实力赢得口碑才是硬道理!

以上

标签: postman 测试工具

本文转载自: https://blog.csdn.net/qq_26030541/article/details/126290372
版权归原作者 dearmrzhang 所有, 如有侵权,请联系我们删除。

“接口管理工具Apifox在前后端分离项目中的实践”的评论:

还没有评论