Full project address: vue-element-admin

Series of articles:

  • Hand touch hand, take you to use vue backstage series a (basic)
  • Hand to hand, take you to use vUE masturbation background series two (login permission)
  • Hand to hand, take you to use the vUE backstage series three (Actual combat)
  • Hand touch, take you with vue Lute background series four (vueAdmin a minimalist background base template)
  • Hand touch hand, take you to use vUE Background Lift series five (v4.0 new version)
  • Hand to hand, take you to wrap a Vue Component
  • Hand to hand, with your elegant use of icon
  • Using webpack4 with proper posture (part 1)
  • Using webpack4 with proper posture

preface

It took half a month to write the second tutorial. But I am a business ape, every day by our company’s products abuse, before and sick rest for a few days, we forgive you.

Get to the point, do background project is different from other projects, authority verification and security is very important, can be said to be a background project at the beginning of the basic core functions must be considered and built. All we need to do is: different permissions correspond to different routes, and the sidebar needs to be generated asynchronously according to different permissions. Here is a brief introduction to my login and permission verification ideas.

  • Login: After the user fills in the account and password, the server verifies whether it is correct. After the verification is successful, the server will return a token. After receiving the token (I will store the token in the cookie to ensure that the login status of the user can be remembered after refreshing the page), The front-end will then pull a user_info interface based on the token to obtain user details (such as user permissions, user names and other information).
  • Permission verification: Obtains the user’s role based on the token, dynamically calculates routes to the user’s permission based on the user’s role, and dynamically mounts these routes through router.addRoutes.

All of the above data and operations are controlled by vuEX global management. (Note: The content of Vuex will also be lost after refreshing the page, so you need to repeat the above operations.) Next, we will implement the system step by step.

Login article

First let’s implement the basic login function regardless of the permissions.

Randomly find a blank page on the two input boxes, one is the login account, one is the login password. Place another login button. We attach the click event to the login button and submit the account and password to the server for verification after clicking login. This is a simple login page. If you want to be a little more polished, you can simply verify your account and password before submitting it to the server. Detailed code

Click event triggers login action:

this.$store.dispatch('LoginByUsername'.this.loginForm).then((a)= > {
  this.$router.push({ path: '/' }); // Redirect to home page after successful login
}).catch(err= > {
  this.$message.error(err); // Failed to log in
});

Copy the code

action:

LoginByUsername({ commit }, userInfo) {
  const username = userInfo.username.trim()
  return new Promise((resolve, reject) = > {
    loginByUsername(username, userInfo.password).then(response= > {
      const data = response.data
      Cookies.set('Token', response.data.token) // Store the token in a cookie after successful login
      commit('SET_TOKEN', data.token)
      resolve()
    }).catch(error= > {
      reject(error)
    });
  });
}
Copy the code

After successful login, the server will return a token (the token is a key that can uniquely identify the user’s identity), and then we will store the token in the local cookie, so that we can remember the user’s login status when we open the page or refresh the page next time, so that we do not need to go to the login page to log in again.

Ps: In order to ensure security, all the Expires/ max-age in the background of our company are sessions, which are lost when the browser is closed. Re-opening the browser requires re-login verification. The backend will also refresh the token at a fixed point every week, so that all background users can re-log in once to ensure that background users will not be arbitrarily used due to computer loss or other reasons.

Obtaining User information

After the user logs in successfully, we intercept the route in the global hook router.beforeEach to determine whether the user has obtained the token. After obtaining the token, we need to obtain the user’s basic information

//router.beforeEach
if(store. Getters. Roles. Length = = = 0) {/ / whether the current user has finished pull user_info information store. Dispatch ('GetInfo'// Pull user_info const roles = res.data.role; next(); } / / resolve hook)Copy the code

As mentioned above, I only stored one user’s token locally, and did not store other user information (such as user authority, user name, user profile picture, etc.). Some people ask why not save some other user information as well? The main reasons are as follows:

If I log in to another computer and change my user name, and then log in to the computer with the previous user information, it will read the name in the local cookie by default, and will not pull the new user information.

Therefore, the current policy is: the page will check the cookie to see if there is a token, if there is no token, go through the previous part of the process to log in again, if there is a token, the token will be returned to the backend to pull user_Info, to ensure that the user information is up to date. Of course, if it is a single sign-on function, the user information can be stored locally. When you log on to one computer, the other is taken offline, so you always log back in to get the latest content.

At the code level, I recommend keeping login and get_user_info separate. In this era of full backend microservices, backend students also want to write elegant code


Permission to post

To start with my main idea of permission control, there will be a routing table at the front, which represents the permissions that each route can access. After the user logs in, the user obtains the role of the user through the token, calculates the route to the user based on the user’s role, and then dynamically mounts the route through router.addRoutes. But these controls are only page level, to put it bluntly, the front end no matter how to do permission control is not absolutely safe, the back end permission verification is not escape.

Our company is now the front end to control the page-level permissions. Users with different permissions display different sidebars and limit the pages they can enter (a little button level permissions control is also done). The back end will verify each operation involving requests and verify whether it has the permissions for this operation. Every backend request, no matter get or POST, will let the front-end carry the user’s token in the request header, and the back-end will verify whether the user has the permission to perform the operation according to the token. If there is no permission, a corresponding status code is thrown, and the front end detects the status code and performs corresponding operations.

Permissions front-end or back-end to control?

Many people say that the routing table of their company is dynamically generated at the back end according to user permissions. The reasons for our company are as follows:

  • The continuous iteration of the project will make you extremely painful. The front end will develop a new page and have to let the back end configure routing and permissions. It reminds us of the horrible time when the front and back end were not separated and dominated by the back end.
  • Second, take our business, although also have permission to verify the back-end indeed, but its validation is by it’s for business, such as super editors can publish articles, and internship editor can edit articles cannot be released, but for the front-end whether super editor or internship has access to edit page article. Therefore, the division of front-end and back-end permissions is not consistent.
  • Also, mounting routes asynchronously before VUe2.2.0 was a hassle! But fortunately, the official also out of the new API, although the original intention is to solve the PAIN points of SSR…

addRoutes

Previously, it was difficult to dynamically return the front-end route through the back-end because vue-Router had to be mounted before the VUE was instantiated, making it difficult to dynamically change. Router. AddRoutes has been added since Vue2.2.0

Dynamically add more routes to the router. The argument must be an Array using the same route config format with the routes constructor option.

With this we can do permission control relatively easily. There are a lot of if/else logic decisions in the permission control code, which is quite complex and very coupling. By the way, there are a lot of if/else logic decisions in the permission control code.


The specific implementation

  1. The VUe-Router is mounted when the Vue instance is created, but the Vue-Router mounts some public pages for login or non-permission purposes.
  2. After the user logs in, the user obtains the role and compares the role with the required permissions on each page of the routing table to generate a routing table accessible to the end user.
  3. Call router.addroutes (store.getters. AddRouters) to add a route accessible to the user.
  4. Manage routing tables using VUEX, rendering sidebar components based on routes accessible in VUEX.

router.js

First, we implement router.js routing tables. Here we use the front-end control route as an example.

// router.js
import Vue from 'vue';
import Router from 'vue-router';

import Login from '.. /views/login/';
const dashboard = resolve => require(['.. /views/dashboard/index'], resolve); / / using the vue - routerd [Lazy Loading Routes] (https://router.vuejs.org/en/advanced/lazy-loading.html) / / general routing table all permissions // For example, the home page and login page and some public pages without permissionexport const constantRouterMap = [
  { path: '/login', component: Login },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    name: 'home',
    children: [{ path: 'dashboard', Component: dashboard}]},] // Only constantRouter is mounted when instantiating vueexportdefault new Router({ routes: constantRouterMap }); // Asynchronously mounted routes // Dynamically loaded routing tables based on permissionsexport const asyncRouterMap = [
  {
    path: '/permission',
    component: Layout,
    name: 'Permission Tests',
    meta: { role: ['admin'.'super_editor'}, // children: [{path:'index',
      component: Permission,
      name: 'Permission Test Page',
      meta: { role: ['admin'.'super_editor'}}}, {path:The '*', redirect: '/ 404', hidden: true}];Copy the code

Here we use meta tags to indicate the access permissions of the modified page according to the method officially recommended by vue-Router. For example, meta: {role: [‘admin’,’super_editor’]} indicates that only admin and super editors can enter this page.

Matters needing attention: One important thing to note here is that 404 pages must be loaded last. If a 404 is declared with a constantRouterMap, all subsequent pages will be blocked to 404. See addRoutes when You’ve got a wildcard Route for 404s does not work for more details

main.js

The key of the main. Js

// main.js
router.beforeEach((to, from, next) => {
  if(store.getters. Token) {// Check whether there is a tokenif (to.path === '/login') {
      next({ path: '/' });
    } else {
      if(store. Getters. Roles. Length = = = 0) {/ / whether the current user has finished pull user_info information store. Dispatch ('GetInfo'). Then (res => {// pull info const roles = res.data.role; store.dispatch('GenerateRoutes'Router.addroutes (store.getters.addrouters) router.addroutes (store.getters.addrouters) to, replace:true}) // the hack method ensures that addRoutes are complete,set the replace: true so the navigation will not leave a history record
          })
        }).catch(err => {
          console.log(err);
        });
      } else{next() // If the user has permissions, all accessible routes are generated. If the user has permissions, the user will automatically enter the 404 page}}}else {
    if(whiteList.indexOf(to.path) ! == -1) {// Next (); }else {
      next('/login'); // Otherwise all redirects to the login page}}});Copy the code

Router.beforeeach here also combines some of the login logic code from the previous chapter.

addRoutes
if/else
addRoutes
404

Router. AddRoutes next() may fail because the router is not added completely at next()

next(‘/’) or next({ path: ‘/’ }): redirect to a different location. The current navigation will be aborted and a new one will be started.

In this way, we can easily avoid the previous problem by next(to). This line re-enters the route. beforeEach hook and releases the hook with next() to ensure that all routes are hung.

store/permission.js

So let’s talk about GenerateRoutes Action

// store/permission.js
import { asyncRouterMap, constantRouterMap } from 'src/router';

function hasPermission(roles, route) {
  if (route.meta && route.meta.role) {
    return roles.some(role => route.meta.role.indexOf(role) >= 0)
  } else {
    return true
  }
}

const permission = {
  state: {
    routers: constantRouterMap,
    addRouters: []
  },
  mutations: {
    SET_ROUTERS: (state, routers) => {
      state.addRouters = routers;
      state.routers = constantRouterMap.concat(routers);
    }
  },
  actions: {
    GenerateRoutes({ commit }, data) {
      return new Promise(resolve => {
        const { roles } = data;
        const accessedRouters = asyncRouterMap.filter(v => {
          if (roles.indexOf('admin') > = 0)return true;
          if (hasPermission(roles, v)) {
            if (v.children && v.children.length > 0) {
              v.children = v.children.filter(child => {
                if (hasPermission(roles, child)) {
                  return child
                }
                return false;
              });
              return v
            } else {
              return v
            }
          }
          return false;
        });
        commit('SET_ROUTERS', accessedRouters); resolve(); }}}};export default permission;

Copy the code

This code basically does one thing: it matches the user’s permissions with the permissions previously required for each asyncRouterMap page in router.js, and returns a list of routes that the user can access.


The sidebar

The last place where permissions are involved is the sidebar, but it’s already handy to dynamically display the sidebar. Here the sidebar is based on the Element-UI NavMenu. The code is a little too much to post detailed code, interested can directly go to Github to see the address, or directly read the documentation about the sidebar.

To put it bluntly, the permission_routers calculated before are used for dynamic V-for rendering after they are taken by VUex. But there’s a lot of judgment here because there’s some business requirements like we’re going to add a lot of parameters when we define routes

/**
* hidden: true                   if `hidden:true` will not show in the sidebar(default is false)
* redirect: noredirect           if `redirect:noredirect` will no redirct in the breadcrumb
* name:'router-name'             the name is used by <keep-alive> (must set!!!!!!!!!) * meta : { role: ['admin'.'editor']     will control the page role (you can set multiple roles)
   title: 'title'               the name show in submenu and breadcrumb (recommend set)
   icon: 'svg-name'             the icon show in the sidebar,
   noCache: true                if fasle ,the page will no be cached(default is false* * /)}Copy the code

This is for reference only, and this project uses recursive components for all of the sidebar blocks in order to support infinite nesting. If necessary, please adapt your own sidebar to meet your business needs.

Sidebar highlighting QUESTION: A lot of people in the group ask why their sidebar can’t be highlighted with their own routes. It’s actually quite simple

:default-active=”$route.path” :default-active=”$route.path


Button level permission control

A lot of people have been asking about button level granularity of permission control. Now our company is like this, there are not many places that really need the button level control. Now, after obtaining the user’s role, v-IF is used to distinguish the buttons corresponding to different permissions manually in the front end. As mentioned before, the authority judgment of granularity of our company is entrusted to the back-end, and the back-end will judge the authority for each operation. And I think in fact, the front end really need button level judgment is not a lot of places, if a page has a lot of different permissions of the button, I think more should consider whether the product level design is reasonable. Of course you forced me to do button level permission control, you can also refer to the route level practice, make an operation permission table… But PERSONALLY I think it’s a bit superfluous. Or encapsulate it as an instruction.


Axios interceptor

Let’s talk a little bit more about Axios. I covered it briefly in the last series of articles, but it’s worth nagging here. As mentioned above, our server validates permissions for each request, so here we encapsulate the request for the business. First we use the request interceptor to insert tokens in each request header so that the back end can verify the request permission. And create a Respone interceptor, when the server returns a special status code, we do the same thing, such as no permission or token invalid operation.

import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// Create an axios instance
const service = axios.create({
  baseURL: process.env.BASE_API, / / API base_url
  timeout: 5000 // Request timeout
})

// Request interceptor
service.interceptors.request.use(config= > {
  // Do something before request is sent
  if (store.getters.token) {
    config.headers['X-Token'] = getToken() // Let each request carry token--[' x-token '] is a custom key. Change it according to the actual situation
  }
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// Respone interceptor
service.interceptors.response.use(
  response= > response,
  /** * the following comment indicates the request status through the custom code, if the code returns as follows: permission problem, log out and return to the login page */
  // const res = response.data;
  // if (res.code ! = = 20000) {
  // Message({
  // message: res.message,
  // type: 'error',
  // duration: 5 * 1000
  / /});
  // // 50008: Invalid token; 50012: Another client is logged in. 50014:Token expired;
  // if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
  // messagebox.confirm (' you have been logged out, you can cancel to remain on this page, or log in again ', 'sure to log out ', {
  // confirmButtonText: 'relogin ',
  // cancelButtonText: 'cancel ',
  // type: 'warning'
  // }).then(() => {
  // store.dispatch('FedLogOut').then(() => {
  // location.reload(); // To re-instantiate vue-Router objects to avoid bugs
  / /});
  / /})
  / /}
  // return Promise.reject('error');
  // } else {
  // return response.data;
  / /}
  error => {
    console.log('err' + error)// for debug
    Message({
      message: error.message,
      type: 'error'.duration: 5 * 1000
    })
    return Promise.reject(error)
  })

export default service

Copy the code

Two steps to verify

The article also said at the beginning, the security of the background is very important, a simple account + password is difficult to ensure security. Therefore, the backstage projects of our company all use the two-step verification method. We have tried to use google-Authenticator or Youbikey before, but the difficulty and operation cost are quite high. Later or ready to use Tencent dad, this era who do not use wechat… Security Tencent dad also helped me do a good job of security. “Two-step verification recommends supporting multiple channels — not just wechat or QQ, which took two or three days to fix.

The two authentications here are a bit misnomer, in fact, after the account password authentication also need a bound third-party platform login authentication. It is also very simple to write, in the original login logic to transform it.

this.$store.dispatch('LoginByEmail', this.loginForm).then(() => {
  //this.$router.push({ path: '/'}); // Do not redirect to the home page this.showDialog =trueCatch (err => {this.$message.error(err); });Copy the code

After successful login, the user will not directly jump to the home page, but let the user log in in two steps, choose the login platform. The next step is for all third parties to log in to the same place via OAuth2.0 authorization. This major platform is much the same, we look up the document, not to expand, said a wechat authorization pit place. Note that you can’t even change the order of the arguments, otherwise the validation will fail. The code, and I’ve also wrapped the openWindow method for you to see. When a third party authorization succeeds, it jumps to a page where you have a redirect — URI passed in before you

So we also need to open an AuthRedirect page in the background: code. He is the function of the third party will default to authorize page after login successfully, authorized by the page will redirect back to our background again, because it is spa, change the experience of routing is not good, we through the window. The opener. Location. Change the hash href way, Listen for changes to the hash in login.js. When the hash changes, the code returned by the third party after successful login and the UID returned after the first login are sent to the server for verification. If the code is correct, the login succeeds.

 created() {
     window.addEventListener('hashchange', this.afterQRScan);
   },
   destroyed() {
     window.removeEventListener('hashchange', this.afterQRScan);
   },
   afterQRScan() {
     const hash = window.location.hash.slice(1);
     const hashObj = getQueryObject(hash);
     const originUrl = window.location.origin;
     history.replaceState({}, ' ', originUrl);
     const codeMap = {
       wechat: 'code',
       tencent: 'code'
     };
     const codeName = hashObj[codeMap[this.auth_type]];
     this.$store.dispatch('LoginByThirdparty', codeName).then(() => {
       this.$router.push({
         path: '/'
       });
     });
   }
Copy the code

Here involves the login authority of the thing is almost finished, here the landlord just gave you a realization of the idea (are the landlord constantly groping for blood and tears history), each company to achieve the scheme are some differences, please carefully choose suitable for their business form of solution. If you have any ideas or suggestions, please leave comments under this project and discuss with us.


Of the pit

Conventional occupy pit, here is the hand touch hand, take you with vue lift backstage series. Full project address: vue-element-admin

  • Hand touch hand, take you to use vue backstage series a (basic)
  • Hand to hand, take you to use vUE masturbation background series two (login permission)
  • Hand to hand, take you to use the vUE backstage series three (Actual combat)
  • Hand touch, take you with vue Lute background series four (vueAdmin a minimalist background base template)
  • Hand touch hand, take you to use vUE Background Lift series five (v4.0 new version)
  • Hand to hand, take you to wrap a Vue Component
  • Hand to hand, with your elegant use of icon
  • Using webpack4 with proper posture (part 1)
  • Using webpack4 with proper posture