最近正在负责一个新平台的构建和开发,有一个场景需要对应用做新增,修改和撤回的操作起先是因为之前写过类型的功能,不想在和以前一样一个操作类型一个 api,觉得代码太过冗余了。于是有了以下的构思
第一版 将当前界面所有 api 请求,合并成一个 request,以 type 作为操作类型的区分,data 为提交的数据 这样当前界面所有操作都使用一个接口来处理,并且问题统一处理
处理 token 失效
处理 catch
处理通信成功后都通知
处理权限
优化版 当设计成第一版后,我觉得操作类型暴露在外面有些不妥,起先想的是后端生成随机码和对应的加密值,通过解密拿到方法名。后来优化了一下,加入了 url 来源的判断,还能防止 postman 的攻击 后端代码如下:
redisImp 为 redis
utils 为工具类
token 和权限的检查放在了外层,进入方法的都当成 token 和权限通过的
const apiPrefix = ‘ApiType:’;
// 通过 viewConfig 生成对应配置
async function generateConfig (owner, viewConfig) {
var viewName = viewConfig.name; // 界面名称
var viewMethods = viewConfig.methods; // 界面所支持的操作方法
let key = apiPrefix + owner + ‘:’ + viewName;
await redisImp.del(key);
let para = [], config = [], secret = [];
// 生成 10 个长度为 12 的随机码
for (var i = 0; i < 10; i++) {
var randomKey = utils.generateRandomStr(12);
config.push(randomKey);
}
// 生成三个 10 一下的数字
var random1 = Math.ceil(Math.random() * 10);
var random2 = Math.ceil(Math.random() * 10);
var random3 = Math.ceil(Math.random() * 10);
// todo 检查 3 个随机数是否相等
var randomList = [random1, random2, random3];
// 生成随机码和操作方法的关联数据
viewMethods.forEach(function (value, index) {
para.push(config[randomList[index]]);
para.push(value);
secret.push(randomList[index]);
})
// aes 加密
var enc = utils.cryptedAES(secret.toString());
let redisResult = await redisImp.hSet(key, para);
if (redisResult.code === 200) {
return {
apis: config,
secret: enc
}
}
return null;
}
// 获取界面的配置
function getViewConfig (ctx) {
var referer = ctx.request.header.referer; // 原始 url
var origin = ctx.request.header.origin; // 来源
var fontUrl = referer.replace(origin, ”).split(‘?’); // 去除 domain 和 url 参数后的路径
var config;
switch (fontUrl[0]) {
case ‘/app/base’: {
config = {
name: ‘appBase’, // 界面名称
methods: [‘add’, ‘modify’, ‘retract’] // 界面操作权限
}
break;
}
default: {
// todo 处理异常攻击
}
}
return config;
}
// 获取配置,暴露给前端的 api 接口
const getConfig = async (ctx) => {
const fName = _name + ‘getConfig’;
lifecycleLog.info(‘[Enter] ‘ + fName);
// 获取当前用户 id
const redisResult = await redis.GetTokenValue(ctx, ‘id’);
let owner;
if (redisResult.code === 200) {
owner = redisResult.data;
} else {
ctx.body = redisResult;
return;
}
// 获取界面配置
var viewConfig = getViewConfig(ctx);
if (viewConfig) {
var result = await generateConfig(owner, viewConfig);
if (result) {
// 生成成功后返回给前端
ctx.body = Object.assign({code: 200}, result);
} else {
ctx.body = controller.dataError();
}
} else {
ctx.body = controller.dataError();
}
lifecycleLog.info(‘[Return] ‘ + fName);
}
const appBase = require(‘./appBase’)
// 处理应用界面的接口
const handleAppBaseData = async (ctx) => {
const fName = _name + ‘handleAppBaseData’;
lifecycleLog.info(‘[Enter] ‘ + fName);
var viewConfig = getViewConfig(ctx);
if (viewConfig) {
const name = ctx.request.body.name; // 前端传过来的操作码
const para = ctx.request.body.data; // 前端传过来的数据
let data;
try {
data = JSON.stringify(para);
} catch (err) {
ctx.body = controller.dataError();
return;
}
// 验证数据完整性
if (controller.dataMissed(ctx, fName, ctx.request.body, name + data)) {
return;
}
const redisResult = await redis.GetTokenValue(ctx, ‘id’);
let owner;
if (redisResult.code === 200) {
owner = redisResult.data;
} else {
ctx.body = redisResult;
return;
}
// 从 redis 拿到当前用户在当前界面的操做类型
let apiType = await redisImp.hGet(apiPrefix + owner + ‘:’ + viewConfig.name, name);
if (apiType.code === 200) {
if (apiType.data.length) {
var methods = apiType.data[0];
// 添加操作
if (methods === ‘add’) {
await appBase.add(ctx, para, owner);
} else {
let option = {
_id: para._id,
owner: owner
};
// 检测该用户是否拥有该 app
const gameResult = await commonModel.getInfo(ctx, collection, option);
if (gameResult) {
if (gameResult.code === 200) {
var gameDoc = gameResult.info[‘_doc’];
} else {
ctx.body = controller.dataError();
return;
}
} else {
ctx.body = controller.serverError();
return;
}
// 修改操作
if (methods === ‘modify’) {
await appBase.modify(ctx, para, gameDoc);
} else if (methods === ‘retract’) {// 撤回炒作
await appBase.retract(ctx, gameDoc);
} else {
ctx.body = controller.dataError();
return;
}
}
// 如果入库成功,则将新一轮的操作码反给前端
if (ctx.body.code === 200) {
var result = await generateConfig(owner, viewConfig);
ctx.body = Object.assign(ctx.body, result);
}
} else {
ctx.body = controller.dataError();
}
} else {
ctx.body = controller.serverError();
}
} else {
ctx.body = controller.dataError();
}
lifecycleLog.info(‘[Return] ‘ + fName);
}
这是返回的结构
前端就不上代码了,稍微说下应该都能明白
1. 进入界面的时候,请求 getConfig
2. 前端拿到数据进行解密
3. 操作界面的时候,发送操作码和数据
4. 请求完成,拿到新的操作码进行本地更新,并对之前的操作作出反应(数据更新 / 界面跳转 / 弹框提示等)
延伸版获取界面配置,可以放在一个任何界面都会访问的地方,统一处理,后端配好路由的 url 即可
解决 / 预防了哪些问题 1. 代码冗余问题 2. 爬虫问题(由于所有的操作入参都是动态返回且随机生成,爬虫们没法按着一个接口和数据爬取数据,增大了难度)3. 非正常的访问
以上就是我对 API 安全策略的想法,如有异议或新的方式欢迎评论留言。