214 lines
14 KiB
Markdown
214 lines
14 KiB
Markdown
# 16 | 实战痛点2:项目开发中的权限系统
|
||
|
||
你好,我是大圣。
|
||
|
||
在上一讲中,我们使用组件库完成了项目的搭建,并且引入了Element3和axios等基础库。基于Element3,我们可以很方便地使用组件搭建项目。而使用axios,可以很方便地获取后端数据。在项目中,权限系统的控制需要前后端配合完成,而且权限系统也是后端管理系统中常见的一个难点。不过,今天我们主要从前端的角度,来聊一下项目的权限系统。
|
||
|
||
下面,我们先从登录权限谈起,因为登录权限对于一个项目来说是必备的功能模块。完成了登录选项的设置后,下一步需要做的是管理项目中的页面权限,而角色权限在这一过程中则可以帮助我们精细化地去控制页面权限。
|
||
|
||
## 登录权限
|
||
|
||
继续上一讲我们搭建起来的项目,你可以看到现在所有的页面都可以直接访问了,通常来说管理系统的内部页面都需要登录之后才可以访问,比如个人中心、订单页面等等。首先,我们来设计一个这样的权限限制功能,它能保证某些页面在登录之后才能访问。
|
||
|
||
为了实现这个功能,我们首先需要模拟登录的接口和页面。我们先新增路由页面,进入到项目目录下,在router.js中新增路由配置。下面的代码中,routes数组新增/login路由访问。
|
||
|
||
```javascript
|
||
import Login from '../components/Login.vue'
|
||
const routes = [
|
||
...
|
||
{
|
||
path: '/login',
|
||
component: Login,
|
||
hidden: true,
|
||
}
|
||
]
|
||
|
||
```
|
||
|
||
然后,我们进入到src/components/Login.vue组件中,组件的代码如下所示。在代码中,我们能看到,用户在输入用户名和密码之后,把用户名和密码传递给后端,然后就可以实现登录认证。
|
||
|
||
```javascript
|
||
handleLogin() {
|
||
formRef.value.validate(async valid => {
|
||
if (valid) {
|
||
loading.value = true
|
||
const {code, message} = await useStore.login(loginForm)
|
||
loading.value = false
|
||
if(code===0){
|
||
router.replace( toPath || '/')
|
||
}else{
|
||
message({
|
||
message: '登录失败',
|
||
type: 'error'
|
||
})
|
||
}
|
||
} else {
|
||
console.log('error submit!!')
|
||
return false
|
||
}
|
||
})
|
||
}
|
||
|
||
```
|
||
|
||
由于我们的项目是一个前端项目,所以我们需要在Vite内部做数据结构的模拟。我们在src目录下面新建mock目录,用来放置假数据的结构。我们写死一个用户名dasheng,使用用户名dasheng登录成功之后,我们把用户名、过期日期等重要信息进行加密,生成一个token返回给前端。
|
||
|
||
**这个token就算是一个钥匙,****对于那些****需要权限****才能读取到****的页面数据,前端需要带上这个钥匙才能读取到数据,否则****访问那些页面的时候****就会显示没有权限**。
|
||
|
||
```javascript
|
||
{
|
||
url: '/geek-admin/user/login',
|
||
type: 'post',
|
||
response: config => {
|
||
const { username } = config.body
|
||
const token = tokens[username]
|
||
|
||
// mock error
|
||
if (user!=='dasheng') {
|
||
return {
|
||
code: 60204,
|
||
message: 'Account and password are incorrect.'
|
||
}
|
||
}
|
||
|
||
return {
|
||
code: 20000,
|
||
data: token
|
||
}
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
我们回到前端页面,登录成功后,首先需要做的事情,就是把这个token存储在本地存储里面,留着后续发送数据。这一步的实现比较简单,直接把token存储到localStorage中就可以了。我们拿到这个token后,为了进行接口权限认证,要把token放在HTTP请求的header内部。
|
||
|
||
我们看下面的代码,在axios的请求发出之前,我们在配置中使用getToken从localStorage中读取token,放在请求的header里发送。由于我们使用了请求拦截的方式,所以所有的后端数据发送的时候,都会带上这个token,完成受限数据的请求。
|
||
|
||
```javascript
|
||
service.interceptors.request.use(
|
||
config => {
|
||
const token = getToken()
|
||
// do something before request is sent
|
||
if (token) {
|
||
config.headers.gtoken = token
|
||
}
|
||
return config
|
||
},
|
||
error => {
|
||
// do something with request error
|
||
console.log(error) // for debug
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
```
|
||
|
||
通过上面的操作,我们完成了前端网络请求的token限制。但是还有一个需求没有实现,就是用户没有登录某个受限页面的时候,或者说没有token的时候,如果直接访问受限页面,比如个人中心,那么就需要让vue-router拦截这次页面的跳转。
|
||
|
||
与vue-router拦截页面的跳转,并显示无权限的报错信息相比,直接跳转登录页是现在更流行的交互方式。但这种方式需要在vue-router上加一层限制,这层限制就是说,在路由跳转的时候做权限认证,我们把vue-router的这个功能称作导航守卫。
|
||
|
||
关于导航守卫的API,你可以从Vue Router的官网看到很详细的[介绍](https://next.router.vuejs.org/zh/guide/advanced/navigation-guards.html)。这里我们实际应用下,下面的代码中,我们在router.beforeEach函数中设置一个全局的守卫。
|
||
|
||
每次页面跳转的时候,vue-router会自动执行守卫函数,如果函数返回false的话,页面跳转就会失败。不过,我们也可以手动地跳转到其他页面。现在我们设置的路由很简单,如果token不存在的话直接跳转登录页面,否则返回true,页面正常跳转。
|
||
|
||
```javascript
|
||
router.beforeEach(async (to, from,next) => {
|
||
// canUserAccess() 返回 `true` 或 `false`
|
||
let token = getToken()
|
||
if(!token){
|
||
next('/login')
|
||
}
|
||
return true
|
||
})
|
||
|
||
```
|
||
|
||
当然,在路由守卫的函数内,只要是页面跳转时想实现的操作,都可以放在这个函数内部实现,比如一些常见的交互效果,就像给项目的主页面顶部设置一个页面跳转的进度条、设置和修改页面标题等等。和我们现在对全部页面进行一次性的粗略拦截相比,后面还会在路由守卫那里进行更精确的路由拦截。
|
||
|
||
到这里你可能会有疑问:之前开发项目的时候,和登录注册相关的配置,不需要自己管理token,都是后端直接设置cookie。那么这里用到的token和之前项目开发时,交给后端设置的cookie到底有什么区别呢?
|
||
|
||
这是个非常好的问题,我们在第一讲聊前端发展史的时候,提到了jQuery时代的前端项目是作为后端项目的模块部署的。
|
||
|
||
那时候前后端不分家,整个应用的入口是后端控制模板的渲染。在模板渲染前,后端会直接判断路由的权限来决定是否跳转。登录的时候,后端只需要设置setCookie这个header,之后浏览器会自动把cookie写入到我们的浏览器存起来,然后当前域名在发送请求的时候都会自动带上这个cookie。
|
||
|
||
在Chrome浏览器中,我们先进入极客时间的官网,然后打开调试窗口页面,再选择Network页面。之后,我们在页面中点击Fetch/XHR,然后在Name这一栏中,我们可以任选一个接口点开。这样,我们就可以看到这个接口请求的所有细节了。
|
||
|
||
在下图中,我们点击list请求,也就是极客时间的推荐接口时,HTTP的Request Headers里就有Cookie这个数据,这是浏览器自动管理和发送的,也算是权限认证的最佳方案之一。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/98/8e/98b1ce4137ecfb9ea01dc4ea0381e68e.png?wh=1920x1084)
|
||
|
||
但是,在现在这种前后端分离的场景下,通常前后端项目都会部署在不同的机器和服务器之上,Cookie在跨域上有诸多的限制。所以在这种场景下,我们更愿意手动地去管理权限,于是就诞生了现在流行的基于token的权限解决方案,你也可以把token理解为我们手动管理的cookie。
|
||
|
||
## 角色权限
|
||
|
||
实现登录权限验证之后,我们就可以针对项目中的页面进行登录的保护。但现在,我们只能通过登录状态去判断页面的显示与否,而这远远达不到我们实际开发的需求。
|
||
|
||
比如,在我们的管理系统开发中,订单页面是所有人都可以看到的,但是像账单的查询页面,以及其他一些权限更高的页面,我们需要管理员权限才能看到。这时候,我们就需要对系统内部的权限进行分级,每个级别都对应着可以访问的不同页面。
|
||
|
||
我们通常使用的权限解决方案就是RBAC权限管理机制。简单来说,就是在下图所示的这个模型里,除了用户和页面之外,**我们需要一个新的概念,就是角色****。**每个用户有不同的角色,每个角色对应不同的页面权限,这个数据结构的关系设计主要是由后端来实现。
|
||
|
||
根据下图这个结构,在用户登录完成之后我们会获取页面的权限数据,也就是说后端会返回给我们当前页面的动态权限部分。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/e9/95/e93dd5a15c93b193debe462eb0349c95.jpg?wh=1347x697)
|
||
|
||
这样有一部分页面是写在代码的src/router/index.js中,另外一部分页面我们通过axios获取数据后,通过调用vue-router的addRoute方法动态添加进项目整体的路由配置中。
|
||
|
||
关于这部分动态路由的内容,官网的文档中有详细的[API介绍](https://next.router.vuejs.org/zh/guide/advanced/dynamic-routing.html)。在下面的代码中,我们在Vuex中注册addRoute这个action,通过后端返回的权限页面数据,调用router.addRoute新增路由。
|
||
|
||
```javascript
|
||
addRoutes({ commit }, accessRoutes) {
|
||
// 添加动态路由,同时保存移除函数,将来如果需要重置路由可以用到它们
|
||
const removeRoutes = []
|
||
accessRoutes.forEach(route => {
|
||
const removeRoute = router.addRoute(route)
|
||
removeRoutes.push(removeRoute)
|
||
})
|
||
commit('SET_REMOVE_ROUTES', removeRoutes)
|
||
},
|
||
|
||
```
|
||
|
||
与新增路由对应,在页面重新设置权限的时候,我们需要用router.removeRoute来删除注册的路由,这也是上面的代码中我们还有一个remoteRoutes来管理动态路由的原因。
|
||
|
||
然后,我们需要把动态路由的状态存储在本地存储里,否则刷新页面之后,动态的路由部分就会被清空,页面就会显示404报错。我们需要在localStorage中把静态路由和动态路由分开对待,在页面刷新的时候,通过src/router/index.js入口文件中的routes配置,从localStorage中获取完整的路由信息,并且新增到vue-router中,才能加载完整的路由。
|
||
|
||
权限系统中还有一个常见的问题,就是登录是有时间限制的。在常见的登录状态下,token有效期只能保持24小时或者72小时,过了这个期限,token会自动失效。即使我们依然存在token,刷新页面后也会跳转到登录页。所以,对token有效期的判断这个需求该如何实现呢?
|
||
|
||
首先,token的过期时间认证是由后端来实现和完成的。如果登录状态过期,那么会有一个单独的报错信息,我们需要在接口拦截函数中,统一对接口的响应结果进行拦截。如果报错信息显示的是登录过期,我们需要清理所有的token和页面权限数据,并且跳转到登录页面。
|
||
|
||
下面的代码中,50008和50012都代表着状态过期,我们可以直接使用Element3的messageBox组件显示一个错误信息,提示用户需要重新登录,然后直接跳转到登录页面就可以了。
|
||
|
||
```javascript
|
||
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
|
||
if (res.code === 50008 || res.code === 50012) {
|
||
// to re-login
|
||
Msgbox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
|
||
confirmButtonText: 'Re-Login',
|
||
cancelButtonText: 'Cancel',
|
||
type: 'warning',
|
||
}).then(() => {
|
||
store.dispatch('user/resetToken').then(() => {
|
||
location.reload()
|
||
})
|
||
})
|
||
}
|
||
return Promise.reject(new Error(res.message || 'Error'))
|
||
|
||
```
|
||
|
||
## 总结
|
||
|
||
今天这一讲的内容就讲完了,我们来总结一下今天学到的重点吧。首先,在登录权限这一部分,我们模拟了登录的接口和页面,登录成功后的token存储在localStorage中,使用axios的拦截器管理所有接口的token信息。
|
||
|
||
然后,我们学习了路由守卫的概念,借助路由守卫,我们可以实现页面的权限保护,让指定页面需要登录之后才可以访问。我们使用的是token权限校验机制,在用户登录之后,后端会把用户信息加密成一个token返回给前端,然后前端把token存储在localStorage中。
|
||
|
||
在axios的全局拦截函数中,每个请求发出去之前都会获取token,然后放在HTTP请求的header之中,后端会读取这个token进行权限认证。除了后端接口的权限,vue-router也会做页面的权限拦截,这个功能也就是路由守卫,vue-router允许你在所有路由跳转之前执行钩子函数,这个函数内部我们做权限认证,如果符合要求就允许跳转,否则就跳转到404页面。
|
||
|
||
最后,我们还了解了更复杂一些的权限系统设计,在这种设计下,用户可以根据角色的划分,对应到不同的页面权限,一部分页面是登录后就可以访问,一部分页面是需要额外地获取后端接口,匹配到当前用户权限之后,才可以访问的页面,我们把这种权限设计称之为动态路由。vue-router提供的addRoute和removeRoute这两个函数,可以很好地帮助我们实现这一功能。
|
||
|
||
## 思考题
|
||
|
||
最后,是给你留的一个思考题,如果在任意一个页面里,我们想实现按钮级别的权限认证,那我们应该如何扩展我们的权限系统呢?
|
||
|
||
欢迎在留言区分享你的看法,也欢迎你把这一讲的内容分享给你的同事、朋友们,我们下一讲再见。
|
||
|