【中台】靓仔请留步,跟我一起撸中后台啊~(系列篇三:鉴权、动态路由 )


前言

当我们的项目依赖都配置完毕之后,最重要的一步就来了,那就是如何去处理用户权限,给用户分配指定的菜单。首先需要知道的一点是,我们的路由表只会配置一小部分,这部分是不需要任何权限就能访问的,也就是白名单,比如登录路由404路由500路由等等,其它的我们都可以从接口里面去获取,这样我们就可以根据用户权限来做一些过滤处理,从而达到不同用户显示不同菜单和路由的目的。

当然也有其它方式来做这些处理,个人目前还是比较喜欢这种方式。好了,话不多说,直接开撸。

配置axios拦截器

像配置axios拦截器,一千个人估计有一千种写法,不过核心的逻辑是不会变的,无非就是做一些设置一些请求头和对响应数据做一些处理。

创建实例

首先我们创建一个 axios 实例,通过 axios.create(config) 方法。baseURL 将自动加在我们的 url 前面(除非url是一个绝对URL),timeout表示超时时间,如果请求超过了这个时间还没有响应,请求会被中断。

const service = axios.create({
  baseURL: defaultConfig.zhtApiUrl,
  timeout: timeout * 1000
})
 

创建好实例之后,就可以配置拦截器了。

请求拦截器

在请求拦截器中,我们可以对请求头做一些处理。比如我们可以在请求拦截器上添加 TOKEN,可以这样做:

service.interceptors.request.use(
  config => {
    const TOKEN = getToken()
    if (TOKEN) {
      config.headers.Authorization = TOKEN
    }
    return config
  },
  error => {
    Promise.reject(error)
  }
)
 

响应拦截器

在响应拦截器中,主要是对响应数据做处理,比如响应的状态,响应的数据,以及一些错误处理。

service.interceptors.response.use(
response => {
const { url: apiUrl } = response.config
const { status, statusText, data } = response
if (status === 200) {
if (apiUrl!.includes('auth/social/token')) {
// 获取 user token 的接口,返回格式跟其它接口有区别,
return {
code: 0,
msg: 'success',
data
}
} else {
return data
}
} else {
// 报错
message.error(`出错了哦 status: ${status} - statusText: ${statusText}`)
return Promise.reject({
code: 1,
msg: statusText
})
}
},
error => {
// { response, message, request, config } = error
if (error.response) {
const { status, statusText } = error.response
if (status === 401) {
message.error(`接口没有权限访问,请检查接口或者参数! status: ${status} - statusText: ${statusText}.`)
} else if (status === 500) {
message.error(`请检查接口或服务器状态! status: ${status} - statusText: ${statusText}.`)
router.push('/portal-error500')
} else {
message.error(`出错啦! message: ${error.message} - statusText: ${statusText}.`)
}
} else {
message.error(`禁止访问! 错误信息: ${error}`)
}
}
)

每个人或者说不同的项目,可能配置都不一样,所以,这里的配置仅供参考,具体的配置还需要根据业务需求来。

取消拦截器

如果你想在什么时候取消拦截器,可以这样做:

const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

导航守卫

vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。

我们可以创建一个 permissions.ts 文件,来写一些全局的导航守卫。

全局前置守卫

你可以使用 router.beforeEach 注册一个全局前置守卫。

router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})

permissions.ts 文件中,我们就可以这样去配置,在导航跳转前,我们需要判断当前用户是否登录,如果他想去的不是登陆页面,那么我们就让他跳转到登录页,如果登录了,就判断是否他已经获取了权限,如果没有获取权限,我们就去调接口,否则我们就直接进入到项目的首页(如果记住了跳转的页面,就 redirect 到那个页面,而不是首页)。

我们的登录接口仅仅只是返回了跟 TOKEN 相关的信息,那么还需要根据 token 去获取此用户的权限,以及其它信息。

router.beforeEach(async (to, from, next) => {
const token = getToken()
const isWhite = whiteList.findIndex(w => w === to.path)
NProgress.start()
if (token) {
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取应用
await store.dispatch(`userModule/${userAction.GetApplictions}`)
// 获取用过户信息权限
const roles = await store.dispatch(`userModule/${userAction.GetInfo}`)
// 获取路由配置,可以根据 roles 来过滤
const accessRoutes = await store.dispatch(`permissionsModule/${permissionAction.GenerateRoutes}`, roles)
next({ path: '/', replace: true })
} catch (error) {
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
} else {
if (to.path === '/login') {
next()
NProgress.done()
} else if (isWhite > -1) {
// 在白名单范围之内
next()
} else {
next('/login')
}
}
})

全局后置钩子

我们也可以创建全局后置钩子,它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。

router.afterEach((to, from) => {
NProgress.done()
})

状态管理 Store

权限模块

创建一个用户模块(store/modules/permissions/index.ts),这里作为用来存储、权限处理路由的中心。

state 类型

首先我们定义好 permissions 里面的 state 类型,我在这个里面只有一个 routes 数据,所有只需要定义这个 routes 类型就可以了。

export type ChildRouteType= Array<RouteType>
export interface RouteType {
name: string,
path: string,
component: string,
redirect?: string,
children?: ChildRouteType
}
export default interface PermissionsStateTypes {
routes: Array<RouteType>
}

mutations 和 actions 常量

接着定义好我们的 MutationTypesActionTypes 类型,当然如果不定义也可以,只是页面中可能就导出充斥着魔法字符串了。(官方也是建议我们这样去定义一些常量,后续就改动的话也只需要改一处地方了)

export enum MutationTypes {
SetRoutes = "SET_ROUTES"
}
export enum ActionTypes {
GenerateRoutes = "GENERATE_ROUTES"
}

mutations 和 actions 方法类型

定义好这些之后,我们还需要定义 mutationsactions 方法类型。

export type Mutations<T = PermissionsStateTypes> = {
[MutationTypes.SetRoutes](state: T, routes: RouteType[]): void
}
type ActionArgs = Omit<ActionContext<PermissionsStateTypes, RootState>, 'commit'> & {
commit<k extends keyof Mutations>(
key: k,
payload: Parameters<Mutations[k]>[1]
): ReturnType<Mutations[k]>
}
export type Actions = {
[ActionTypes.GenerateRoutes]({ commit }: ActionArgs, roles: string[]): void
}

实现

定义好所有的类型之后,就可以开始实现 permissions 里面的核心逻辑了。

actions 中,我们去获取接口中的路由数据,然后通过我们设置的 roles 可以去做过滤处理,如果有权限的路由,我们就可以通过 router.addRoute 去动态添加。

由于接口返回的路由并不是真正的组件,

const state: PermissionsStateTypes = {
routes: []
}
const mutations: MutationTree<PermissionsStateTypes> & Mutations = {
[MutationTypes.SetRoutes](state: PermissionsStateTypes, routes: RouteType[]) {
state.routes = routes
}
}
const handleParseChildRoutes = (childs: ChildRouteType, prePath: string): ChildRouteType => {
if (childs) {
// @ts-ignore
return childs.map((c: RouteType) => {
return {
name: c.name,
path: c.path,
component: c.component === 'RouterView' ? RouterView : () => import(`@/views${prePath}/index.vue`)
}
})
} else {
return []
}
}
const actions: ActionTree<PermissionsStateTypes, RootState> & Actions = {
[ActionTypes.GenerateRoutes]({ commit }, roles: string[]) {
// 可以根据 roles 来选择性返回 route
// console.log('permissions roles', roles)
return new Promise<void>((resolve, reject) => {
// 这里的 asyncRoutesMap 是静态的数据,模拟从后台接口调用数据
asyncRoutesMap.forEach((route: RouteType) => {
router.addRoute({
name: route.name,
path: route.path,
redirect: route.redirect ? route.redirect : undefined,
component: () => import(`@/${route.component.toLowerCase()}/index.vue`),
children: handleParseChildRoutes((route.children as ChildRouteType), route.path)
})
})
// 经过处理之后,可以返回 accessRoutes
commit(MutationTypes.SetRoutes, asyncRoutesMap)
resolve(asyncRoutesMap)
})
}
}

用户模块

创建用户模块(store/modules/user/index.ts),用来存储用户信息、用户的权限以及应用和菜单的中心。

state类型

用户模块的 state 包含了用户姓名、权限、头像、介绍、当前中台的应用对象(currentApp)以及所有应用的数组集合。

export interface ApplicationType {
id?: number
appId?: number
path?: string
menuName?: string
childMenu?: Array<ApplicationType>
[propName:string]: any
}
export default interface UserModuleStateType {
name: string
roles: Array<string>
currentApp: ApplicationType
applications: Array<ApplicationType>
avatar?: string
introduction?: string
}

mutations 和 actions 常量

export enum MutationTypes {
SetRoles = "SET_ROLES",
SetApplications = "SET_APPLICATIONS",
SetCurrentApp = "SET_CURRENT_APP"
}
export enum ActionTypes {
GetInfo = "GET_INFO",
GetApplictions = "GET_APPLICATONS"
}

mutations 和 actions 方法类型

type ActionArgs = Omit<ActionContext<UserModuleStateType, RootState>, 'commit'> & {
commit<k extends keyof Mutations>(
key: k,
payload: Parameters<Mutations[k]>[1]
): ReturnType<Mutations[k]>
}
export type Mutations<T = UserModuleStateType> = {
[MutationTypes.SetRoles](state: T, roles: Array<string>): void
[MutationTypes.SetApplications](state: T, apps: Array<ApplicationType>): void
[MutationTypes.SetCurrentApp](state: T, app: ApplicationType): void
}
export type Actions = {
[ActionTypes.GetInfo]({ commit }: ActionArgs): void
[ActionTypes.GetApplictions]({ commit }: ActionArgs): void
}

实现

定义好所有的类型之后,就需要去实现具体的逻辑了。

这里面包括设置当前用户角色 SET_ROLSE(即权限),获取应用数据集合 SET_APPLICATIONS以及设置当前中台所接入的应用对象 SET_CURRENT_APP。其中数据都是静态数据,模拟从后台接口调用的数据。

const state: UserModuleStateType = {
name: '',
avatar: '',
introduction: '',
roles: [],
applications: [],
currentApp: {}
}
const mutations: MutationTree<UserModuleStateType> & Mutations = {
[MutationTypes.SET_ROLES](state: UserModuleStateType, roles: Array<string>) {
state.roles = roles
},
[MutationTypes.SET_APPLICATIONS](state: UserModuleStateType, apps: Array<ApplicationType>) {
state.applications = apps
},
[MutationTypes.SET_CURRENT_APP](state: UserModuleStateType, app: ApplicationType) {
state.currentApp = app
}
}
const actions: ActionTree<UserModuleStateType, RootState> & Actions = {
[ActionTypes.GetInfo]({ commit }) {
return new Promise((resolve, reject) => {
// getInfo() 获取接口
const roles = ['admin', 'editor']
commit(MutationTypes.SET_ROLES, roles)
resolve(roles)
})
},
[ActionTypes.GetApplictions]({ commit }) {
return new Promise<void>((resolve, reject) => {
commit(MutationTypes.SET_APPLICATIONS, applications)
commit(MutationTypes.SET_CURRENT_APP, applications[0])
resolve()
})
}
}
const UserModule: Module<UserModuleStateType, RootState> = {
namespaced:  true,
state,
mutations,
actions
}
export default UserModule

到这里,我们的用户模块也就配置好了。

总结

vuex 中我们采用了大量 TypeScript 语法,很多都是 TypeScript 中内置工具类型,比如:

  • Pick<T, K>

    能够从已有对象类型中选取给定的属性及其类型,然后构建出一个新的对象类型。

  • Omit<T, K>

    与“Pick<T, K>”工具类型是互补的,它能够从已有对象类型中剔除给定的属性,然后构建出一个新的对象类型。

还有很多具有实用性的工具类型待我们去发现和使用,在使用 TypeScript 的过程中,可能会遇到很多问题,这个是需要时间去打磨的,写得越多也就越熟练。

一起交流

不管你遇到什么问题,或者是想交个朋友一起探讨技术(=。=),都可以加入我们的组织,和我们一起 ~

喜欢这部分内容,就加入我们的QQ群,和大家一起交流技术吧~

QQ群1032965518

原创文章,作者:我心飞翔,如若转载,请注明出处:https://www.pipipi.net/14751.html

发表评论

登录后才能评论