【web安全】API的安全策略

最近正在負責一個新平臺的構建和開發,有一個場景須要對應用作新增,修改和撤回的操做
起先是由於以前寫過類型的功能,不想在和之前同樣一個操做類型一個api,以爲代碼太過冗餘了。
因而有了如下的構思
  • 初版
    將當前界面全部api請求,合併成一個request,以type做爲操做類型的區分,data爲提交的數據
    clipboard.png
    這樣當前界面全部操做都使用一個接口來處理,而且問題統一處理前端

    1. 處理token失效
    2. 處理catch
    3. 處理通訊成功後都通知
    4. 處理權限
  • 優化版
    當設計成初版後,我以爲操做類型暴露在外面有些不妥,起先想的是後端生成隨機碼和對應的加密值,經過解密拿到方法名。
    後來優化了一下,加入了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);
      }
    • 這是返回的結構
      clipboard.png
前端就不上代碼了,稍微說下應該都能明白
    1. 進入界面的時候,請求getConfig
    2. 前端拿到數據進行解密
    3. 操做界面的時候,發送操做碼和數據
    4. 請求完成,拿到新的操做碼進行本地更新,並對以前的操做做出反應(數據更新/界面跳轉/彈框提示等)
  • 延伸版
    獲取界面配置,能夠放在一個任何界面都會訪問的地方,統一處理,後端配好路由的url便可
  • 解決/預防了哪些問題
    1.代碼冗餘問題
    2.爬蟲問題(因爲全部的操做入參都是動態返回且隨機生成,爬蟲們無法按着一個接口和數據爬取數據,增大了難度)
    3.非正常的訪問
以上就是我對API安全策略的想法,若有異議或新的方式歡迎評論留言。
相關文章
相關標籤/搜索