└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # § Vue 权限控制 2 | 3 | > 在看本文档之前,您需要阅读 [Vue 另类状态管理](https://github.com/kenberkeley/vue-state-management-alternative) 4 | 5 | 业界一向认为,权限只能是后端做 6 | 但如果在前后端分离的前提下仍是这样实现,那么前后端分离是没有任何意义的,还不如直接后端渲染实在 7 | 8 | 目前有关 Vue 的权限控制并没有一个相对主流的解决方案,故在此抛砖引玉 9 | 10 | 首先先说明,我司并没有用 Vuex,仅仅就是 Vue + Vue Router 11 | 稍微复杂一点的业务场景,都可以使用上述的“另类状态管理”以及 [eventbus](https://cn.vuejs.org/v2/guide/components.html#非父子组件的通信) 去解决 12 | 13 | *** 14 | 15 | ## ⊙ 概览 16 | 17 | 一般我们项目的源码目录 `src/` 下都会有一个 `mixins/`,里面必然存在一个 `session.js`,有如: 18 | 19 | > 我司约定 mixin 中的变量以及函数都应当使用 `$` 结尾(为什么不能用在开头?因为 Vue 不会代理 `$` / `_` 开头的变量,故统一置尾) 20 | 21 | ```js 22 | /*** src/mixins/session.js ***/ 23 | import authService from '@/services/authService' // 权限相关的 API 封装服务 24 | const goToSSO = () => { 25 | location.replace(`<我司单点登录 URL>?returnUrl=${encodeURIComponent(location.href)}`) 26 | } 27 | 28 | /* 登录凭据 */ 29 | export const session$ = { 30 | id: null, 31 | username: '', 32 | role: '', 33 | isLeader: null 34 | } 35 | 36 | /* 是否管理员 */ 37 | export const isAdmin$ = () => { 38 | return session$.role === 'admin' 39 | } 40 | 41 | /* 是否销售主管 */ 42 | export const isSalesLeader$ = () => { 43 | return session$.role === 'sales' && session$.isLeader 44 | } 45 | 46 | /* 挂载 DOM 前调用本函数 */ 47 | export const syncSession$ = () => { 48 | return authService.getSession().then(sess => { 49 | Object.assign(session$, sess) // 这里不建议写成 session$ = sess 50 | }).catch(() => { 51 | goToSSO() // 跳转到单点登录 52 | throw new Error('Redirecting to SSO') // 继续抛出,避免之后的 then 执行挂载 DOM 53 | }) 54 | } 55 | 56 | // @export.default 57 | export default { 58 | data: () => ({ 59 | session$ 60 | }), 61 | computed: { 62 | isAdmin$, 63 | isSalesLeader$ 64 | }, 65 | methods: { 66 | logout$ () { 67 | authService.logout().then(goToSSO) 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | 在启动文件中一般是这样的: 74 | 75 | ```js 76 | /*** src/app.js ***/ 77 | import 'babel-polyfill' 78 | import Vue from 'vue' 79 | import App from '@/components/App' 80 | import { syncSession$ } from '@/mixins/session' 81 | 82 | // 同步 session 后才挂载 DOM 83 | syncSession$().then(() => { 84 | /* eslint-disable no-new */ 85 | new Vue({ 86 | el: '#app', 87 | router: require('@/routes/').default, // 路由涉及权限,因此须在同步 session 后才执行 88 | render: h => h(App) 89 | }) 90 | }) 91 | ``` 92 | 93 | 以上就是我司权限管理的基石 94 | 95 | *** 96 | 97 | ## ⊙ 常见的疑问 98 | 99 | Q:为什么不使用 LocalStorage / SessionStorage / cookies 去保存登录凭据?这样可以全局访问很方便啊 100 | A:可被篡改,没有安全性可言,而且还得考虑其过期时间以及解析错误等一系列不必要的麻烦 101 | 102 | Q:用 mixin 全局共享状态的好处是什么? 103 | A:首先必须指出,要想让 `session$` 变成响应式,您必须要把 `src/mixins/session.js` 引入到任一组件中,这样就可以在组件内部(包括模板)访问到所有的变量与方法。而且,您还可以在非组件内部中访问。虽然 `export default` 的是 mixin 的固定格式,但 `export` 的却是直接的变量或方法,因此可以直接 `import { session$ } from '@/mixins/session'`,这几乎就像全局变量般便利,但又可以最大程度地保证安全性。更重要的,您还可以享受 Vue 带来的全局响应式、计算属性等一系列特性,而不仅仅是一个无法被外界篡改的闭包变量。举例说明: 104 | 105 | ```js 106 | import session from '@/mixins/session' 107 | 108 | export default { 109 | mixins: [session], 110 | watch: { 111 | /* 需求:测试模式下允许即时修改用户角色,请在控制台显示 */ 112 | 'session$.role' (newRole, oldRole) { 113 | console.info('角色已切换:', oldRole, ' => ', newRole) 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | Q:路由控制怎么处理? 120 | A:下面我们接着说 121 | 122 | *** 123 | 124 | ## ⊙ 路由级别控制 125 | 126 | 能在 Vue 层面上解决的事情没必要动用到 Vue Router 的特性,否则权限就写得太散了 127 | 业内主流的方式都是通过 `beforeEach` 来获取 `meta` 信息以拦截 128 | 不过话说回来,既然你都不想该角色看到的路由,为什么你还要挂载?画蛇添足多此一举莫过于此 129 | 130 | 举个例子,一个项目有 `/a`(默认)、`/b`、`/c`、`/d`、`/e` 五个路由,需满足: 131 | * 只有 管理员 可以看到 `/e` 132 | * 只有 管理员 与 销售 Leader 可以看到 `/d` 133 | 134 | 那么路由的定义可以这样写: 135 | 136 | ```js 137 | import { isAdmin$, isSalesLeader$ } from '@/mixins/session' 138 | 139 | export default [ 140 | { 141 | path: '/a', 142 | alias: '/', 143 | component: require('@/views/a/') 144 | }, 145 | 146 | { 147 | path: '/b', 148 | component: require('@/views/b/') 149 | }, 150 | 151 | { 152 | path: '/c', 153 | component: require('@/views/c/') 154 | }, 155 | 156 | (isAdmin$() || isSalesLeader$()) && { 157 | path: '/d', 158 | component: require('@/views/d/') 159 | }, 160 | 161 | isAdmin$() && { 162 | path: '/e', 163 | component: require('@/views/e/') 164 | }, 165 | 166 | { // 404 置尾 167 | path: '*', 168 | component: { 169 | beforeCreate () { 170 | this.$router.replace('/') 171 | }, 172 | render: h => null 173 | } 174 | } 175 | ].filter(route => route) // 排除掉为 false 的项 176 | ``` 177 | 178 | 这下你能可以理解为什么在启动文件中要在 `syncSession$` 完成后才引入路由了吧? 179 | 如果在开头就 `import routes from '@/routes/'` 则无法实现控权(因为是先执行) 180 | 181 | *** 182 | 183 | ## ⊙ 按钮级别控制 184 | 185 | 基本就是把 `@/mixins/session` 引入到组件中就可以了,没有任何难度 186 | 187 | *** 188 | *** 189 | 190 | ### 2017/12/6 针对 SegmentFault 下[评论](https://segmentfault.com/p/1210000012206425?_ea=2945405)的更新 191 | 192 | * 针对 `没有用动态路由,导致用户登录前不能初始化Vue应用,所以登陆页只能单独做,开始我也是这么做的,但始终觉得url跳转的体验不好,所以用动态路由解决了` 的解决方案:如果您的公司没有 SSO,那么每个项目都只能重复造轮子做登录页(之前我司就是如此),此时只能借助 `vue-router` 的钩子函数 `beforeEach` 控权: 193 | 194 | ```js 195 | import { isLogin$ } from '@/mixins/session' 196 | const LOGIN_PATH = '/auth/login' 197 | 198 | export default function authInterceptor(to, from, next) { 199 | if (isLogin$()) { 200 | switch (to.path) { 201 | case LOGIN_PATH: 202 | next('/') 203 | return 204 | default: 205 | next() 206 | } 207 | } else { 208 | switch (to.path) { 209 | case LOGIN_PATH: 210 | next() 211 | return 212 | default: 213 | next(`${LOGIN_PATH}?referrer=${encodeURIComponent(to.fullPath)}`) 214 | } 215 | } 216 | } 217 | 218 | // 使用方式:router.beforeEach(authInterceptor) 219 | ``` 220 | 221 | * 针对 `在前端路由文件中根据角色做判断的做法不够灵活,路由权限还是由后端分发给前端比较好,这样当需要修改角色权限时,后端改一下配置,前端刷新就生效了` 的回应:把我司现行完善的权限设计一股脑搬出来说没有意义,以上例子只是为了简要说明,更重要的是思想 222 | --------------------------------------------------------------------------------