最近正在負責一個新平臺的構建和開發,有一個場景須要對應用作新增,修改和撤回的操做
起先是由於以前寫過類型的功能,不想在和之前同樣一個操做類型一個api,以爲代碼太過冗餘了。
因而有了如下的構思
初版
將當前界面全部api請求,合併成一個request,以type做爲操做類型的區分,data爲提交的數據
這樣當前界面全部操做都使用一個接口來處理,而且問題統一處理前端
優化版
當設計成初版後,我以爲操做類型暴露在外面有些不妥,起先想的是後端生成隨機碼和對應的加密值,經過解密拿到方法名。
後來優化了一下,加入了url來源的判斷,還能防止postman的攻擊
後端代碼以下:redis
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. 請求完成,拿到新的操做碼進行本地更新,並對以前的操做做出反應(數據更新/界面跳轉/彈框提示等)
以上就是我對API安全策略的想法,若有異議或新的方式歡迎評論留言。