How to add authorization header when runtime impor

2020-07-17 08:03发布

The purpose of this task is to make it impossible to download the Vue-component package (*.js file) knowing the address of the component, but not having an access token.

I'm developing an access control system and a user interface in which the set of available components depends on the user's access level.

The system uses the JSON API and JWT authorization. For this, Axios is used on the client side. To build the application, we use Webpack 4, to load the components, we use the vue-loader.

After the user is authorized, the application requests an array of available routes and metadata from the server, then a dynamically constructed menu and routes are added to the VueRouter object.

Below I gave a simplified code.

            import axios from 'axios'
            import router from 'router'

            let API = axios.create({
              baseURL: '/api/v1/',
              headers: {
                Authorization: 'Bearer mySecretToken12345'
              }
            })

            let buildRoutesRecursive = jsonRoutes => {
              let routes = []
              jsonRoutes.forEach(r => {
                let path = r.path.slice(1)
                let route = {
                  path: r.path,
                  component: () => import(/* webpackChunkName: "restricted/[request]" */ 'views/restricted/' + path)
                  //example path: 'dashboard/users.vue', 'dashboard/reports.vue', etc...
                }
                if (r.children)
                  route.children = buildRoutesRecursive(r.children)
                routes.push(route)
              })
              return routes
            }

            API.get('user/routes').then(
              response => {

                /*
                  response.data = 
                        [{
                      "path": "/dashboard",
                      "icon": "fas fa-sliders-h",
                              "children": [{
                        "path": "/dashboard/users",
                        "icon": "fa fa-users",
                                }, {
                        "path": "/dashboard/reports",
                        "icon": "fa fa-indent"
                                }
                            ]
                        }
                    ]
                */

                let vueRoutes = buildRoutesRecursive(response.data)
                router.addRoutes(vueRoutes)   
              },
              error => console.log(error)
            )

The problem I'm having is because Webpack loads the components, by adding the 'script' element, and not through the AJAX request. Therefore, I do not know how to add an authorization header to this download. As a result, any user who does not have a token can download the code of the private component by simply inserting his address into the navigation bar of the browser.

import dashboard-reports.vue

Ideally, I would like to know how to import a vue component using Axios.

import using ajax

Or, how to add an authorization header to an HTTP request.

Auth header in script request

3条回答
放我归山
2楼-- · 2020-07-17 08:46

I needed something similar and came up with the following solution. First, we introduce a webpack plugin that gives us access to the script element before it's added to the DOM. Then we can munge the element to use fetch() to get the script source, and you can craft the fetch as needed (e.g. add request headers).

In webpack.config.js:

/*
 * This plugin will call dynamicImportScriptHook() just before
 * the script element is added to the DOM. The script object is
 * passed to dynamicImportScriptHook(), and it should return
 * the script object or a replacement.
 */
class DynamicImportScriptHookPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "DynamicImportScriptHookPlugin", (compilation) =>
        compilation.mainTemplate.hooks.jsonpScript.tap(
          "DynamicImportScriptHookPlugin", (source) => [
            source,
            "if (typeof dynamicImportScriptHook === 'function') {",
            "  script = dynamicImportScriptHook(script);",
            "}"
          ].join("\n")
        )
    );
  }
}

/* now add the plugin to the existing config: */
module.exports = {
   ...
   plugins: [
     new DynamicImportScriptHookPlugin()
   ]
}

Now, somewhere convenient in your application js:

/*
 * With the above plugin, this function will get called just
 * before the script element is added to the DOM. It is passed
 * the script element object and should return either the same
 * script element object or a replacement (which is what we do
 * here).
 */
window.dynamicImportScriptHook = (script) => {
  const {onerror, onload} = script;
  var emptyScript = document.createElement('script');
  /*
   * Here is the fetch(). You can control the fetch as needed,
   * add request headers, etc. We wrap webpack's original
   * onerror and onload handlers so that we can clean up the
   * object URL.
   *
   * Note that you'll probably want to handle errors from fetch()
   * in some way (invoke webpack's onerror or some such).
   */
  fetch(script.src)
    .then(response => response.blob())
    .then(blob => {
      script.src = URL.createObjectURL(blob);
      script.onerror = (event) => {
        URL.revokeObjectURL(script.src);
        onerror(event);
      };
      script.onload = (event) => {
        URL.revokeObjectURL(script.src);
        onload(event);
      };
      emptyScript.remove();
      document.head.appendChild(script);
    });
  /* Here we return an empty script element back to webpack.
   * webpack will add this to document.head immediately.  We
   * can't let webpack add the real script object because the
   * fetch isn't done yet. We add it ourselves above after
   * the fetch is done.
   */
  return emptyScript;
};
查看更多
叛逆
3楼-- · 2020-07-17 08:46

Although sspiff's answer looks quite promising, it did not work directly for me.

After some investigation this was mainly due to me using Vue CLI 3 and thus a newer version of webpack. (which is kinda weird as sspiff mentioned using webpack 4.16.1).

Anyway to solve it I used the following source: medium.com, Which gave me the knowledge to edit the given code.

This new code is situated in vue.config.js file:

/*
 * This plugin will call dynamicImportScriptHook() just before
 * the script element is added to the DOM. The script object is
 * passed to dynamicImportScriptHook(), and it should return
 * the script object or a replacement.
 */
class DynamicImportScriptHookPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "DynamicImportScriptHookPlugin", (compilation) =>
        compilation.mainTemplate.hooks.render.tap(
          {
            name: "DynamicImportScriptHookPlugin",
            stage: Infinity
          },
          rawSource => {
          const sourceString = rawSource.source()

          if (!sourceString.includes('jsonpScriptSrc')) {
            return sourceString;
          } else {
            const sourceArray = sourceString.split('script.src = jsonpScriptSrc(chunkId);')

            const newArray = [
              sourceArray[0],
              'script.src = jsonpScriptSrc(chunkId);',
              "\n\nif (typeof dynamicImportScriptHook === 'function') {\n",
              "  script = dynamicImportScriptHook(script);\n",
              "}\n",
              sourceArray[1]
            ]

            return newArray.join("")
          }
        }
      )
    );
  }
}

module.exports = {
  chainWebpack: (config) => {
    config.plugins.delete('prefetch')
  },
  configureWebpack: {
    plugins: [
      new DynamicImportScriptHookPlugin()
    ]
  }
}

The second piece of code provided by sspiff has stayed the same and can be placed in the App.vue file or the index.html between script tags.

Also to further improve this answer I will now explain how to split the chunks in Vue CLI 3 for this specific purpose.

as you can see I also added the chainWebpack field to the config. This makes sure that webpack does not add prefetch tags in the index.html. (e.g. it will now only load lazy chunks when they are needed)

To further improve your splitting I suggest changing all your imports to something like:

component: () => import(/* webpackChunkName: "public/componentName" */ /* webpackPrefetch: true */'@/components/yourpubliccomponent')

component: () => import(/* webpackChunkName: "private/componentName" */ /* webpackPrefetch: false */'@/components/yourprivatecomponent')

This will make sure that all your private chunks end up in a private folder and that they will not get prefetched. The public chunks will end up in a public folder and will get prefetched.

For more information use the following source how-to-make-lazy-loading-actually-work-in-vue-cli-3

Hope this helps anyone with this problem!

查看更多
Ridiculous、
4楼-- · 2020-07-17 08:55

To perform a simple component download using an access token, you can do the following...

1) Use asynchronous component loading with file extraction. Use webpackChunkName option to separate file or directory/file, like:

components: {
    ProtectedComp: () => import(/* webpackChunkName: "someFolder/someName" */ './components/protected/componentA.vue')
  }

2) Configure server redirection for protected files or direcory. Apache htaccess config for example:

RewriteRule ^js/protected/(.+)$ /js-provider.php?r=$1 [L]

3) write a server-side script that checks the token in the header or cookies and gives either the contents of .js or 403 error.

查看更多
登录 后发表回答