You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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这两个函数可以很好地帮助我们实现这一功能。
## 思考题
最后,是给你留的一个思考题,如果在任意一个页面里,我们想实现按钮级别的权限认证,那我们应该如何扩展我们的权限系统呢?
欢迎在留言区分享你的看法,也欢迎你把这一讲的内容分享给你的同事、朋友们,我们下一讲再见。