旭锋运营管理平台v2--客户端路由动态构建
1 业务需要
根据当前登录用户拥有操作权限构建与之对应的控制页面跳转的路由列表。即每一个用户的路由列表中,只包含系统公共页面和拥有操作权限页面的路由信息。
1.1 需要原由
运营管理平台需要实现基于RBAC权限模块的操作权限管理。
客户端页面跳转由vue-router通过路由列表控制,而非由服务端控制。要实现用户依据权限访问对应页面,实现中心思路:即不提供无权限页面跳转入口即不提供路由系统触发方式。
对于客户端项目而言,触发路由的方式包含两种:以dom事件触发和通过地址栏输入URL触发两种。而对于路由列表中包含的路由信息,即使用不提供DOM事件触发方式,也可以通过地址栏输入URL方式触发该路由信息。如果要确保用户不能访问无权限的页面,最核心的方式即为路由列表中不包含对应页面的信息。同时不提供DOM事件触发路由的入口,即便提供了DOM事件入口,由于没有对应路由信息,也不会进入对应的页面。
如果路由列表以静态方式包含当前系统所有路由信息时,即使系统不提供DOM事件触发入口,也可以通过输入URL方式跳转至该页面。
1.2 实现要点
要实现依据用户权限构建路由列表,要确定动态构建路由时机,即什么时候构建路由,什么时候需要重新构建路由。
1.2.1 vue-router的特点
- 静态路由列表:以硬编码方式,在初始化
const routes: Array<RouteRecordRaw>
时写入所有路由信息。供创建router对象时使用,其特点:- 当点击浏览器刷新按钮时,构建的路由列表不会被销毁。
- 动态路由列表:通过vue-router提供的
addRoute()
api动态添加的。其特点:- 通过该方式添加的路由信息,如果与以前路由列表中有相同name属性时,将覆盖以前路由列表中对应的路由信息。
- 当点击浏览器刷新按钮时,则动态构建的路由列表将会被销毁。
1.2.2 创建时机
由于动态创建的路由信息列表,在浏览器刷新时会被销毁特点,而用户刷新浏览器的动作无法预先确定,因此,在每次路由跳转触发前,都需要做好构建和重新构建路由列表准备。具体实现在全局路由前置导航守护中。
1.2.3 重建标识
并不是每次路由跳转都需要重新构建动态路由列表,因为并不是每次跳转之前,用户就一定点击刷新按钮,而导致原路由列表销毁;因此需要确定重建路由列表标识。
- 标识选择
- 当点击刷新按钮后,vue-router和vuex同时会被重新初始化,可以在vuex中设置状态标识,用于标识vue-router被初始化。
2 实现逻辑
- 当触发路由跳转时,校验当前路由跳转路径
- 如果跳转路径为
/login
,- 清空sessionStorage,目的:如果用户通过退出本系统时,清理当前用户登录token。
- 重置vuex。以便再次登录时,重新构建路由列表。
- 如果跳转路径为
- 跳转路径为其它路径时,校验是否存在token
- 如果不存在,则强制跳转到login页面。
- 如果存在token,则检验当前路由列表是否被重置。
- 如果被重置,则请求当前用户拥有权限菜单信息,重新构建路由列表,构建完成后,跳转到指定页面。
- 如果未重置,直接跳转到指定页面。
3 客户端实现
3.1 定义工具方法
src/plugins/api.ts
中定义所需工具方法generateRoutes(resData)
,具体实现如下:
/*构建路由*/
export function generateRoutes(source: any)
{
if(!Array.isArray(source))
return null;
return source.filter((item:any)=>item.path).map((item:any) => {
let route:any = {};
route.path = item.path;
let comDir = item.comDir === "/" ? item.comDir : item.comDir+"/";
/*动态导入组件时,需要如下写法,否则将报找不到对应组件错误*/
route.component = () => import(`../views${comDir}${item.comName}.vue`);
route.meta = {};
route.meta.title = item.comTitle;
route.meta.permission = item.permission;
route.meta.alias = item.alias;
route.meta.parentId = item.parentId;
return route;
})
}
3.2 前置路由导航守卫实现
src/router/index.js
中定义全局前置路由导航守卫,具体实现如下:
router.beforeEach(async (to, from, next) => {
/*用户访问登录页面*/
if (to.path === "/login") {
/**
* 用户登录成功后:
* 使用sessionStorage保存服务端返回的access_token与refresh_token。
* 当用户显示退出登录,则要清空sessionStorage;
* */
store.commit("setPermissions", undefined);
store.commit("setMenus", undefined);
sessionStorage.clear();
return next();
}
/**
* 用户访问其它页面:
* 首先检查当前用户是否登录,
* 如果登录,则sessionStorage中保存有token数据,
* 如果未登录,则没有。
* */
/*当sessionStorage中没有token数据,则用户未登录,页面强制跳转到登录页面。*/
if (!sessionStorage.getItem("access_token"))
return next("/login");
/*当sessionStorage中保存有token数据,则用户已登录, 则校验是否需要构建动态路由*/
if (!store.state.menus)
{
/*需要,动态构建路由,并跳转至待访问的页面*/
// @ts-ignore
let currentUser = JSON.parse(sessionStorage.getItem("login_user"));
store.commit("setPermissions", currentUser?.permissions);
const {data: {resCode, resData}} = await $axios.get(`/menu/currentmenus/${currentUser?.id}`);
if(2000 !== resCode)
{
notifyBox("系统初始化失败,请稍后重试!", "error");
return next("/login");
}
store.commit("setMenus", resData);
/*构建路由:*/
let dynamicRoutes = generateRoutes(resData);
let homeRoute = {
path: '/home',
name: 'home',
component: HomeView,
redirect: "/dashboard",
children: dynamicRoutes
}
let Route404 = {
path: "/:catchAll(.*)",
component: () => import("../views/xf_404.vue")
}
// @ts-ignore
router.addRoute(homeRoute);
router.addRoute(Route404);
// @ts-ignore
/*dynamicRoutes.forEach((item: any) => {
router.addRoute("home", item);
})*/
return next({...to, replace: true});
}
/*不需要,则直接跳转至待访问页面!*/
next();
});
3.3 实现注意事项
- 在路由导航守卫中,需要确保每次路由导航守卫只能执行一次
next()
方法,因此,如果路由导航守卫存在多种分支时,分支与分支之要么是互斥的,要么多个分支,一定要保证next()
执行且执行一次。 - 当重建路由后,页面跳转到待跳转页面,在"^4.X"版本vue-router必须使用以下方式调用next():
next({...to, replace: true});
- 对于404页面路由信息,在"^4.X"版本vue-router需要使用正则表达式进行匹配,实现方式之一:
path: "/:catchAll(.*)",
不能使用"/**"匹配。
4 服务端实现
4.1 创建服务端模块
4.1.1 创建xfsy-consumer模块
该模块为所有服务消费方模块的父模块,用于引入服务消费方所需的公共依赖,定义服务消费方所需类
4.1.1.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>xfsy-server</artifactId>
<groupId>org.wjk</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xfsy-consumer</artifactId>
<packaging>pom</packaging>
<modules>
<module>xfsy-service-manager</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.wjk</groupId>
<artifactId>xfsy-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
4.1.1.2 定义服务消费方资源服务器配置
用于完成SpringSecurity配置
- 具体实现
package org.wjk.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.wjk.handler.XfAccessDeniedHandler;
import org.wjk.handler.XfAuthenticationEntryPoint;
import org.wjk.properties.TokenBuilderProperties;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SvcsSecurityConfig extends ResourceServerConfigurerAdapter
{
private final TokenBuilderProperties properties;
private final XfAuthenticationEntryPoint entryPoint;
private final XfAccessDeniedHandler deniedHandler;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception
{
resources.resourceId(properties.getResourceId())
.authenticationEntryPoint(entryPoint)
.accessDeniedHandler(deniedHandler);
}
@Override
public void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
4.1.1.3 定义Feign请求拦截器
当消费方通过feign调用服务提供方时,OpenFeign组件将重新构建一个普通的http请求,该请求中,无access_token,导致服务提供方接收请求时,将返回503响应,导致请求失败。因此,在需要在OpenFeign组件构建的请求,添加access_token,而添加方式即通过Feign请求拦截完成,具体实现如下:
package org.wjk.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Configuration
@Slf4j
public class FeignRequestConfig
{
@Bean
public RequestInterceptor requestInterceptor()
{
return new RequestInterceptor()
{
@Override
public void apply(RequestTemplate requestTemplate)
{
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
String token = attributes.getRequest().getHeader("Authorization");
String systemType = attributes.getRequest().getHeader("system_type");
if(null != token)
{
requestTemplate.header("Authorization", token);
}
if(null != systemType)
requestTemplate.header("system_type", systemType);
}
};
}
}
4.1.2 创建xfsy-service-manager模块
该模块用于接收运营管理客户端发起的所有请求,并调用服务消费方完成数据库相关操作。
4.1.2.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>xfsy-consumer</artifactId>
<groupId>org.wjk</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xfsy-service-manager</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.wjk</groupId>
<artifactId>xfsy-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
4.1.2.2 bootstrap.yml
server:
port: 9010
spring:
application:
name: xfsy-service-manager
cloud:
nacos:
config:
server-addr: 49.233.38.67:8848
file-extension: yml
group: dev
namespace: 2934dee2-2954-4cb4-93db-0c0c8643f4b9
discovery:
server-addr: 49.233.38.67:8848
namespace: 2934dee2-2954-4cb4-93db-0c0c8643f4b9
group: dev
main:
banner-mode: off
4.1.2.3 配置中心的yml配置文件
- 配置文件信息如图:
- 具体配置信息
jedis:
max-idle: 33
max-total: 33
min-idle: 33
host: 49.233.38.67
timeout: 10000
password: 138496wr
thread:
core-size: 33
max-size: 33
keep-alive: 60
queue-capacity: 256
name-prefix: xfsy_svcs_mngr_
logging:
level:
org.wjk: debug
system:
token:
signer-key: xfsy-systems
token-type: xfsy
resource-id: xfsy-service-manager
4.2 xfsy-service-manager模块实现获取当前用户拥有操作权限菜单信息
4.2.1 定义接收请求的Controller方法
在
org.wjk.controller.SysMenuCtrllr
中定义接收请求方法
package org.wjk.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wjk.entity.pojo.SysMenuPojo;
import org.wjk.entity.vo.ResponseResult;
import org.wjk.service.SysMenuSvc;
import javax.validation.constraints.NotNull;
import java.util.List;
@RestController
@RequestMapping("/menu")
@RequiredArgsConstructor
@Slf4j
@Validated
public class SysMenuCtrllr
{
private final SysMenuSvc menuSvc;
@GetMapping("/currentmenus/{userId}")
public ResponseResult<List<SysMenuPojo>> getCurrentUserHasMenus(@PathVariable @NotNull Integer userId)
{
return ResponseResult.success("ok", menuSvc.getCurrentUserHasMenus(userId));
}
}
4.2.2 定义业务层处理请求的业务方法
4.2.2.1 定义业务层接口方法声明
package org.wjk.service;
import org.wjk.entity.pojo.SysMenuPojo;
import java.util.List;
public interface SysMenuSvc
{
List<SysMenuPojo> getCurrentUserHasMenus( Integer userId);
}
4.2.2.2 定义业务层实现类业务方法
package org.wjk.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.wjk.entity.pojo.SysMenuPojo;
import org.wjk.remote.SysMenuFeign;
import org.wjk.service.SysMenuSvc;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SysMenuSvcImpl implements SysMenuSvc
{
private final SysMenuFeign menuFeign;
@Override
public List<SysMenuPojo> getCurrentUserHasMenus(Integer userId)
{
return menuFeign.getCurrentUserHasMenus(userId);
}
}
4.2.3 定义远程调用Feign接口
package org.wjk.remote;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.wjk.entity.pojo.SysMenuPojo;
import java.util.List;
@FeignClient(name = "xfsy-prdc-system", contextId = "sysMenuFeign", path = "/menu")
public interface SysMenuFeign
{
@GetMapping("/currentmenus/{userId}")
List<SysMenuPojo> getCurrentUserHasMenus(@PathVariable("userId") Integer userId);
}
4.3 xfsy-prdc-system模块业务实现
4.3.1 定义接收Feign请求的Controller方法
package org.wjk.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.wjk.entity.pojo.SysMenuPojo;
import org.wjk.service.PrdcrSysMenuSvs;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/menu")
@RequiredArgsConstructor
@Validated
public class PrdcrSysMenuCtrllr
{
private final PrdcrSysMenuSvs menuSvs;
@GetMapping("/currentmenus/{userId}")
public List<SysMenuPojo> getCurrentUserHasMenus(@PathVariable @NotNull Integer userId)
{
return menuSvs.getCurrentUserHasMenus(userId);
}
}
4.3.2 定义业务层
4.3.2.1 定义业务层接口及方法声明
package org.wjk.service;
import org.wjk.entity.pojo.SysMenuPojo;
import java.util.List;
public interface PrdcrSysMenuSvs
{
List<SysMenuPojo> getCurrentUserHasMenus(Integer userId);
}
4.3.2.2 定义业务层实现类及方法实现
package org.wjk.service.impl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.wjk.entity.pojo.SysMenuPojo;
import org.wjk.exception.ExceptionSpec;
import org.wjk.exception.XfNonDbOperationException;
import org.wjk.mapper.PrdcrSysMenuMppr;
import org.wjk.service.PrdcrSysMenuSvs;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class PrdcrSysMenuSvsImpl implements PrdcrSysMenuSvs
{
private final PrdcrSysMenuMppr menuMppr;
@Override
public List<SysMenuPojo> getCurrentUserHasMenus(Integer userId)
{
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
Integer systemType = Optional.of(Integer.parseInt(attributes.getRequest().getHeader("system_type")))
.orElseThrow(()-> new XfNonDbOperationException(ExceptionSpec.SYSTEM_ERROR)) ;
return menuMppr.getCurrentUserHasMenus(userId, systemType);
}
}
4.3.3 持久层定义
package org.wjk.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.wjk.entity.pojo.SysMenuPojo;
import java.util.List;
@Mapper
public interface PrdcrSysMenuMppr extends BaseMapper<SysMenuPojo>
{
List<SysMenuPojo> getCurrentUserHasMenus(@Param("userId") Integer userId, @Param("systemType") Integer systemType);
}
4.3.4 SQL映射文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.wjk.mapper.PrdcrSysMenuMppr">
<!--查询语句的书写顺序:[on Expr][where Expr][group by][having][order by][limit]-->
<select id="getCurrentUserHasMenus" resultType="org.wjk.entity.pojo.SysMenuPojo">
select sm.id, sm.name, sm.alias, sm.icon_class, sm.type, sm.parent_id, sm.enabled, sm.sort, sm.permission, sm.path, sm.com_dir,
sm.com_name, sm.com_title
from sys_menu sm left join sys_menu_pstn smp on sm.id=smp.menu_id left join hum_emp_pstn hep on smp.pstn_id=hep.pstn_id
left join hum_emp he on he.id=hep.emp_id left join sys_user su on he.user_id=su.id
where su.id=#{userId} and sm.apply_sys=#{systemType} and sm.enabled=1 order by sm.sort
</select>
</mapper>