CustomAuth总结

概述

完成了对auth前端的编码部分,这里对整个过程做个总结。

整体的过程,如下图所示:

custom-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
  }

注册Menu

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修改为清理缓存,打破这个循环。

缓存问题

  • 问题描述

    这种测试还发现一个缓存问题,前端虽然退出,但在缓存中依然存在的token,一定程度上造成了内存的泄露

  • 原因

    对于同一用户每次login发放的token不同,前端会覆盖每次的localstorage,这就造成了泄露,退出时只删除了最后一次的token。

  • 处理

    对每个用户发放相同的token

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×