系列文章目录
《SpringBoot整合SpringSecurity实现权限控制(一):实现原理》
《SpringBoot整合SpringSecurity实现权限控制(二):权限数据基本模型设计》
《SpringBoot整合SpringSecurity实现权限控制(三):前端动态装载路由与菜单》
《SpringBoot整合SpringSecurity实现权限控制(四):角色管理》
《SpringBoot整合SpringSecurity实现权限控制(五):用户管理》
《SpringBoot整合SpringSecurity实现权限控制(六):菜单管理》
/** * 权限表 * * @author zhuhuix * @date 2021-10-26 */
@ApiModel(value = "权限信息")
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_permission")
public class SysPermission {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
// 角色id
private Long roleId;
// 菜单id
private Long menuId;
private Timestamp createTime;
public SysPermission(Long roleId, Long menuId, Timestamp createTime) {
this.roleId = roleId;
this.menuId = menuId;
this.createTime = createTime;
}
}
/** * 权限表DAO接口 * * @author zhuhuix * @date 2021-10-26 */
@Mapper
public interface SysPermissionMapper extends BaseMapper<SysPermission> {
// BaseMapper接口已经默认实现了基本的增删改查操作
}
/** * 角色信息接口 * * @author zhuhuix * @date 2021-09-13 * @date 2021-10-26 增加getPermission,savePermission */
public interface SysRoleService {
....
/** * 获取角色权限 * * @param roleId 角色id * @return 角色权限列表 */
List<SysPermission> getPermission(Long roleId);
/** * 保存角色权限 * * @param roleId 角色id * @param menus 权限表 * @return 是否成功 */
Boolean savePermission(Long roleId,Set<Long> menus);
}
/** * 角色实现类 * * @author zhuhuix * @date 2021-09-13 * @date 2021-10-26 实现getPermission,savePermission接口 */
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(propagation = Propagation.SUPPORTS, readOnly = true, rollbackFor = Exception.class)
public class SysRoleServiceImpl implements SysRoleService {
private final SysRoleMapper sysRoleMapper;
private final SysPermissionMapper sysPermissionMapper;
...
@Override
public List<SysPermission> getPermission(Long roleId) {
QueryWrapper<SysPermission> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(SysPermission::getRoleId, roleId);
return sysPermissionMapper.selectList(queryWrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean savePermission(Long roleId, Set<Long> menus) {
// 先根据roleId删除原有权限
QueryWrapper<SysPermission> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(SysPermission::getRoleId, roleId);
sysPermissionMapper.delete(queryWrapper);
// 再插入roleId新权限
for (Long menu : menus) {
int rowCount = sysPermissionMapper.insert(
new SysPermission(roleId, menu, Timestamp.valueOf(LocalDateTime.now())));
if (rowCount <= 0) {
throw new RuntimeException("保存权限失败");
}
}
return true;
}
}
/** * 角色信息api * * @author zhuhuix * @date 2021-09-13 * @date 2021-10-26 增加getPermission,savePermission API接口 */
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/api/role")
@Api(tags = "角色信息接口")
public class SysRoleController {
private final SysRoleService sysRoleService;
....
@ApiOperation("获取角色权限信息")
@GetMapping("{roleId}/permission")
public ResponseEntity<Object> getPermission(@PathVariable Long roleId) {
return ResponseEntity.ok(sysRoleService.getPermission(roleId));
}
@ApiOperation("保存角色权限信息")
@PostMapping("{roleId}/permission")
public ResponseEntity<Object> savePermission(@PathVariable Long roleId,@RequestBody Set<Long> menus) {
return ResponseEntity.ok(sysRoleService.savePermission(roleId,menus));
}
}
/** * 角色访问后端api * 2021-10-26 添加getPermission,savePermission */
import request from '@/utils/request'
...
export function getPermission(roleId) {
return request({
url: '/api/role/' + roleId + '/permission',
method: 'get'
})
}
export function savePermission(roleId, data) {
return request({
url: '/api/role/' + roleId + '/permission',
method: 'post',
data
})
}
/** * role/index.vue * 2021-10-27 增加角色权限分配操作页面 */
<template>
<div class="app-container">
<!--工具栏-->
<div class="head-container">
<!-- 搜索 -->
<el-input
v-model="roleName"
size="small"
clearable
placeholder="输入角色名称搜索"
style="width: 200px"
class="filter-item"
@keyup.enter.native="doQuery"
/>
<el-date-picker
v-model="createTime"
:default-time="['00:00:00', '23:59:59']"
type="daterange"
range-separator=":"
size="small"
class="date-item"
value-format="yyyy-MM-dd HH:mm:ss"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
<el-button
class="filter-item"
size="mini"
type="success"
icon="el-icon-search"
@click="doQuery"
>搜索</el-button>
<el-button
class="filter-item"
size="mini"
type="primary"
icon="el-icon-circle-plus-outline"
@click="doAdd"
>新增</el-button>
</div>
<!-- 表单渲染 -->
<el-dialog
append-to-body
:close-on-click-modal="false"
:before-close="doBeforeClose"
:visible.sync="showDialog"
width="520px"
>
<el-form
ref="form"
:inline="true"
:model="form"
:rules="rules"
size="small"
label-width="80px"
>
<el-form-item label="角色编码" prop="roleCode">
<el-input v-model="form.roleCode" style="width: 380px" />
</el-form-item>
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" style="width: 380px" />
</el-form-item>
<el-form-item label="描述信息" prop="description">
<el-input
v-model="form.description"
style="width: 380px"
rows="5"
type="textarea"
/>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="text" @click="doCancel">取消</el-button>
<el-button
:loading="formLoading"
type="primary"
@click="doSubmit(form)"
>确认</el-button>
</div>
</el-dialog>
<el-row>
<el-col
v-for="item in roles"
:key="item.id"
:span="5"
style="margin-bottom: 10px"
:offset="1"
>
<el-card>
<div slot="header" class="clearfix">
<i class="el-icon-user" /><span style="margin-left: 5px">{{ item.roleName }}</span>
<div style="display: inline-block; float: right; cursor: pointer" @click="doEdit(item.id)">
<el-tooltip effect="dark" content="编辑角色" placement="top">
<i class="el-icon-edit-outline" style="margin-left: 15px" />
</el-tooltip>
</div>
</div>
<div>
<ul class="role-info">
<li>
<div class="role-left">描述信息:{{ item.description }}</div>
</li>
<li>
<div class="role-left">
创建时间:{{ parseTime(item.createTime) }}
</div>
</li>
</ul>
</div>
<div style="display: inline-block; float: left; cursor: pointer" @click="doAssignPemission(item.id,item.roleName)">
<el-tooltip effect="dark" content="权限分配" placement="bottom">
<i class="el-icon-menu" />
</el-tooltip>
</div>
<div style="display: inline-block; float: right; cursor: pointer" @click="doDelete(item.id)">
<el-tooltip effect="dark" content="删除角色" placement="bottom">
<i class="el-icon-delete" style="margin-left: 15px" />
</el-tooltip>
</div>
</el-card>
</el-col>
</el-row>
<!-- 分配权限表单 -->
<el-dialog
append-to-body
:close-on-click-modal="false"
:visible.sync="showPermissionDialog"
:title="permission.roleName"
width="520px"
>
<treeselect
v-model="permission.menus"
:options="menuTree"
:show-count="true"
style="width: 480px"
:multiple="true"
:sort-value-by="sortValueBy"
:value-consists-of="valueConsistsOf"
:default-expand-level="1"
placeholder="请选择或搜索菜单进行权限分配"
/>
<div slot="footer" class="dialog-footer">
<el-button type="text" @click="doPemissionCancel">取消</el-button>
<el-button
type="primary"
@click="doSubmitPemission(permission)"
>确认</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { parseTime } from '@/utils/index'
import { getRoleList, getRole, saveRole, deleteRole, getPermission, savePermission } from '@/api/role'
import { getMenuList } from '@/api/menu'
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
name: 'Role',
components: { Treeselect },
data() {
return {
showDialog: false,
loading: false,
formLoading: true,
form: {},
roles: [],
roleName: '',
createTime: null,
rules: {
roleCode: [
{ required: true, message: '请输入角色编码', trigger: 'blur' }
],
roleName: [
{ required: true, message: '请输入角色名称', trigger: 'blur' }
]
},
showPermissionDialog: false,
permission: {},
menuTree: [],
valueConsistsOf: 'ALL_WITH_INDETERMINATE',
sortValueBy: 'INDEX'
}
},
created() {
},
methods: {
parseTime,
doQuery() {
var param = { roleName: this.roleName }
if (this.createTime != null) {
param.createTimeStart = Date.parse(this.createTime[0])
param.createTimeEnd = Date.parse(this.createTime[1])
}
getRoleList(param).then(res => {
if (res) {
this.roles = res
}
})
},
doAdd() {
this.showDialog = true
this.formLoading = false
this.form = {}
},
doEdit(id) {
this.showDialog = true
getRole(id).then(res => {
if (res) {
this.form = res
this.formLoading = false
}
})
},
doCancel() {
this.showDialog = false
this.formLoading = true
this.form = {}
},
doSubmit(role) {
this.$refs.form.validate(valid => {
if (valid) {
this.formLoading = true
saveRole(role).then(res => {
if (res) {
this.showDialog = false
this.$notify({
title: '保存成功',
type: 'success',
duration: 2500
})
this.doQuery()
}
}).catch(() => {
this.formLoading = false
})
}
})
},
doDelete(id) {
this.$confirm(`确认删除此条数据?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() =>
deleteRole([id]).then(res => {
if (res) {
this.$notify({
title: '删除成功',
type: 'success',
duration: 2500
})
this.doQuery()
}
})
).catch(() => {
})
},
doBeforeClose() {
this.showDialog = true
},
doAssignPemission(roleId, roleName) {
var param = { name: '' }
getMenuList(param).then(res => {
if (res) {
this.menuTree = this.ArrayToTreeData(res)
getPermission(roleId).then(res => {
if (res) {
const menus = []
res.forEach(element => {
menus.push(element.menuId)
})
this.permission = { roleId: roleId, roleName: roleName, menus: menus }
this.showPermissionDialog = true
}
})
}
})
},
doPemissionCancel() {
this.showPermissionDialog = false
this.permission = {}
},
doSubmitPemission(permission) {
console.log(permission)
savePermission(permission.roleId, permission.menus).then(res => {
if (res) {
this.showPermissionDialog = false
this.$notify({
title: '配置权限成功',
type: 'success',
duration: 2500
})
}
})
},
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属性,并赋值
const parentArr = cloneData.filter(parent => parent.id === father.pid) // 判断该菜单的父级菜单是否存在
if (parentArr.length === 0) { return father } // 如果该菜单的父级菜单不存在,则直接返回该菜单
return father.pid === null // 返回第一层
})
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss">
.role-span {
font-weight: bold;
color: #303133;
font-size: 15px;
}
.role-info {
margin-top: 0;
padding-top: 0;
padding-left: 0;
list-style: none;
li {
border-bottom: 1px solid #f0f3f4;
padding: 11px 0;
font-size: 12px;
}
.role-left {
color: rgb(148, 137, 137);
overflow: hidden;
white-space: nowrap;
text-align: left;
text-overflow: ellipsis;
}
.line{
width: 100%;
height: 1px;
border-top: 1px solid #ccc;
}
}
</style>
<style rel="stylesheet/scss" lang="scss" scoped>
::v-deep .el-input-number .el-input__inner {
text-align: left;
}
::v-deep .vue-treeselect__multi-value {
margin-bottom: 0;
}
::v-deep .vue-treeselect__multi-value-item {
border: 0;
padding: 0;
}
</style>
/** * 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' && store.getters.user.userName !== undefined) {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.user.userName === undefined) {
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()
})
/** * 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('addRoutes routes', state.routes)
// 增加动态路由
router.addRoutes(routes)
},
SET_MENULOADED: (state, menuLoaded) => {
state.menuLoaded = menuLoaded
// console.log('menuLoaded', state.menuLoaded)
}
}
const actions = {
generateRoutes({ commit }) {
return new Promise(resolve => {
let accessedRoutes
getUserPermission(store.getters.user.id).then(res => {
console.log('res', res)
accessedRoutes = ArrayToTreeData(res)
console.log('accessedRoutes', accessedRoutes)
let asyncRouter = []
if (accessedRoutes && accessedRoutes.length) {
asyncRouter = filterAsyncRouter(accessedRoutes)
console.log('asyncRouter', asyncRouter)
}
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
}
/** * 侧边栏 * Siderbar/index.vue */
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
/** * 菜单组件 * Siderbar/SiderbarItem.vue */
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
// zhuhuix 2021-10-27去除 父菜单下只有一个子菜单,也显示父菜单;(以下注释取消的效果是父菜单下只有一个子菜单,不显示父菜单,直接显示子菜单)
// if (showingChildren.length === 1) {
// return true
// }
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://zhuhuix.blog.csdn.net/article/details/120995403
内容来源于网络,如有侵权,请联系作者删除!