概述
完成了对auth前端的编码部分,这里对整个过程做个总结。
整体的过程,如下图所示:
这里1-7是登录,8-11是认证与鉴权。
认证:如果token不携带,或者cache中没有token,认证没有登录,返回401(可以自定义)
鉴权:判断user在该org下是否有路由(资源)所要求的访问权限
具体过程这里不再展开,下边对前后端实现过程中出现的几个问题个总结。
跨域问题
token返回时,最先使用的cookie形式,通过:
Cookie cookie = new Cookie("token", token);
cookie.setMaxAge(60 * 60);
cookie.setPath("/");
response.addCookie(cookie);
将token加到cookie中,这种的好处是不需要前端编码,浏览器完成了cookie的存储与携带过程。但这种好处带来的是受制于浏览器的跨域策略。
单机开发时,前后端的端口是不一致的,这样浏览器就会认为它们是2个domain,后端addCookie是发在了后端访问URL上,前端访问时也就不会携带相应的cookie了。
这个问题有2种解决方式:
-
niginx方式
单机情况,可以通过配置nginx的方式,让nginx来做转发,后端与前端的域名相同这就能继续使用cookie了。
-
body返回token方式
放弃cookie,而是用header。将token通过body体返回,前端收到后保存到自己的storage中,进行访问时,通过axios的拦截器机制,将token放到相应的位置上,这个位置既可以是header(如: Authorization),也可以是cookie。这种方式使用范围更广一些。
动态路由机制
在很多情况下,需要根据不同用户的角色(权限),来看到不同的页面,这种情况我称之为“动态路由机制”。
回顾前端的控制结构:
由router做总控,来渲染各个View。View中一些公共的组件称为Layout,放在父route中携带,各页面私有的组件,放到子route中携带。其中最重要的Layout就是Navigation,也就是各个菜单。
总结:route => view => Layout => Navigation
机制
所谓不同权限用户看到不同的页面,也就是看到不同的Navigation,这个机制如下图所示:
途中灰色框代表action或change、空框代表Data。
从Data来看,主要的data有2个,全部的路由allRoutes与用户拥有的权限permission,有这两个数据进行match对比,计算出用户拥有拥有的路由userRoutes与用户没有的路由ignoreRoutes。用userRoutes去注册菜单,控制用户能看到的页面; 用ignoreRoutes进行route的拦截,对不能访问的路由进行重定向。
route配置
这里主要的算法是match与registerMenu,它们都可以看做树的遍历。难点是route的结构与Menu的结构是不匹配的,对于父route只做携带Layout的情况,需要将children渲染到父一级的menu上。allRoutes的结构如下:
export const allRoutes: Array<RouteRecordRaw> = [
{
path: "/",
name: "Layout",
component: Layout,
redirect: "/dashboard",
meta: {
menu: MenuType.ADDON
},
children: [
{
path: "/dashboard",
name: "Dashboard",
meta: {
title: "首页",
icon: "HomeFilled",
menu: true,
permissionList: [],
},
component: () => import("@/views/Dashboard.vue"),
},
],
},
...settingRoutes,
...commonRoutes,
];
Match的算法
function filterUserRoutes(routes:RouteRecordRaw[], permissionList: string[], ignoreRoutes:RouteRecordRaw[]) {
const res:RouteRecordRaw[] = []
routes.forEach(route => {
const tmp = { ...route }
if(tmp.children){
// 递归判断各个children
tmp.children = filterUserRoutes(tmp.children, permissionList, ignoreRoutes)
// 子route都过滤掉的情况,父route也过略掉
if(tmp.children.length == 0){
ignoreRoutes.push(tmp)
return res;
}
}
// 匹配
if(!tmp.meta
|| !tmp.meta.permissionList
|| (tmp.meta as any).permissionList.length==0
|| permissionList.some(perm => (tmp.meta as any).permissionList.includes(perm))
){
res.push(tmp);
}else{
ignoreRoutes.push(tmp);
}
})
return res
}
registerMenu的过程是将算法融入到组件中,组件的render像极了函数的调用:
父组件,递归的入口:
<template>
<div class="sidebar">
<el-menu class="sidebar-el-menu" :default-active="onRoutes" :collapse="isCollapse" router>
<sidebar-item v-for="route of menuRoutes" :key="route.path" :item="route"></sidebar-item>
</el-menu>
</div>
</template>
子组件,递归调用sidebar-item:
<template>
<div v-if="props.item.meta.menu != MenuType.NONE">
<template v-if="checkMenu(props.item)">
<el-menu-item
v-for="menu of state.menuList"
:index="resolvePath(menu.path)"
:key="resolvePath(menu.path)"
>
<el-icon v-if="menu?.meta?.icon">
<component :is="menu.meta.icon"></component>
</el-icon>
<template #title>{{ menu.meta.title }}</template>
</el-menu-item>
</template>
<el-sub-menu v-else :index="resolvePath(props.item.path)">
<template #title>
<el-icon v-if="item?.meta?.icon">
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{ props.item.meta.title }}</span>
</template>
<sidebar-item
v-for="child in props.item.children"
:key="child.path"
:item="child"
:base-path="resolvePath(props.item.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<script lang="ts" setup>
// ...省略
let state: UnwrapRef<any> = reactive({
menuList: null,
basePath: props.basePath,
})
const checkMenu = (item: any) => {
if (item.meta.menu == MenuType.ADDON) {
state.menuList = item.children;
state.basePath = resovle(props.basePath, item.path);
} else {
if (item?.children?.length > 0) {
state.menuList = []
} else {
state.menuList = [item]
}
}
return state.menuList?.length != 0;
}
// ...省略
</scripte>
在递归的过程中,对于父route只做携带的情况,在checkMenu()中进行判断。
改进
joynop同学提醒,对于ignoreRoute的拦截部分可能直接使用permission,这样不仅复杂度降低一点,效率上也会变快一些,确实如此,感谢!

reactive图标问题
-
问题调试记录
这个问题是需要是动态路由机制中出现的问题,在route中设置的图标没有正确的显示出来,而且报warnning:
Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref
意思是reactive的component不能直接放到<component :is="menu.meta.icon"></component>
中去显示。
经过一帆查找后发现可以通过toRaw的方式来将reactive的变成非reatice的,<component :is="toRaw(menu.meta.icon)"></component>
。
但是这时候又出现了问题,显示组件没有render函数,无法进行渲染:
Component is missing template or render function.
又经过一帆对比后发现,原来这是由于存localstorage时,进行了json序列化缘故,组件经过序列化丧失了render()函数,导致无法序列化成功。
解决的办法,需要全局注册图标,然后通过String的方式来引用图标,跳过组件的json化过程。
-
main中全局注册
import installElementPlus from './utils/element'
const app = createApp(App)
installElementPlus(app)
app
.use(store)
.use(router)
.mount('#app')
for (const name in ElIcons){
app.component(name,(ElIcons as any)[name])
}
-
routes配置修改
将routes配置中meta中的icon由组件(HomeFilled)转换成string("HomeFilled")
{
path: "/dashboard",
name: "Dashboard",
meta: {
title: "首页",
icon: "HomeFilled",
menu: true,
permissionList: [],
},
component: () => import("@/views/Dashboard.vue"),
},
}
-
直接使用icon
<el-icon v-if="menu?.meta?.icon">
<component :is="menu.meta.icon"></component>
</el-icon>
多页面登录测试
死循环问题
-
问题描述
多页面登录:在浏览器中先看多个login页面,然后分别去登录,接着在某个页面中点击退出,最后其他页面进行操作。
正常情况下,其他页面也应该退出,这里也正常退出了,但出现了一致调用logout的情况。
-
原因分析
经过调试发现,这种情况是由于执行logout时,被拦截器拦截掉,由于本该携带的token已经删除,返回401。而前端的axios拦截中,对于401情况又执行了logout(主要为了清理缓存),这样就形成了一个死循环。
-
处理
将前端401的执行的logout修改为清理缓存,打破这个循环。
缓存问题