问题倒不难,但是这个问题之前被松哥忽略了。前两天有小伙伴提出这个疑问,我觉得有必要写篇文章和大家捋一捋这个问题。
先来看一个简短的视频:
一个简单的配置就解决掉 404 问题了,接下来,我再来把这件事的来龙去脉和大家仔细捋一捋。
1.职责划分
在传统的前后端不分的开发中,权限管理主要通过过滤器或者拦截器来进行(权限管理框架本身也是通过过滤器链来实现功能),如果用户不具备某一个角色或者某一个权限,则无法访问某一个页面。
但是在前后端分离中,页面的跳转统统交给前端去做,后端只提供数据,这种时候,权限管理不能再按照之前的思路来。
首先要明确一点,前端是展示给用户看的,所有的菜单显示或者隐藏目的不是为了实现权限管理,而是为了给用户一个良好的体验(把用户没有权限的按钮隐藏起来,避免用户点击后提示 403,提高用户体验),不能依靠前端隐藏控件来实现权限管理,即数据安全不能依靠前端。
这就像普通的表单提交一样,前端做数据校验是为了提高效率,提高用户体验,后端才是真正的确保数据完整性。
所以,真正的数据安全管理是在后端实现的,后端在接口设计的过程中,就要确保每一个接口都是在满足某种权限的基础上才能访问,也就是说,不怕将后端数据接口地址暴露出来,即使暴露出来,只要你没有相应的角色/权限,也是访问不了的。
前端为了良好的用户体验,需要将用户不能访问的接口或者菜单隐藏起来。页面的跳转,按钮的隐藏/展示等等,统统在前端来实现。
2.存在的问题
当前后端分离之后,对于前端所承担的职责,大家可能会面临一个问题:如果用户直接在地址拦输入某一个页面的路径,怎么办?
此时,如果没有做任何额外的处理的话,用户确实可以通过直接输入某一个路径进入到系统中的某一个页面中,但是,不用担心数据泄露问题,因为没有相关的角色/权限,就无法访问相关的接口,即使进入到相关的页面,也看不到数据。
但是,如果用户非这样操作,进入到一个空白的页面,用户体验不好,冒出来一个空白页面,有的用户就手足无措了。
此时,我们可以使用 Vue 中的前置路由导航守卫,来监听页面跳转,如果用户想要去一个未获授权的页面,则直接在前置路由导航守卫中将之拦截下来,重定向到登录页,或者直接就停留在当前页,不让用户跳转,也可以顺手再给用户一点点未获授权的提示信息。
以 vhr 中的代码为例,我在 main.js 中定义了前置路由导航守卫:
1 | router.beforeEach((to, from, next) => { |
这个方法有点类似于 Java 中的过滤器,to 表示要去哪里,有点像 HttpServletResponse;from 表示从哪来,有点像 HttpServletRequest;next 表示一个请求继续向下执行的方法,有点类似于 FilterChain。
这里会监控到所有的页面路由/跳转,主要逻辑是这样:
- 如果要去的地址是 ‘/‘,即要去的地方是登录页面,则直接执行 next 方法表示放行。
- 如果要去的地址不是 ‘/‘,那就要看用户是否登录了,如果已经登录了,则先初始化菜单,然后调用 next 方法继续向下走,想去哪去哪。
- 如果没有登录,则调用 next 方法,跳转路径是 ‘/‘,即回到登录页面,同时携带上一个 redirect 参数,这个是重定向的地址,这个参数的作用是这样:例如我本来输入 ‘/aa/bb’,结果因为没有登录,自动跳转到项目登录页面,当我登录成功后,自动跳回 ‘/aa/bb’。
有这个配置之后,就不怕用户乱跳转了,如果没有登录随意输入一个地址,就会回到登录页面。
3. 404问题
上面的配置还存在一个 404 问题。
在用户还没有登录的时候,如果他在浏览器输入一个不存在的地址,就会自动回到登录页面,这没有问题,但是用户如果已经登录了,在浏览器输入一个不存在的地址,这个时候就会发生 404,当你没做任何定义的时候,所谓的 404 页面其实就是一片空白。
要解决这个问题,很多小伙伴第一个思路就是能不能在前置路由导航守卫里边做一个判断,当你要跳转的时候,先去判断一个跳转的路径是否存在,如果存在再去跳转,否则就不去跳转。
3.1 动态路由
这个思路看起来没问题,但实际上还有更简便的办法,那就是使用 vue-router 中的动态路由。
举一个简单的例子,我们有一个用户展示的页面,这个页面会根据不同的用户 id 来展示不同的用户数据,所以我们在 router.js 中可以按如下方式来定义路由:
1 | routes: [ |
注意这里标记的参数 id 前面有一个 :
。定义完成后,以后像 /user/1
、/user/2
都会映射到相同的路由。而像地中的 1 、2 等参数,我们则可以通过 this.$route.params.id
获取。
上面这种是设置一个参数,我们也可以设置多个参数,我们可以参考官方给出的一个表格:
如果有多个参数,也可以通过 this.$route.params
来获取参数的值。
注意,在动态路由匹配时,如果我们从 /user/1
切换到 /user/2
,原有的 User 组件是不会销毁的,这也意味着组件的生命周期钩子函数不会再被调用,那么要怎么刷新数据呢?这个时候我们可以采用 beforeRouteUpdate 导航守卫,在导航守卫中完成数据更新:
1 | router.beforeRouteUpdate((to, from, next) => { |
3.2 解决 404
最后,就是我们本文要说的 404 问题了。看懂了前面,如何解决 404 其实就很容易明白了。
我们可以使用通配符 *
来匹配任意路径,例如下面这段匹配格式:
1 | { |
这个匹配规则会匹配到所有路径,通常就是用来解决前端页面的 404 问题。也可以自己定义一些前缀,例如下面这样:
1 | { |
当我们使用通配符的时候,可以通过 this.$route.params.pathMatch
来获取通配符匹配到的路径,例如用户请求路径是 /javaboy-aaa
,则 this.$route.params.pathMatch
的值就为 aaa
。
另外还有一个比较重要的点,就是通配符路径的顺序问题。如果路径带有通配符,一般来说要放在路由的最后面。