最近正在负责一个新平台的构建和开发,有一个场景需要对应用做新增,修改和撤回的操作
起先是因为之前写过类型的功能,不想在和以前一样一个操作类型一个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 config; if (!referer || !origin) { // todo 处理异常访问 return config; } else { var fontUrl = referer.replace(origin, '').split('?'); // 去除domain和url参数后的路径 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安全策略的想法,如有异议或新的方式欢迎评论留言。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。