vue-element-admin集成Keycloak實現統一身份驗證、權限控制

vue-element-admin是一個在github擁有極高star數的後臺前端解決方案,基於vueelement-ui實現。vue自己易上手,element-ui組件豐富,即使後端開發人員使用vue-element-admin也能較快的開發出不錯的管理後臺。可是,當公司內部有多個後臺系統的時候,直接使用vue-element-admin內置的登陸、身份驗證功能時,沒法達到統一管理用戶、權限的目的,這時統一的SSO登陸、身份驗證、權限控制就顯得尤其重要和方便。本文便講解如何將vue-element-admin集成Keycloak的方法,從而實現統一的SSO登陸、身份驗證、權限控制。若是對Keycloak還不太瞭解的話,能夠參考我以前寫的一篇Keycloak快速上手指南,先對基本概念進行初步的認識,相信對本文後續的理解會有所幫助。javascript

vue-element-admin身份驗證、權限控制核心代碼分析

既然是要將vue-element-admin與Keycloak進行集成,那麼有必要先來對vue-element-admin自己的登陸、身份驗證、權限控制相關的功能是如何實現的進行一個瞭解。核心代碼主要位於以下幾個文件中:前端

  • src/permission.js:針對vue-router進行的全局導航守衛配置,身份驗證、權限控制最爲核心的邏輯都在這個文件中
  • src/router/index.jsvue-router相關的路由配置,其中asyncRoutes是與roles相關的動態路由配置
  • src/modules/permission.js:根據roles進行權限控制、生成動態路由的vuex相關操做
  • src/modules/user.js:登陸登出、用戶信息、Token等的vuex相關操做
  • src/api/user.js:登陸登出、用戶信息的API操做
  • src/views/login/index.vue:登陸頁面
  • src/layout/components/Navbar.vue:登出入口所在組件

接下來就對上面這些文件中部分的核心代碼進行下分析。vue

src/permission.js核心代碼分析

大部分代碼都比較簡單,token是從cookie中獲取,固然也能夠自定義token的存儲,好比存在localStorage中,重點看下動態路由部分的代碼:java

// determine whether the user has obtained his permission roles through getInfo
  const hasRoles = store.getters.roles && store.getters.roles.length > 0
  if (hasRoles) {
    next()
  } else {
    try {
      // get user info
      // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
      const { roles } = await store.dispatch('user/getInfo')

      // generate accessible routes map based on roles
      const accessRoutes = await store.dispatch('permission/generateRoutes', roles)

      // dynamically add accessible routes
      router.addRoutes(accessRoutes)

      // hack method to ensure that addRoutes is complete
      // set the replace: true, so the navigation will not leave a history record
      next({ ...to, replace: true })
    } catch (error) {
      // remove token and go to login page to re-login
      await store.dispatch('user/resetToken')
      Message.error(error || 'Has Error')
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
複製代碼

調用vuex中的user/getInfo獲取用戶用戶角色,並根據用戶角色生成相應的路由動態添加git

src/router/index.js核心代碼分析

{
    path: '/permission',
    component: Layout,
    redirect: '/permission/page',
    alwaysShow: true, // will always show the root menu
    name: 'Permission',
    meta: {
      title: 'Permission',
      icon: 'lock',
      roles: ['admin', 'editor'] // you can set roles in root nav
    },
    children: [
      {
        path: 'page',
        component: () => import('@/views/permission/page'),
        name: 'PagePermission',
        meta: {
          title: 'Page Permission',
          roles: ['admin'] // or you can only set roles in sub nav
        }
      },
      {
        path: 'directive',
        component: () => import('@/views/permission/directive'),
        name: 'DirectivePermission',
        meta: {
          title: 'Directive Permission'
          // if do not set roles, means: this page does not require permission
        }
      },
      {
        path: 'role',
        component: () => import('@/views/permission/role'),
        name: 'RolePermission',
        meta: {
          title: 'Role Permission',
          roles: ['admin']
        }
      }
    ]
  }
複製代碼

上面是asyncRoutes中的部分定義,能夠看到,meta中的roles即是對相應路由的權限控制github

src/modules/permission.js核心代碼分析

主要是根據roles以及asyncRoutes中定義的路由,來生成有訪問權限的路由vue-router

generateRoutes({ commit }, roles) {
    return new Promise(resolve => {
      let accessedRoutes
      if (roles.includes('admin')) {
        accessedRoutes = asyncRoutes || []
      } else {
        accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
      }
      commit('SET_ROUTES', accessedRoutes)
      resolve(accessedRoutes)
    })
  }
複製代碼

src/modules/user.js核心代碼分析

login action

調用API登陸成功後保存tokenvuex

login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => {
        const { data } = response
        commit('SET_TOKEN', data.token)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  }
複製代碼

getInfo action

獲取用戶信息並進行保存,主要是roles、name、avatar、introduction,本身須要額外的信息在這裏能夠擴展npm

getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.token).then(response => {
        const { data } = response

        if (!data) {
          reject('Verification failed, please Login again.')
        }

        const { roles, name, avatar, introduction } = data

        // roles must be a non-empty array
        if (!roles || roles.length <= 0) {
          reject('getInfo: roles must be a non-null array!')
        }

        commit('SET_ROLES', roles)
        commit('SET_NAME', name)
        commit('SET_AVATAR', avatar)
        commit('SET_INTRODUCTION', introduction)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  }
複製代碼

logout action

登出並清空保存的token、roles等element-ui

logout({ commit, state, dispatch }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        commit('SET_TOKEN', '')
        commit('SET_ROLES', [])
        removeToken()
        resetRouter()

        // reset visited views and cached views
        // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
        dispatch('tagsView/delAllViews', null, { root: true })

        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  }
複製代碼

src/views/login/index.vue核心代碼分析

調用vuex中的user/loginaction進行登陸、保存token操做

this.$refs.loginForm.validate(valid => {
    if (valid) {
      this.loading = true
      this.$store.dispatch('user/login', this.loginForm)
        .then(() => {
          this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
          this.loading = false
        })
        .catch(() => {
          this.loading = false
        })
    } else {
      console.log('error submit!!')
      return false
    }
  })
複製代碼

src/layout/components/Navbar.vue登出代碼分析

調用vuex中的user/logoutaction進行登出、清空token操做,並跳轉登陸頁

async logout() {
    await this.$store.dispatch('user/logout')
    this.$router.push(`/login?redirect=${this.$route.fullPath}`)
  }
複製代碼

vue-element-admin集成Keycloak整合思路

有了上面的身份驗證、權限控制相關的文件及代碼分析,接下來咱們就知道應該改動哪些地方去集成Keycloak了。事實上,須要作的最關鍵的事就是這3件:

  • 身份驗證交給Keycloak,驗證不經過時,用戶登陸直接到Keycloak的登陸頁

  • token及用戶相關的信息(name、roles等)從原來調用獨立API的方式改成從Keycloak直接獲取

  • 用戶登出改成使用Keycloak進行統一登出

說白了,就是登陸、用戶認證信息獲取、登出全都交給Keycloak。

Keycloak後臺配置準備

在正式開始對相關的文件進行修改前,咱們要先在Keycloak的管理後臺建立好相關的client、roles、users等資源,以便給後續vue-element-admin集成Keycloak時使用。

建立客戶端

keycloak-vue-element-admin-client-1

建立2個角色

keycloak-vue-element-admin-role

建立2個用戶

建立用戶admin

建立admin用戶並添加2個attributes:avatar、introduction,Keycloak中默認的用戶信息比較少,須要擴展用戶信息可在attributes中添加

keycloak-vue-element-admin-user-admin-1

爲admin用戶添加角色admin

keycloak-vue-element-admin-user-admin-2

建立用戶editor

建立editor用戶並添加2個attributes:avatar、introduction

keycloak-vue-element-admin-user-editor-1

爲editor用戶添加角色editor

keycloak-vue-element-admin-user-editor-2

客戶端設置attributes mappers

設置attributes mappers以後,idtoken中會包含對應的attributes信息,方便js客戶端直接獲取用戶信息

keycloak-vue-element-admin-client-2

vue-element-admin集成Keycloak代碼詳解

通過上面的分析,咱們知道須要將登陸、用戶認證信息獲取、登出部分進行改動,交給Keycloak處理。咱們將對以下文件進行修改:

  • src/store/modules/user.js:將vuex中登陸、獲取用戶信息、登出相關的action改成經過Keycloak進行管理
  • src/main.js:添加Keycloak初始化集成,將身份驗證及登陸部分交給Keycloak接管
  • src/permission.js:獲取roles部分改成從Keycloak獲取
  • src/layout/components/Navbar.vue:登出邏輯改成調用Keycloak的登出

下面將展現這4個文件改動的具體代碼

src/store/modules/user.js代碼改動

不直接改動原有vuex的action,新增3個Keycloak相關的action

增長keycloakLogin action

keycloakLogin action主要是在Keycloak登陸成功後設置token

keycloakLogin({ commit }, accessToken) {
    return new Promise((resolve, reject) => {
      commit('SET_TOKEN', accessToken)
      setToken(accessToken)
      resolve()
    })
  }
複製代碼

增長getKeycloakInfo action

getKeycloakInfo action用來從Keycloak中獲取用戶相關的信息,此示例中包括roles、name、avatar、introduction這4項用戶信息

getKeycloakInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      if (!Vue.prototype.$keycloak) {
        reject('keycloak not init')
      }

      if (!Vue.prototype.$keycloak.authenticated) {
        reject('Verification failed, please Login again.')
      }

      const roles = Vue.prototype.$keycloak.realmAccess.roles
      const name = Vue.prototype.$keycloak.idTokenParsed.preferred_username
      const avatar = Vue.prototype.$keycloak.idTokenParsed.avatar
      const introduction = Vue.prototype.$keycloak.idTokenParsed.introduction

      // roles must be a non-empty array
      if (!roles || roles.length <= 0) {
        reject('getKeycloakInfo: roles must be a non-null array!')
      }

      // you can also use the method loadUserProfile() to get user attributes
      // Vue.prototype.$keycloak.loadUserProfile().then(profile => {
      // let avatar = profile.attributes.avatar[0]
      // let introduction = profile.attributes.introduction[0]
      // })

      const data = {
        roles,
        name,
        avatar,
        introduction
      }

      commit('SET_ROLES', roles)
      commit('SET_NAME', name)
      commit('SET_AVATAR', avatar)
      commit('SET_INTRODUCTION', introduction)
      resolve(data)
    })
  }
複製代碼

須要說明的是,這裏直接從keycloak的idTokenParsed獲取到了自定義的attributes:avatar、introduction,是由於上面在Keycloak的後臺進行了attributes mappers的設置,若是不進行這項設置,能夠經過keycloak的loadUserProfile()方法獲取到自定義的attributes

增長keycloakLogout action

keycloakLogout action是經過Keycloak進行登出操做,成功後便清除本地保存的信息

keycloakLogout({ commit, state }) {
    return new Promise((resolve, reject) => {
      Vue.prototype.$keycloak.logout().then(() => {
        removeToken() // must remove token first
        resetRouter()
        commit('RESET_STATE')
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  }
複製代碼

src/main.js 代碼改動

入口文件main.js中主要是與Keycloak進行初始化集成,Keycloak身份驗證經過後調用user/keycloakLoginaction保存token

// keycloak init options
const initOptions = {
  url: process.env.VUE_APP_KEYCLOAK_OPTIONS_URL,
  realm: process.env.VUE_APP_KEYCLOAK_OPTIONS_REALM,
  clientId: process.env.VUE_APP_KEYCLOAK_OPTIONS_CLIENTID,
  onLoad: process.env.VUE_APP_KEYCLOAK_OPTIONS_ONLOAD
}

const keycloak = Keycloak(initOptions)

keycloak.init({ onLoad: initOptions.onLoad }).then(async authenticated => {
  if (!authenticated) {
    window.location.reload()
    return
  } else {
    Vue.prototype.$keycloak = keycloak
    await store.dispatch('user/keycloakLogin', keycloak.token)
    console.log('Authenticated', keycloak)
  }

  setInterval(() => {
    keycloak.updateToken(70).then((refreshed) => {
      if (refreshed) {
        console.log('Token refreshed')
      } else {
        console.log('Token not refreshed, valid for ' +
          Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds')
      }
    }).catch(error => {
      console.log('Failed to refresh token', error)
    })
  }, 60000)

  new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App)
  })
}).catch(error => {
  console.log('Authenticated Failed', error)
})
複製代碼

src/permission.js代碼改動

roles獲取改成調用user/getKeycloakInfoaction從Keycloak獲取

const { roles } = await store.dispatch('user/getKeycloakInfo')
複製代碼

src/layout/components/Navbar.vue代碼改動

登出邏輯改成調用user/keycloakLogoutaction經過Keycloak進行統一的登出處理,並去掉往vue-element-admin自帶的login跳轉的邏輯

async logout() {
  await this.$store.dispatch('user/keycloakLogout')
  // this.$router.push(`/login?redirect=${this.$route.fullPath}`)
}
複製代碼

vue-element-admin集成Keycloak後效果演示

本地npm run dev啓動頁面後,首次會進入Keycloak登陸頁,經過上面建立的admin、editor用戶登陸後,效果以下

admin用戶登陸效果

admin用戶登陸後,Dashboard能看到完整的圖表,且Permission菜單下能看到3個子菜單

keycloak-vue-element-admin-result-1

editor用戶登陸效果

editor用戶登陸後,Dashboard只展現了基本的信息,且Permission菜單下只能看到1個子菜單

keycloak-vue-element-admin-result-2

總結

本文先對vue-element-admin原始的身份驗證、權限控制邏輯進行了分析,接着給出了與Keycloak集成的思路,最後經過具體的代碼展現了vue-element-admin與Keycloak集成的方法。對於其餘的前端應用,與Keycloak集成的思路也是相通的,都是將登陸、獲取用戶信息、登出的部分交給Keycloak統一處理。

集成的項目地址:vue-element-admin-keycloak

相關文章
相關標籤/搜索