Bladeren bron

refactor: permission

add interface level authentication, add some conditions to Vue.prototype, change
store.state.user.roles from Array to Set and add store.state.user.accesses
Casper Dai 3 jaren geleden
bovenliggende
commit
9a20ae2a49

+ 18 - 13
src/components/Permission/index.js

@@ -1,17 +1,11 @@
 import { mapGetters } from 'vuex'
 
-function hasPermission (roles, include, exclude) {
+function hasPermission (roles, include) {
   if (include) {
     if (typeof include === 'string') {
-      return roles.includes(include)
+      return roles.has(include)
     }
-    return roles.some(role => include.includes(role))
-  }
-  if (exclude) {
-    if (typeof exclude === 'string') {
-      return !roles.includes(exclude)
-    }
-    return !roles.some(role => exclude.includes(role))
+    return include.some(role => roles.includes(role))
   }
   return false
 }
@@ -28,16 +22,27 @@ export default {
       type: [String, Array],
       default: null
     },
-    exclude: {
-      type: [String, Array],
+    access: {
+      type: String,
       default: null
     }
   },
   computed: {
-    ...mapGetters(['roles', 'isAdmin'])
+    ...mapGetters([
+      'roles',
+      'isAdmin',
+      'accesses'
+    ]),
+    allow () {
+      let allow = this.isAdmin || hasPermission(this.roles, this.include)
+      if (allow && this.access) {
+        allow = this.accesses.has(this.access)
+      }
+      return allow
+    }
   },
   render () {
-    if (this.skip || this.isAdmin || hasPermission(this.roles, this.include, this.exclude)) {
+    if (this.skip || this.allow) {
       return this.$slots.default
     }
     return null

+ 4 - 0
src/constant.js

@@ -22,3 +22,7 @@ export const ScheduleType = {
   CALENDAR: 1,
   RECUR: 2
 }
+
+export const Access = {
+  DELETE_FORCE: 'backend-api:delete-force'
+}

+ 0 - 1
src/layout/components/Navbar/index.vue

@@ -51,7 +51,6 @@ export default {
   },
   computed: {
     ...mapGetters([
-      'hasEditPermission',
       'avatar',
       'name'
     ])

+ 12 - 7
src/main.js

@@ -13,6 +13,8 @@ import router from './router'
 import './icons'
 import './permission'
 
+import { Access } from './constant'
+
 import Wrapper from './components/Wrapper'
 import Permission from './components/Permission'
 import StatusWrapper from './components/StatusWrapper'
@@ -41,25 +43,26 @@ function startApp () {
   Vue.component('EditInput', EditInput)
   Vue.component('SearchInput', SearchInput)
 
+  Vue.prototype.$keycloak = keycloak
   Vue.prototype.$showLoading = showLoading
   Vue.prototype.$closeLoading = closeLoading
 
+  Vue.prototype.Access = Access
+  Vue.prototype.__PLACEHOLDER__ = __PLACEHOLDER__
+  Vue.prototype.__SENSOR_ELK__ = __SENSOR_ELK__
+
   Vue.config.productionTip = false
   Vue.config.errorHandler = err => {
     closeLoading()
     throw err
   }
 
-  Vue.prototype.$keycloak = keycloak
-  Vue.prototype.__PLACEHOLDER__ = __PLACEHOLDER__
-  Vue.prototype.__SENSOR_ELK__ = __SENSOR_ELK__
-
-  console.log(keycloak)
-
   store.dispatch('user/login', keycloak)
   store.dispatch('permission/generateRoutes', store.getters.roles)
   router.addRoutes(store.getters.permissionRoutes)
 
+  Vue.prototype.hasEditPermission = store.getters.roles.size > 1
+
   new Vue({
     router,
     store,
@@ -84,11 +87,13 @@ keycloak
       return
     }
 
+    console.log(keycloak)
+
     // Token Refresh
     setInterval(() => {
       keycloak.updateToken(70).then(refreshed => {
         if (refreshed) {
-          console.info('Token refreshed')
+          console.info('Token refreshed', keycloak)
           store.commit('user/SET_TOKEN', keycloak.token)
         } else {
           console.warn(`Token not refreshed, valid for ${Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000)} seconds`)

+ 7 - 14
src/permission.js

@@ -14,27 +14,20 @@ router.beforeEach(async (to, from, next) => {
   NProgress.start()
 
   if (store.getters.token) {
-    // determine whether the user has obtained his permission roles through getInfo
-    if (store.getters.roles.length) {
-      if (to.path === '/error') {
-        // redirect to the home page
-        next({ path: '/' })
-        NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
-      } else {
-        next()
-      }
+    if (to.path === '/error') {
+      // redirect to the home page
+      next({ path: '/' })
+      NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
     } else {
-      next('/error?code=400')
-      NProgress.done()
+      next()
     }
   } else {
-    /* has no token*/
-    if (whiteList.indexOf(to.path) !== -1) {
+    if (whiteList.includes(to.path)) {
       // in the free login whitelist, go directly
       next()
     } else {
       // other pages that do not have permission to access are redirected to the login page.
-      next(`/error?code=403`)
+      next('/error')
       NProgress.done()
     }
   }

+ 3 - 3
src/store/getters.js

@@ -17,10 +17,10 @@ const getters = {
     return state.permission.routes
   },
   isAdmin (state, getters) {
-    return getters.roles.includes(Role.ADMIN)
+    return getters.roles.has(Role.ADMIN)
   },
-  hasEditPermission (state, getters) {
-    return getters.roles.length > 1 || getters.roles[0] !== Role.VISITOR
+  accesses (state) {
+    return state.user.accesses
   }
 }
 

+ 3 - 3
src/store/modules/permission.js

@@ -3,9 +3,9 @@ import { Role } from '@/constant'
 
 function hasPermission (roles, { include, exclude }) {
   if (include) {
-    return roles.some(role => include.includes(role))
+    return include.some(role => roles.has(role))
   }
-  return !exclude || !roles.some(role => exclude.includes(role))
+  return !exclude || exclude.length < roles.size || exclude.some(role => !roles.has(role))
 }
 
 export function filterAsyncRoutes (routes, roles, force) {
@@ -41,7 +41,7 @@ const mutations = {
 const actions = {
   generateRoutes ({ commit }, roles) {
     let accessedRoutes
-    if (roles.includes(Role.ADMIN)) {
+    if (roles.has(Role.ADMIN)) {
       accessedRoutes = __DEV__ ? asyncRoutes : filterAsyncRoutes(asyncRoutes, roles, true)
     } else {
       accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)

+ 40 - 13
src/store/modules/user.js

@@ -6,7 +6,8 @@ const state = {
   token: '',
   name: '',
   avatar: '',
-  roles: []
+  roles: null,
+  access: null
 }
 
 const mutations = {
@@ -21,32 +22,58 @@ const mutations = {
   },
   SET_ROLES (state, roles) {
     state.roles = roles
+  },
+  SET_ACCESSES (state, accesses) {
+    state.accesses = accesses
+  }
+}
+
+function parseRealmAccess (realmAccess) {
+  const roleSet = new Set()
+  if (realmAccess) {
+    const roleArray = Object.values(Role)
+    realmAccess.roles.forEach(role => {
+      if (roleArray.includes(role)) {
+        roleSet.add(role)
+      }
+    })
+  } else {
+    roleSet.add(Role.VISITOR)
+  }
+  return roleSet
+}
+
+function parseResourceAccess (resourceAccess) {
+  const accessSet = new Set()
+  if (resourceAccess) {
+    Object.keys(resourceAccess).forEach(client => {
+      resourceAccess[client].roles.forEach(role => {
+        accessSet.add(`${client}:${role}`)
+      })
+    })
   }
+  return accessSet
 }
 
 const actions = {
   login ({ commit }, keycloak) {
     if (keycloak.authenticated) {
-      const roleArray = Object.values(Role)
-      const roles = keycloak.realmAccess?.roles?.filter(role => roleArray.includes(role))
       const { preferred_username, family_name, given_name, avatar } = keycloak.tokenParsed
       commit('SET_TOKEN', keycloak.token)
-      commit('SET_ROLES', roles?.length ? roles : [Role.VISITOR])
+      commit('SET_ROLES', parseRealmAccess(keycloak.realmAccess))
+      commit('SET_ACCESSES', parseResourceAccess(keycloak.resourceAccess))
       commit('SET_NAME', family_name && given_name ? `${family_name}${/[a-zA-z]/.test(family_name) ? ' ' : ''}${given_name}` : preferred_username)
       commit('SET_AVATAR', avatar)
     }
   },
 
   logout ({ commit }) {
-    return new Promise((resolve, reject) => {
-      Vue.prototype.$keycloak.logout().then(() => {
-        commit('SET_TOKEN', '')
-        commit('SET_ROLES', [])
-        commit('SET_NAME', '')
-        commit('SET_AVATAR', '')
-        resetRouter()
-        resolve()
-      }, reject)
+    return new Promise(resolve => {
+      // 登出将跳转页面,所以不需其他操作
+      Vue.prototype.$keycloak.logout()
+      commit('SET_TOKEN', '')
+      resetRouter()
+      resolve()
     })
   }
 }

+ 5 - 4
src/views/bigscreen/index.vue

@@ -83,7 +83,10 @@
                 />
               </el-tooltip>
             </template>
-            <permission :skip="item.status === 0">
+            <permission
+              :skip="item.status === 0"
+              :access="Access.DELETE_FORCE"
+            >
               <el-tooltip
                 content="删除"
                 :hide-after="2000"
@@ -167,7 +170,6 @@
 </template>
 
 <script>
-import { mapGetters } from 'vuex'
 import { getAssetUrl } from '@/api/asset'
 import { getRatios } from '@/api/device'
 import {
@@ -198,14 +200,13 @@ export default {
       ratios: [],
       ...createListOptions({
         pageSize: 8,
-        status: this.$store.getters.hasEditPermission ? void 0 : State.RESOLVED,
+        status: this.hasEditPermission ? void 0 : State.RESOLVED,
         name: ''
       }),
       disabled: false
     }
   },
   computed: {
-    ...mapGetters(['hasEditPermission']),
     isAbnormal () {
       return this.error || !this.loading && this.totalCount === 0
     }

+ 0 - 4
src/views/device/camera/index.vue

@@ -257,7 +257,6 @@
 </template>
 
 <script>
-import { mapGetters } from 'vuex'
 import flvjs from 'flv.js'
 import {
   getCamera,
@@ -318,9 +317,6 @@ export default {
       rowNum: 8
     }
   },
-  computed: {
-    ...mapGetters(['hasEditPermission'])
-  },
   created () {
     this.getCamera()
   },

+ 0 - 4
src/views/device/detail/components/DeviceInfo.vue

@@ -94,7 +94,6 @@
 </template>
 
 <script>
-import { mapGetters } from 'vuex'
 import { updateDevice } from '@/api/device'
 
 export default {
@@ -111,9 +110,6 @@ export default {
       info: {}
     }
   },
-  computed: {
-    ...mapGetters(['hasEditPermission'])
-  },
   methods: {
     toEdit () {
       this.info = {

+ 1 - 1
src/views/device/detail/index.vue

@@ -95,7 +95,7 @@ export default {
         { key: 'info', label: '设备信息' },
         { key: 'status', label: '运行状态' },
         { key: 'alarm', label: '设备告警' },
-        this.$store.getters.hasEditPermission ? { key: 'invoke', label: '设备操控' } : null,
+        this.hasEditPermission ? { key: 'invoke', label: '设备操控' } : null,
         { key: 'screen', label: '屏体状态监测' }
       ].filter(val => val)
     }

+ 1 - 1
src/views/device/index.vue

@@ -331,7 +331,7 @@ export default {
     }
   },
   computed: {
-    ...mapGetters(['isAdmin', 'hasEditPermission']),
+    ...mapGetters(['isAdmin']),
     operationColumnWidth () {
       return this.isAdmin ? this.__PLACEHOLDER__ ? 340 : 260 : 200
     },

+ 7 - 41
src/views/error-page/index.vue

@@ -6,20 +6,12 @@
         <div class="c-error-page__logo" />
         <div class="c-error-page__name">浪潮屏媒安播云平台</div>
       </div>
-      <div class="c-error-page__title">{{ message }}</div>
-      <div class="c-error-page__retry u-pointer">
-        <span
-          class="c-error-page__invoke"
-          @click="retry"
-        >
-          重 试
-        </span>
-        <span
-          class="c-error-page__invoke"
-          @click="logout"
-        >
-          登 出
-        </span>
+      <div class="c-error-page__title">鉴权失败,请稍后重试</div>
+      <div
+        class="c-error-page__retry u-pointer"
+        @click="retry"
+      >
+        重 试
       </div>
     </div>
   </div>
@@ -28,27 +20,9 @@
 <script>
 export default {
   name: 'ErrorPage',
-  computed: {
-    code () {
-      return this.$route.query.code
-    },
-    message () {
-      switch (this.code) {
-        case '400':
-          return '无角色,请联系管理员'
-        case '403':
-          return '鉴权失败'
-        default:
-          return ''
-      }
-    }
-  },
   methods: {
     retry () {
       window.location.reload()
-    },
-    logout () {
-      this.$store.dispatch('user/logout')
     }
   }
 }
@@ -109,17 +83,13 @@ export default {
     color: $error;
     font-size: 18px;
     font-weight: bold;
-    text-align: center;
   }
 
   &__retry {
+    position: relative;
     margin-top: 40px;
     color: $warning;
     font-size: 16px;
-  }
-
-  &__invoke {
-    position: relative;
 
     &:hover {
       color: $warning--light;
@@ -134,10 +104,6 @@ export default {
         border-bottom: 1px solid $warning;
       }
     }
-
-    & + & {
-      margin-left: $spacing;
-    }
   }
 }
 </style>

+ 4 - 1
src/views/media/index.vue

@@ -123,7 +123,10 @@
             >
               提交
             </div>
-            <permission :skip="canDel">
+            <permission
+              :skip="canDel"
+              :access="Access.DELETE_FORCE"
+            >
               <div
                 class="c-table__btn u-pointer"
                 @click="toDel(scope.row)"

+ 7 - 4
src/views/profile/index.vue

@@ -131,7 +131,10 @@ export default {
     }
   },
   computed: {
-    ...mapState('user', ['name', 'roles']),
+    ...mapState('user', [
+      'name',
+      'roles'
+    ]),
     avatarStyle () {
       const avatar = this.avatar
       return avatar ? {
@@ -139,13 +142,13 @@ export default {
       } : null
     },
     roleTip () {
-      if (this.roles.includes(Role.ADMIN)) {
+      if (this.roles.has(Role.ADMIN)) {
         return '超级管理员'
       }
-      if (this.roles.includes(Role.SUPERVISOR)) {
+      if (this.roles.has(Role.SUPERVISOR)) {
         return '主管'
       }
-      if (this.roles.includes(Role.STAFF)) {
+      if (this.roles.has(Role.STAFF)) {
         return '员工'
       }
       return '游客'

+ 5 - 4
src/views/schedule/index.vue

@@ -164,7 +164,10 @@
               提交
             </div>
           </template>
-          <permission :skip="scope.row.status === 0">
+          <permission
+            :skip="scope.row.status === 0"
+            :access="Access.DELETE_FORCE"
+          >
             <div
               class="c-table__btn u-pointer"
               @click="toDel(scope.row)"
@@ -262,7 +265,6 @@
 </template>
 
 <script>
-import { mapGetters } from 'vuex'
 import { getRatios } from '@/api/device'
 import {
   getSchedules,
@@ -300,7 +302,7 @@ export default {
       active: 'normal',
       normal: createListOptions({
         type: void 0,
-        status: this.$store.getters.hasEditPermission ? void 0 : State.RESOLVED,
+        status: this.hasEditPermission ? void 0 : State.RESOLVED,
         resolutionRatio: void 0,
         name: ''
       }),
@@ -314,7 +316,6 @@ export default {
     }
   },
   computed: {
-    ...mapGetters(['hasEditPermission']),
     currObj () {
       return this[this.active]
     },