SpringBoot整合SpringSecurity实现权限控制(三):前端动态装载路由与菜单(附源码)

x33g5p2x  于2021-09-24 转载在 Spring  
字(6.7k)|赞(0)|评价(0)|浏览(657)

一、前言

在上篇文章中我们通过RBAC( Role-Based Access Control:基于角色的访问控制策略)进行权限模型设计,并设计了以下表:

  • 本章中前端将通过访问后端接口,拉取用户对应的权限数据,实现动态装载路由。

二、数据准备

  • 由于前端还没有编写用户、角色、菜单、权限的管理界面,我们将直接在后台对应的表结构中模拟数据。

用户表:增加一个管理员

角色表:增加一个系统管理员角色

用户角色表:将用户映射成系统管理员角色

菜单表:增加一个主菜单:系统管理;两个子菜单:用户管理与角色管理

角色权限表:给管理员角色分配相应的菜单功能权限

  • 给以上这些表增加相应数据后,我们可以得到用户最终的权限如下:

三、编写后端接口

  • 接下来编写一个后端接口,通过查询这个接口,可以获取对应用户的权限信息。
  1. 我们先新建一个包含用户权限信息的数据传输对象(DTO)
/** * 用户权限 * * @author zhuhuix * @date 2021-08-31 */
@ApiModel(value = "用户权限信息")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PermissionDto {

    @ApiModelProperty(value = "id")
    private Long id;

    @ApiModelProperty(value = "路由")
    private String path;

    @ApiModelProperty(value = "名称")
    private String name;

    @ApiModelProperty(value = "组件路径")
    private String component;

    @ApiModelProperty(value = "是否隐藏")
    private Boolean hidden;

    @ApiModelProperty(value = "图标")
    private String icon;

    @ApiModelProperty(value = "是否缓存")
    private Boolean cache;

    @ApiModelProperty(value = "重定向")
    private String redirect;

    @ApiModelProperty(value = "父id")
    private Long pId;

}
  1. 接下通过Mybastis-plus自定义查询创建一个查询用户权限的DAO接口
/** * 用户DAO接口 * * @author zhuhuix * @date 2021-07-19 */
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {

   /** * 查询用户权限 * @param userId 用户id * @return 权限信息 */
    @Select("SELECT m.id, m.`path`, m.`name`, m.`component`, m.`icon`, m.`cache`, m.`hidden`, m.`redirect`, m.p_id " +
            "FROM " +
            "sys_menu m " +
            "INNER JOIN sys_permission p ON p.menu_id = m.id " +
            "INNER JOIN sys_user_role ur ON ur.role_id = p.role_id " +
            "INNER JOIN sys_user u ON u.id = ur.user_id " +
            "INNER JOIN sys_role r ON r.id = ur.role_id where ur.user_id=#{userId}"+
            " and m.enabled=1 " +
            " order by m.sort "
    )
    List<PermissionDto> selectUserPermission(Long userId);
 }
  1. 在用户信息业务服务层中加入查询用户权限信息的业务逻辑
/** * 用户信息接口 * * @author zhuhuix * @date 2020-04-03 */
public interface SysUserService {

    ...

    /** * 获取用户权限信息 * @param userId 用户id * @return 权限信息 */
    List<PermissionDto> getUserPermission(Long userId);

}
/** * 用户接口实现类 * * @author zhuhuix * @date 2020-04-03 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class SysUserServiceImpl implements SysUserService {

    private final SysUserMapper sysUserMapper;
    private final UploadFileTool uploadFileTool;

    ...

    @Override
    public List<PermissionDto> getUserPermission(Long userId) {
        return sysUserMapper.selectUserPermission(userId);
    }

   
}
  1. 在Controller层加入Api接口
/** * api用户信息 * * @author zhuhuix * @date 2021-08-16 */
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/api/user")
@Api(tags = "用户信息接口")
public class SysUserController {

    private final SysUserService sysUserService;

	...

    @ApiOperation("获取用户权限")
    @GetMapping("/permission/{userId}")
    public ResponseEntity<Object> getUserPermission(@PathVariable Long userId) {
        return ResponseEntity.ok(sysUserService.getUserPermission(userId));
    }

}
  1. 通过Swagger接口工具进行测试

  1. 填入用户userId

  1. 后台返回结果

四、前端实现动态装载

  1. 添加前端访问后台接口获取用户权限的Api接口:
import request from '@/utils/request'
...
// 获取用户权限
export function getUserPermission(userId) {
  return request({
    url: '/api/user/permission/' + userId,
    method: 'get'
  })
}
  1. 编写一个读取后台Api接口获取用户权限信息的通用模块
  • /src/store/modules/permission.js
  • 以下代码中主要应用到了两个技术手段:数组快速转树型结构和遍历后台传来的路由字符串,转换为组件对象
import { constantRoutes, router } from '@/router'
import { getUserPermission } from '@/api/user'
import store from '@/store'
import Layout from '@/layout/index'
// 处理路由
export const filterAsyncRouter = (routers) => { // 遍历后台传来的路由字符串,转换为组件对象
  return routers.filter(router => {
    if (router.component) {
      if (router.component === 'Layout') { // Layout组件特殊处理
        router.component = Layout
      } else {
        const component = router.component
        router.component = loadView(component)
      }
    }
    router.meta = { title: router.name, icon: router.icon, noCache: !router.cache }
    if (router.children && router.children.length) {
      router.children = filterAsyncRouter(router.children)
    }
    return true
  })
}

export const loadView = (view) => {
  return (resolve) => require([`@/views/${view}`], resolve)
}

const state = {
  routes: [],
  addRoutes: [],
  menuLoaded: false
}

const mutations = {
  SET_ROUTES: (state, routes) => {
    state.addRoutes = routes
    state.routes = constantRoutes.concat(routes)
    console.log('routes', state.routes)
    // 增加动态路由
    router.addRoutes(routes)
  },
  SET_MENULOADED: (state, menuLoaded) => {
    state.menuLoaded = menuLoaded
  }
}

const actions = {
  generateRoutes({ commit }) {
    return new Promise(resolve => {
      let accessedRoutes
      getUserPermission(store.getters.user.id).then(res => {
        accessedRoutes = ArrayToTreeData(res)
        if (accessedRoutes && accessedRoutes.length) {
          const asyncRouter = filterAsyncRouter(accessedRoutes)
          asyncRouter.push({ path: '*', redirect: '/404', hidden: true })
          commit('SET_ROUTES', asyncRouter)
          commit('SET_MENULOADED', true)
          resolve(asyncRouter)
        }
      })
    })
  },

  setMenuLoaded({ commit }, munuLoaded) {
    return new Promise(resolve => {
      commit('SET_MENULOADED', munuLoaded)
    })
  }
}

function ArrayToTreeData(data) {
  const cloneData = JSON.parse(JSON.stringify(data)) // 对源数据深度克隆
  return cloneData.filter(father => {
    const branchArr = cloneData.filter(child => father.id === child.pid) // 返回每一项的子级数组
    branchArr.length > 0 ? father.children = branchArr : '' // 如果存在子级,则给父级添加一个children属性,并赋值
    return father.pid === null // 返回第一层
  })
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}
  1. 改写导航守卫,增加动态装载路由的功能:
  • /src/permission.js
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'

NProgress.configure({ showSpinner: false }) // NProgress Configuration

const whiteList = ['/login', '/register'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()

  if (hasToken) {
    if (to.path === '/login') {
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done()
    } else {
      if (store.getters.roles.length === 0) {
        try {
          // 首次登录需要获取用户信息
          store.dispatch('getInfo').then(() => {
            // 根据用户信息获取用户权限并动态加载菜单
            store.dispatch('permission/generateRoutes').then(() => next({ ...to, replace: true }))
          })
        } catch (error) {
          store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      } else {
        // 如果菜单未装载过,则需要重新
        if (!store.getters.menuLoaded) {
          store.dispatch('permission/generateRoutes').then(() => next())
        } else {
          next()
        }
      }
    }
  } else {
    /* has no token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

五、效果演示

六、源码

相关文章