前言
近期由于客户现场出现了非法接口的调用,为了处理此类问题和杜绝后续问题的出现。针对公司现有的项目实现接口加密,防止接口被恶意的调用。
设计思路:客户端存储超级私钥,客户端返回加密的公钥。客户端先使用超级私钥对公钥进行解密,使用解密出来的公钥对接口进行加密。把加密后的字符串进行验签发送给服务器,服务器进行字符串对比。
相关引用文件
文件的引用这里就只放截图了,源文件网上很容易找到的。这里使用jsencrypt.min.js
是为了节省小程序的空间。
获取服务器的加密公钥
var baseURL = require('./api.js');
var superPrivateKey = "MIIEpQIBAAKCAQEAyNftZEtP9PdwCZT8ZE8xSzuLA8tSZbczwBaLg+NiWKibCGsk11YXYcL8kZ9DBxVdpv1ysp7vGBu6QdmDKeymeae6HCEu3ThDjzGMbCCITswIWjT01Y7Fa6MQlbYJZD55MY3w/ZWDnzsjuTF6l3KJodqUwhXUlUvTbi60gdQXOmgNUE3LkGc9dsDonKBId46WLMY92B3vlLoA/GArBeCZQcwF29ztfrfvRP2tCtvV9tsqoRXSJXzbEgI3fX2/WVt9drXgMd9ZekM53PZwwEHQyEwnLrilOTAzlmzGO2ZbXY+4ahUo2iOH5WAvA65Y/qw7QgIzT24ooJ1HY1eyyFJUnwIDAQABAoIBAQC0lWF/Yi/8RFbaZrkgwAvEZz8xJClsB3NibWL4LQUKTl9HDH9NlrzjS9yoWph7z/wq34u3iyvTc2rfNmG22m88x1CRZkyq36HvKE/oEMA+iTmhUigptHtRsoaA9fIvzfROWB+tAjgcfaI7K3/cmEGj49MJR3Oi4VCzqw7mBPr1qWvNFx5vd4Rwjy4qO5K+BAlx8+yQ87z04tULlzUqBPIPK8WGb98TlG1CmMw5/ZQo1HfjxA82+8AO3qMapPDC6hzUBPvS+AQ2NxRdjceDxPmNzA2s2Kp2DIN3sQalSSy3+9ESLQpwwLhSJSmX/6E1XZFhv76nd3zOtbSUrzLBwzuxAoGBAPaVYfGMod/9eAZGQsCSv3sGQSis//jhjtHxef3VMdhGr3vgCmokLb1RlP50zVYlXYh3qvksn7ZjC5XkVbVDofNC4MJatzi4rIpQ/uUNmlxMMhZkwn8S4C9gIgkxXsY4siSb4zNKT6SxOFazi4qeMbwE+PlaYe2mxwl+iizw4slDAoGBANCDYwNEVFUE6F+ver9Lt25mBVP8L+pOjXSCGb3c9M2YvKVKuEZcOAwE/HnZms5xLAZGG7SCVbkLCUlOHoKigbKYr1jEvujMhbSF0IuhXHSnuvWmUX1HSo/bzdEyLEaQyZ+G2bQpYYUgKjEtfPcbHuDelUw99JY3ghoV04vv3DN1AoGBAK66JaAK/f2BV1Zi3RQmKEbdpLhU9kD+W7yKdt4V/u75D4ogtGCH6F1ZfNFeJM5hRcjYuy87nqSXxHLfTJhYJ17/ydIOg/xOZ/zO7f+SxwmV+HwDxApVbsRDQ3ruH/En5ZupVrJWet8BsSFGsp9z/1vyzhWrJO9ImYsxvmmf+6OPAoGALlMCTFeB5OGSPq/dtWI8/mnsBRyiCIwrIRdGYMgWGxcz0gUnq3oReZoh/XA61GKQRVSOEyxhnxq0lXSlkqBH8EW7rx0GzPGjQtf33Q2cXM5m2ux4bjzIc+2BbFiZPZQtNyPeegg3gjwDI6nXeY6s7YiF4spg7H6oiOMQfKZtZYECgYEAttlJveIjoMWNrnPWeegZ0VU+6HT7A4okcIIE0CLEmE9qVLoGGnehgpntf71spCePJOJhaPydZsDZPSrz916gHtg1vGwoApAgecweom3s95hKyOMsN6LoZRvrR7mx9UQ4Qjaq8bZzzxhF+XsvzC9QYBVUzeZp6y2rkXgKktBQ37c="
var Encrypt = require('../utils/jsencrypt.min.js');
var encryptor = new Encrypt.JSEncrypt();
function timest() {
let tmp = Date.parse(new Date()).toString();
tmp = tmp.substr(0, 10);
return tmp;
}
function getPrivatekey() {
return new Promise(function(resolve, reject) {
let time = timest();
wx.request({
url: baseURL.baseURL + 'api/services/app/Setting/GetCerPriKeyAsync?timestamp=' + time,
data: '',
header: {},
method: 'Get',
success: function(res) {
encryptor.setPrivateKey(superPrivateKey);
let array = res.data.Result.Data;
let sourcePrivate = '';
for (let a = 0; a < array.length; a++) {
// sourcePrivate = encryptor.decrypt(array[0]) + array[1];
if (a == 0) {
sourcePrivate += encryptor.decrypt(array[a]);
} else {
sourcePrivate += array[a];
}
}
console.log("原始私钥" + sourcePrivate);
resolve(sourcePrivate);
},
fail: function(res) {
console.log(res)
reject(res)
},
complete: function(res) {},
})
});
}
module.exports = {
getPrivatekey: getPrivatekey
}
baseURL
是定义接口地址,在获取加密公钥的时候由于RSA
自身的限制解密的字节大小最多是128
个。所以在此处采用分段解密,同时为了优化小程序首次的等待时间。只返回部分公钥,并且只有第一段公钥是加密的。这在很大程度上减少了等待的时间,让用户的体验更为流畅。
在这里将获取加密公钥的方法封账成一个Promise
方法,是为了保证小程序进行编译的时候在app.js
尚未执行完成的情况先,index.js
中的方法进行先行调用时没有公钥的情况。
处理首次获取加密公钥失败的情况
//app.js
// 引入请求文件
const http = require('/server/request.js');
const sm2 = require('/server/sm2.js');
const api = require('/server/api.js');
const util = require('/utils/weapp.js');
App({
globalData: {
uuid: ''
},
onLaunch: function(e) {
this.init();
this.globalData.uuid = wx.getStorageSync('uuid');
wx.clearStorageSync();
wx.setStorageSync('uuid', this.globalData.uuid);
},
// 初始化
async init() {
await this.getPrivatekey(); // 请求数据
this.getHomeSetting(); // 等待请求数据成功后
this.checkUpdateVersion(); //更新版本号
},
// 获取系统设置
getHomeSetting() {
let that = this;
http.requestLoading('api/services/app/CRMMemberService/GetHomeSetting', {}, 'GET').then(res => {
if (res.data.Result.Code === 0) {
let skin = "";
if (res.data.Result.Data.SkinColor == 0) {
skin = "orange";
that.globalData.skin = skin;
that.setorangeTabBar();
} else if (res.data.Result.Data.SkinColor == 1) {
skin = "blue";
that.globalData.skin = skin;
that.setBlueTabBar()
} else if (res.data.Result.Data.SkinColor == 2) {
skin = "pink";
that.globalData.skin = skin;
}
//保存到本地
wx.setStorageSync('skin', skin);
wx.setStorageSync('isShow', res.data.Result.Data.ShopGoodClassShowType);
wx.setStorageSync('ScoreShopBannerImg', res.data.Result.Data.ScoreShopBannerImg);
}
})
},
// 小程序版本检测
checkUpdateVersion() {
//判断微信版本是否 兼容小程序更新机制API的使用
if (wx.canIUse('getUpdateManager')) {
//创建 UpdateManager 实例
const updateManager = wx.getUpdateManager();
console.log('是否进入模拟更新');
//检测版本更新
updateManager.onCheckForUpdate(function(res) {
console.log('是否获取版本');
// 请求完新版本信息的回调
if (res.hasUpdate) {
//监听小程序有版本更新事件
updateManager.onUpdateReady(function() {
console.log('获取版本');
wx.showModal({
title: '更新提示',
content: '新版本已经准备好,是否马上重启小程序?',
success: function(res) {
if (res.confirm) {
//TODO 新的版本已经下载好,调用 applyUpdate 应用新版本并重启 ( 此处进行了自动更新操作)
updateManager.applyUpdate();
}
}
})
})
updateManager.onUpdateFailed(function() {
// 新版本下载失败
wx.showModal({
title: '已经有新版本喽~',
content: '请您删除当前小程序,到微信 “发现-小程序” 页,重新搜索打开哦~',
})
})
}
})
} else {
//TODO 此时微信版本太低(一般而言版本都是支持的)
wx.showModal({
title: '溫馨提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
})
}
},
//获取私钥
getPrivatekey() {
return new Promise((resolve, reject) => {
if (this.globalData.privateKey) {
resolve(this.globalData.privateKey);
} else {
sm2.getPrivatekey().then((res) => {
this.globalData.privateKey = res;
resolve(res);
})
.catch((err) => {
console.error(err);
reject(err);
})
}
})
}
})
在app.js
中针对获取公钥的方法外层又封装了一层Promise
方法,是为了使用async await
的方法。使获取设置信息的接口等待获取到公钥后在进行请求。在调用getPrivatekey方法的时候,会先判断当前是否已经有公钥了。存在的话则会直接使用不在请求获取公钥的接口。
加密参数进行验签及多并发处理
var baseURL = require('./api.js');
var appJs;
// 展示进度条的网络请求
// url:网络请求的url
// params:请求参数
// message:进度条的提示信息
// methods:请求方式
// hideLoad:隐藏wx.showLoading
// hideToast:隐藏wx.showToast
//用于生成uuid
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
function guid() {
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
}
function objKeySort(obj) { //排序的函数
let newkey = Object.keys(obj).sort();
//先用Object内置类的keys方法获取要排序对象的属性名,再利用Array原型上的sort方法对获取的属性名进行排序,newkey是一个数组
let newObj = {}; //创建一个新的对象,用于存放排好序的键值对
for (let i = 0; i < newkey.length; i++) { //遍历newkey数组
newObj[newkey[i]] = obj[newkey[i]]; //向新创建的对象中按照排好的顺序依次增加键值对
}
return newObj; //返回排好序的新对象
}
function timest() {
let tmp = Math.round(new Date() / 1000);
return tmp;
}
var uuid;
var token = "";
var _token_ = "";
var rsa = require('../utils/cryptojs-master/cryptojs.js');
var Encrypt = require('../utils/jsencrypt.min.js');
var encryptor = new Encrypt.JSEncrypt();
function requestLoading(url, params, message, methods, hideLoad, hideToast, CallbackTimes = 0) {
if (!appJs) {
appJs = getApp();
}
let newUrl = url;
let newParams = params;
return new Promise(function(resolve, reject) {
appJs.getPrivatekey().then(res => {
encryptor.setPrivateKey(res);
var method = methods || "GET";
params = params || {};
params.MpContentType = 1;
if (url.substring(0, 1) == '/') {
url = url.substr(1);
}
// token存在直接使用,不存在为空
if (appJs) {
token = appJs.globalData.token;
_token_ = token;
}
// url拼接token和__shop__
let __shop__ = wx.getStorageSync('organizationid') || '';
// uuid存在直接使用,否则存储本地使用新的
uuid = wx.getStorageSync('uuid');
if (!uuid) {
uuid = guid();
wx.setStorageSync('uuid', uuid);
}
if (url.lastIndexOf("?") == -1) {
url = url + '?_token_=' + _token_ + '&__shop__=' + __shop__ + '&uuid=' + uuid
} else {
url = url + '&_token_=' + _token_ + '&__shop__=' + __shop__ + '&uuid=' + uuid
}
let SourceStr = '';
let Timest = timest();
if (method == "GET" || method =="get") {
let sha256sign = "";
let str = "";
for (let i = 0; i < 2; i++) {
params.timestamp = Timest;
params = objKeySort(params);
str = Object.keys(params).map(function(key) {
return "".concat(key, "=").concat(params[key]);
}).join('&');
str = '_token_=' + _token_ + '&__shop__=' + __shop__ + '&uuid=' + uuid + '&' + str;
console.log(str)
sha256sign = encryptor.sign(str, rsa.Crypto.SHA256, "SHA256");
if (sha256sign.substring(sha256sign.length - 2) == '==') {
break
}
Timest = Timest + 1;
}
if (sha256sign.substring(sha256sign.length - 2) != '==') {
wx.showModal({
title: '提示',
content: '签名错误',
showCancel: false,
mask: true
})
return
}
SourceStr = encodeURIComponent(str);
sha256sign = encodeURIComponent(sha256sign);
params.sign = sha256sign;
}
if ((method == "POST" || method == 'post') || (method == "PUT" || method =='put')) {
let index = url.lastIndexOf("?");
let str = url.substring(index + 1, url.length);
let parameter = JSON.stringify(params);
let sha256sign = "";
for (let i = 0; i < 2; i++) {
str = str + '×tamp=' + Timest + parameter;
sha256sign = encryptor.sign(str, rsa.Crypto.SHA256, "SHA256");
if (sha256sign.substring(sha256sign.length - 2) == '==') {
break
}
Timest = Timest + 1;
}
if (sha256sign.substring(sha256sign.length - 2) != '==') {
wx.showModal({
title: '提示',
content: '签名错误',
showCancel: false,
mask: true
})
return
}
SourceStr = str;
sha256sign = encodeURIComponent(sha256sign);
url = url + '×tamp=' + Timest + '&sign=' + sha256sign;
}
if (method == "DELETE" || method =="delete") {
let index = url.lastIndexOf("?");
let str = '';
let sha256sign = "";
for (let i = 0; i < 2; i++) {
str = url.substring(index + 1, url.length) + '×tamp=' + Timest + JSON.stringify(params);
sha256sign = encryptor.sign(str, rsa.Crypto.SHA256, "SHA256");
if (sha256sign.substring(sha256sign.length - 2) == '==') {
break
}
Timest = Timest + 1;
}
if (sha256sign.substring(sha256sign.length - 2) != '==') {
wx.showModal({
title: '提示',
content: '签名错误',
showCancel: false,
mask: true
})
return
}
SourceStr = encodeURIComponent(str);
sha256sign = encodeURIComponent(sha256sign);
url = url + '×tamp=' + Timest + '&sign=' + sha256sign;
}
var Device = "device/" + uuid
var appId = ''
if (wx.canIUse('getAccountInfoSync')) {
const accountInfo = wx.getAccountInfoSync();
appId = accountInfo.miniProgram.appId;
var MicroApp = accountInfo.miniProgram.version;
if (MicroApp) {
Device = Device + " microapp/" + MicroApp
}
}
wx.request({
url: baseURL.baseURL + url,
data: params,
header: {
'Content-Type': 'application/json',
'Abp.Tenantld': '2',
'authorization': token,
"Device": Device
},
method: method,
success: function(res) {
wx.hideLoading()
//请求成功
//判断状态码---errCode状态根据后端定义来判断
if (res.statusCode == 200) {
if (res.data.Result.Code == 0) {
resolve(res);
} else if (res.data.Result.Code == 401 && url != 'api/services/app/MemberSession/GetLoginState') {
resolve(res);
} else if (res.data.Result.Code == 404 || res.data.Result.Code == 101) {
resolve(res);
console.log('请求参数:' + params, '请求接口:' + url, '返回结果:' + res)
} else {
resolve(res);
console.log('请求参数:' + params, '请求接口:' + url, '返回结果:' + res)
}
} else if (res.statusCode == 401) {
wx.reLaunch({
url: '/pages/member/loginAndRegister/loginAndRegister',
})
} else if (res.statusCode == 402) {
wx.showModal({
title: '提示',
content: res.data.Error.Message,
showCancel: false,
mask: true
})
reject(res);
} else {
if (res.data.Error.Code == 503) {
if (CallbackTimes < 3) {
CallbackTimes++;
requestLoading(newUrl, newParams, message, methods, hideLoad, hideToast, CallbackTimes);
}
}
wx.hideToast();
setTimeout(function() {
wx.showToast({
title: '请求异常',
icon: 'none',
mask: true
})
}, 500)
console.log('请求参数:' + params, '请求接口:' + url, '返回结果:' + res)
//其他异常
reject('运行时错误,请稍后再试');
}
},
fail: function(res) {
console.log('请求参数:' + params, '请求接口:' + url, '返回结果:' + res)
//请求失败
reject(res);
},
complete: function(res) {},
})
})
})
}
module.exports = {
requestLoading: requestLoading
}