0 引言
这篇文章自己准备了好几个周末,如果不是中间踩了太多的坑的话上上的周末就应该发表了,实在是因为踩坑太多而自己也比较执拗,坚持要写出一篇解决掉遇到的99%以上的Bug,能经得起读者实践验证的项目实战文章,拖到今天才发布。笔者一直坚持文章质量重于数量,内容足够好的文章才会让更多的读者传阅。
这篇文章前端以开源项目vue-element-admin基础,后端以Vblog项目中后端项目blogserver为基础。为啥前端没用Vblog项目中的vueblog前端项目?因为vueblog项目中的很多组件没有,包括vuex, 还有很多组件版本过低,笔者当时安装完各种需要的依赖包之后发现项目都启动不了,还一直报错,短时间之内根本无法解决。而我之前有克隆过vue-element-admin项目的源码,里面大部分需要的前端组件和依赖包都有,最重要的是里面有mock模拟后台数据实现的用户登录和动态加载路由资源和初始化基于角色控制的菜单列表的实现。我们只需要在这个项目的基础上进行业务需求的修改即可,下面开始呈上笔者的代码实现。
废话不多说,下面开始呈上内容干货!
1 后端代码修改
1.1 用户增加当前角色
public class User implements UserDetails {
// 当前角色
private Role currentRole;
public Role getCurrentRole() {
return currentRole;
}
public void setCurrentRole(Role currentRole) {
this.currentRole = currentRole;
}
// 其他代码省略
}
1.2 UserService#loadUserByUsername方法修改
UserService.java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
//避免返回null,这里返回一个不含有任何值的User对象,在后期的密码比对过程中一样会验证失败
return new User();
}
//查询用户的角色信息,并返回存入user中
List<Role> roles = rolesMapper.getRolesByUid(user.getId());
// 权限大的角色排在前面
roles.sort(Comparator.comparing(Role::getId));
// 下面两行代码为新增设置当前角色代码
user.setRoles(roles);
user.setCurrentRole(roles.get(0));
return user;
}
1.3 新增根据角色ID查询路由ID集合接口
RouterResourceController.java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null) {
//避免返回null,这里返回一个不含有任何值的User对象,在后期的密码比对过程中一样会验证失败
return new User();
}
//查询用户的角色信息,并返回存入user中
List<Role> roles = rolesMapper.getRolesByUid(user.getId());
// 权限大的角色排在前面
roles.sort(Comparator.comparing(Role::getId));
// 下面两行代码为新增设置当前角色代码
user.setRoles(roles);
user.setCurrentRole(roles.get(0));
return user;
}
RoleRouterService.java
public List<String> queryCurrentRoleResourceIds(Integer roleId){
List<Integer> resourceIds = roleRouterMapper.queryRouteResourceIdsByRoleId(roleId);
List<String> resultList = new ArrayList<>();
for(Integer resourceId: resourceIds){
resultList.add(String.valueOf(resourceId));
}
return resultList;
}
RoleRouterMapper.java
List<Integer> queryRouteResourceIdsByRoleId(Integer roleId);
RolesMapper.xml
<select id="queryRouteResourceIdsByRoleId" parameterType="Integer" resultType="Integer">
select resource_id from role_resources
where role_id=#{roleId,jdbcType=INTEGER}
</select>
1.4 WebSecurityConfig 类中增加跨域配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置跨域
http.cors().configurationSource(corsConfigurationSource());
// 本方法其他代码省略,已传至个人gitee代码仓库,感兴趣的小伙伴可以克隆下来查看
}
//配置跨域访问资源
private CorsConfigurationSource corsConfigurationSource() {
CorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("http://localhost:3000"); //同源配置,*表示任何请求都视为同源,若需指定ip和端口可以改为如“localhost:8080”,多个以“,”分隔;
corsConfiguration.addAllowedHeader("*");//header,允许哪些header,本案中使用的是token,此处可将*替换为token;
corsConfiguration.addAllowedMethod("*"); //允许的请求方法,PSOT、GET等
corsConfiguration.setAllowCredentials(true); // 允许cookie认证
((UrlBasedCorsConfigurationSource) source).registerCorsConfiguration("/**",corsConfiguration); //配置允许跨域访问的url
return source;
}
1.5 数据准备
(1)参照vue-element-admin
项目src/router
目录下index文件中的动态路由数据执行blogserver
项目下src/main/resources
目录下router_resource_data.sql
脚本文件中的sql
脚本为路由资源表中添加vue-element-admin
项目中的动态菜单路由资源。
(2)执行blogserver
项目下src/main/resources
目录下role_resources_data.sql
给角色admin
角色分配路由资源
(3)启动后台服务后通过postman的注册接口三个用户(由于用户数据入库时对用户登录密码进行了加密处理,因此不好执行sql添加,而用户注册的逻辑中恰好使用spring-security对用户登录密码进行了加密处理)
post http://localhost:8081/blog/user/reg
{
"username": "sang",
"nickname":"江南一点雨",
"password": "sang123",
"email": "sang2021@163.com"
}
在请求体中以此换成下面的数据完成用户注册
{
"username": "zhangsan",
"nickname":"张三",
"password": "zhangsan123",
"email": "zhangsan2021@163.com"
}
{
"username": "heshengfu",
"nickname":"程序员阿福",
"password": "heshengfu123",
"email": "heshengfu2018@163.com"
}
本文后端代码已上传到笔者的gitee后端代码仓库地址:gitee.com/heshengfu12…
2.2 修改src/utils/request.js
import axios from 'axios'
// import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'
axios.defaults.withCredentials = true
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
if (store.getters.token) {
// let each request carry token
// ['X-Token'] is a custom headers key
// please modify it according to the actual situation
config.headers['X-Token'] = getToken()
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
/**
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 20000) {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
) */
export default service
这里需要注释对接口相应体的拦截,因为我们后台借口出参的状态码成功时并不是2000
2.3 修改src/api/user.js
和src/api/role.js
两个文件
(1) user.js
// 修改登录接口函数
export function login(data) {
return request({
url: '/user/login',
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data,
transformRequest: [function(data) {
// Do whatever you want to transform the data
let ret = ''
for (const it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}]
})
}
注意:所有post请求类型的api
必须加上以上headers
和 transformRequest
,尤其是对入参的处理回调函数transformRequest
,不加上的化登录的时候后台拿到的用户名一直为空字符串,用户认证无法通过。
(2) role.js
role.js
文件中增加根据角色ID查询动态路由集合的接口函数
export function getRouteIds(roleId) {
return request({
url: `/routerResource/currentRoleResourceIds?roleId=${roleId}`,
method: 'get',
headers: {
'Content-Type': 'application/json'
}
})
}
2.3 修改src/store/modules/user.js
文件
import { login, logout } from '@/api/user'
import { getRouteIds, getRoutes } from '@/api/role'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
import { Message } from 'element-ui'
const state = {
token: getToken(),
userBase: null,
name: '',
avatar: '',
// introduction: '',
roles: [],
currentRole: null
}
const mutations = {
SET_TOKEN: (state, token) => {
state.token = token
},
// SET_INTRODUCTION: (state, introduction) => {
// state.introduction = introduction
// },
SET_NAME: (state, name) => {
state.name = name
},
SET_USER_BASE: (state, userBase) => {
state.userBase = userBase
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_CURRENT_ROLE: (state, currentRole) => {
state.currentRole = currentRole
}
}
const actions = {
// user login
login({ commit }, userInfo) {
const { username, password } = userInfo
return new Promise((resolve, reject) => {
login({ username: username, password: password }).then(response => {
if (response.status === 200 && response.data) {
const data = response.data.userInfo
const useBaseInfo = {
username: data.username,
nickname: data.nickname,
email: data.email
}
window.sessionStorage.setItem('userInfo', JSON.stringify(useBaseInfo))
const { roles, currentRole } = data
commit('SET_TOKEN', useBaseInfo)
commit('SET_NAME', useBaseInfo.username)
setToken(currentRole.id)
commit('SET_ROLES', roles)
window.sessionStorage.setItem('roles', JSON.stringify(roles))
commit('SET_CURRENT_ROLE', currentRole)
window.sessionStorage.setItem('currentRole', currentRole)
const avtar = '@/assets/avtars/avtar1.jpg'
commit('SET_AVATAR', avtar)
getRouteIds(currentRole.id).then(response => {
if (response.status === 200 && response.data.status === 200) {
const routeIds = response.data['data']
window.sessionStorage.setItem('routeData', JSON.stringify(routeIds))
} else {
Message.error('response.status=' + response.status + 'response.text=' + response.text)
}
})
resolve(useBaseInfo)
} else {
Message.error('user login failed')
resolve()
}
}).catch(error => {
console.error(error)
reject(error)
})
})
},
// 获取用户信息,已弃用
/**
getInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getInfo(state.token).then(response => {
const { data } = response
if (!data) {
reject('Verification failed, please Login again.')
}
const { roles, name, avatar, introduction } = data
// roles must be a non-empty array
if (!roles || roles.length <= 0) {
reject('getInfo: roles must be a non-null array!')
}
commit('SET_ROLES', roles)
commit('SET_NAME', name)
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(data)
}).catch(error => {
reject(error)
})
})
}, */
// 用户登出
logout({ commit, state, dispatch }) {
return new Promise((resolve, reject) => {
logout(state.token).then(() => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
commit('SET_NAME', '')
commit('SET_CURRENT_ROLE', null)
window.sessionStorage.removeItem('userInfo')
window.sessionStorage.removeItem('routeIds')
window.sessionStorage.removeItem('roles')
window.sessionStorage.removeItem('currentRole')
removeToken()
resetRouter()
// reset visited views and cached views
// to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
dispatch('tagsView/delAllViews', null, { root: true })
resolve()
}).catch(error => {
reject(error)
})
})
},
// remove token
resetToken({ commit }) {
return new Promise(resolve => {
commit('SET_TOKEN', '')
commit('SET_ROLES', [])
removeToken()
resolve()
})
},
// 切换角色
changeRoles({ dispatch }, roleId) {
return new Promise(async resolve => {
resetRouter()
// generate accessible routes map based on roles
getRoutes(roleId).then(response => {
if (response.status === 200 && response.data.status === 200) {
const dynamicRouteData = response.data['data']
window.sessionStorage.setItem('routeData', JSON.stringify(dynamicRouteData))
// dynamically add accessible routes
dispatch('permission/generateRoutes', dynamicRouteData)
// reset visited views and cached views
dispatch('tagsView/delAllViews', null)
} else {
Message.error('response.status=' + response.status + 'response.text=' + response.text)
}
})
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
vuex
存储全局共享数据需要结合sessionStorage
一起使用
2.4 修改src/store/modules/permission.js
文件
import { constantRoutes } from '@/router'
// import { getRoutes } from '@/api/role'
import { Message } from 'element-ui'
import { allRouteComponentMap, asyncRoutes } from '@/router/index'
/**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
/**
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
}*/
/**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
/**
export function filterAsyncRoutes(routes, roles) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)
}
res.push(tmp)
}
})
return res
}*/
/**
* 后台路由数据转路由组件数组(这是之前的方案转换路由数据为路由组件的函数,最后的方案没用)
* @param routes
* @returns {Array}
*/
export function transferDynamicRoutes(routes) {
const routeVos = []
if (!routes || routes.length === 0) return routeVos
const length = routes.length
for (let i = 0; i < length; i++) {
const item = routes[i]
let routeComponent
if (item.componentUrl) {
if (allRouteComponentMap[item.componentUrl]) {
routeComponent = allRouteComponentMap[item.componentUrl]
} else {
routeComponent = allRouteComponentMap['@/views/error-page/404']
}
} else {
routeComponent = null
}
const routeVo = { id: item.id, path: item.path, redirect: item.redirect ? item.redirect : 'noRedirect',
name: item.name, alwaysShow: item.name === 'Permission',
hidden: item.hidden,
meta: { title: item.title,
icon: item.icon,
noCache: true
},
children: [],
component: routeComponent
}
if (item.children.length > 0) {
routeVo.children = transferDynamicRoutes(item.children)
}
routeVos.push(routeVo)
}
return routeVos
}
const state = {
routes: [],
addRoutes: []
}
const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
}
}
const actions = {
generateRoutes({ commit }, routeIds) {
const routeIdMap = {}
for (let i = 0; i < routeIds.length; i++) {
routeIdMap[routeIds[i]] = routeIds[i]
}
return new Promise((resolve) => {
if (routeIds && routeIds.length > 0) {
const dynamicRoutes = filterPermissionRoutes(routeIdMap, asyncRoutes)
commit('SET_ROUTES', dynamicRoutes)
resolve(dynamicRoutes)
} else {
// throw new Error('transferDynamicRoutes error')
Message.error('transferDynamicRoutes error')
resolve([])
}
})
}
}
/**
* 后台返回组装的routeIdMap获取过滤后的动态路由集合
* @param {Object} routeIdMap
* @param {Array} dynamicRoutes
* @returns permissionRoutes
*/
export function filterPermissionRoutes(routeIdMap, dynamicRoutes) {
const permissionRoutes = []
for (let i = 0; i < dynamicRoutes.length; i++) {
const routeItem = dynamicRoutes[i]
if (routeIdMap[routeItem.id]) {
const permissionRouteItem = {
id: routeItem.id,
path: routeItem.path,
name: routeItem.name,
alwaysShow: routeItem.alwaysShow != null && routeItem.alwaysShow,
redirect: routeItem.redirect,
meta: routeItem.meta,
hidden: routeItem.hidden != null && routeItem.hidden,
component: routeItem.component,
children: []
}
permissionRoutes.push(permissionRouteItem)
if (routeItem.children && routeItem.children.length > 0) {
permissionRouteItem.children = filterPermissionRoutes(routeIdMap, routeItem.children)
}
}
}
return permissionRoutes
}
export default {
namespaced: true,
state,
mutations,
actions
}
2.5 修改 src/store/getter.js
文件
const getters = {
sidebar: state => state.app.sidebar,
size: state => state.app.size,
device: state => state.app.device,
visitedViews: state => state.tagsView.visitedViews,
cachedViews: state => state.tagsView.cachedViews,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
// introduction: state => state.user.introduction,
roles: state => state.user.roles,
permission_routes: state => state.permission.routes,
dynamicRoutes: state => state.permission.addRoutes,
errorLogs: state => state.errorLog.logs
}
export default getters
2.6 修改src/permission.js
文件
import router from './router'
import { constantRoutes } from './router'
import store from './store'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import { getToken, removeToken } from '@/utils/auth' // get token from cookie
import getPageTitle from '@/utils/get-page-title'
import { getRouteIds } from '@/api/role'
import { Message } from 'element-ui'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/auth-redirect'] // 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 permissionRoutes = store.getters.permission_routes
const dynamicRoutes = store.getters.dynamicRoutes
const roleId = getToken()
if (!permissionRoutes || permissionRoutes.length === 0) {
// 如果固定路由还没有添加到路由对象中则先添加固定路由列表
router.addRoutes(constantRoutes)
}
if (dynamicRoutes && dynamicRoutes.length > 0) {
// 用户已登录,如果是继续进入登录页面则直接进入首页
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
next()
}
} else {
/* 动态路由列表尚未添加到路由对象中*/
if (whiteList.indexOf(to.path) !== -1) {
// 白名单路由直接进入
next()
} else {
// 用户已登录
if (roleId) {
// 判断sessionStorage是否已保存路由数据
const routeIdsJson = window.sessionStorage.getItem('routeIds')
if (routeIdsJson) {
const routeIds = JSON.parse(routeIdsJson)
store.dispatch('permission/generateRoutes', routeIds).then(response => {
if (response && response.length > 0) {
const dynamicRoutes = response
router.addRoutes(dynamicRoutes)
next()
} else {
// 拿到角色的动态路由数组为空
window.sessionStorage.removeItem('routeData')
Message.warning('the permission routes belong to the current role is empty')
next()
}
})
} else {
getRouteIds(roleId).then(response => {
if (response.status === 200 && response.data.data.length > 0) {
const routeIds = response.data.data
window.sessionStorage.setItem('routeIds', JSON.stringify(routeIds))
store.dispatch('permission/generateRoutes', routeIds).then(response => {
if (response && response.length > 0) {
const dynamicRoutes = response
router.addRoutes(dynamicRoutes)
next()
} else {
// 拿到角色的动态路由数组为空
window.sessionStorage.removeItem('routeIds')
Message.warning('the permission routes belong to the current role is empty')
next()
}
})
} else {
// 获取角色动态路由失败
window.sessionStorage.removeItem('routeIds')
Message.warning('failed to get permission routes belong to the current role ')
next()
}
}).catch(error => {
// 调用获取角色下的动态路由列表接口失败,需要重新登录
Message.error(error)
removeToken()
if (window.sessionStorage.getItem('userInfo')) {
window.sessionStorage.removeItem('userInfo')
// eslint-disable-next-line no-trailing-spaces
}
next(`/login?redirect=${to.path}`)
NProgress.done()
})
}
} else {
// 用户未登录,重定向到登录界面
removeToken()
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
动态加载路由菜单的逻辑都在router.beforeEach
守卫函数中实现,这个文件中的修改是实现动态渲染菜单的关键,笔者也是通过一步步debug调试,踩了很多坑才最终修改好的。
2.7 修改src/views/index.vue
文件
修改登录组件中的用户名和密码为之前自己通过postman调用注册接口时的值
data() {
const validateUsername = (rule, value, callback) => {
if (!validUsername(value)) {
callback(new Error('Please enter the correct user name'))
} else {
callback()
}
}
const validatePassword = (rule, value, callback) => {
if (value.length < 6) {
callback(new Error('The password can not be less than 6 digits'))
} else {
callback()
}
}
return {
// loginForm中的username和password对应的值为修改的内容
// 用户不修改的化也可以在输入框中删除原来的用户名和密码后再输入正确的用户名和密码
loginForm: {
username: 'heshengfu',
password: 'heshengfu123'
},
loginRules: {
username: [{ required: true, trigger: 'blur', validator: validateUsername }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
},
passwordType: 'password',
capsTooltip: false,
loading: false,
showDialog: false,
redirect: undefined,
otherQuery: {}
}
}
2.8 修改 src/utils/validate.js
文件
修改其中的validUsername
方法,原项目中限制了只能是admin和editor两个用户
export function validUsername(username) {
if (username == null || username.trim() === '') {
Message.error('用户名不能为空')
return false
}
return true
}
2.9 修改build/index.js
和vue.config.js
文件
(1)将build/index.js
中的端口号改为3000,读者也可以改为任意安全且没有被占用的端口
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
// 端口设置为3000
const port = 3000
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)
(2) 将vue.config.js
文件中的代理转发注释掉
// All configuration item explanations can be find in https://cli.vuejs.org/config/
module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: true
}
// proxy: {
// '/api': {
// target: 'http://localhost:8081/blog',
// changeOrigin: true,
// pathRewrite: {
// '^/api': 'http://localhost:8081/blog'
// }
// }
// }
// after: require('./mock/mock-server.js')
},
因为我们的后台服务做了跨域设置,也就不需要代理转发了
2.10 修改.env.development
文件
# base api
#VUE_APP_BASE_API = '/api'
VUE_APP_BASE_API = 'http://localhost:8081/blog'
port = 3000
将VUE_APP_BASE_API
改为后台API
的请求前缀,即http://localhost:8081/blog
到这里前台要改的文件也就改完了
3 效果体验
3.1 启动前后台服务
先启动后台服务,需要注意的是在启动后台之前请先启动本地的mysql
服务,防止程序连接不上mysql
数据库而报错
修改好vue-element-admin
项目中的js
文件后,在vue-element-admin
项目的根目录下右键->git bash ,在弹出的控制台中输入npm run dev
控制台出现如下信息代表前端服务启动成功:
App running at:
- Local: http://localhost:3000/
- Network: http://192.168.1.235:3000/
3.2 登录并进入首页
前端服务启动成功后会自动打开浏览器跳转到登录页面,如图1所示
图 1 前端项目启动成功后进入登录界面
点击Login按钮登录成功后即跳转到vue-element-admin项目首页
图 2 登录成功后进入项目首页
登录的过程中我们可以通过点击鼠标右键->检查 进入开发者模式查看浏览器发起的网络请求,我们清楚地看到用户登录成功接口和根据角色ID查询路由资源列表接口
图 3 登录请求标头
图 4 登录请求响应数据预览
图 5 获取当前角色路由ID集合数据预检请求
图 6 获取当前角色路由ID集合数据GET请求
图 7 获取当前角色路由ID集合数据接口成功响应
进入首页后我们点击动态加载出来的路由Permission菜单下的子菜单Page Permission发现可以顺利进入权限控制页面,而没有出现从后台动态加载整个路由组件时出现的报404的问题。
图 8 进入动态控制菜单的字菜单Page Permission页面
至此,使用vue和vue-router整合合spring-boot技术实现基于角色动态加载菜单,并按权限访问页面的功能最难的一关已近闯过来了!后面笔者将再接再厉在此基础上实现给用户分配角色、给角色分配资源并结合spring-security实现按钮粒度的权限控制等一整套权限控制体系,敬请期待!
4 结语 vue-element-admin 项目是国内非常有名的一个开源项目,目前github上的start数已经超过4万。项目作者是就职于国内知名互联网公司今日头条的作者PanJiaChen。作者的关于权限控制的专栏文章地址:手摸手,带你用vue撸后台 系列二(登录权限篇) (juejin.cn),感兴趣的读者可以好好看一看。
本文的功能实现依赖于对vue-element-admin项目源码的深度研究,尤其对src目录下的permission.js、src/store/module目录下的permission.js和user.js以及与菜单有关的src/layout目录下的index.vue
及components/Sidebar/SidebarItem.vue和conponents/AppMain.vue等几个重要文件中的源码的深入研究。读者如果需要对vue-element-admin项目进行改造,建议重点研读这几个文件中的源码。
5 推荐阅读
[2] 介绍一个开源博客项目VBlog并打包部署到已运行多个Web项目的Nginx服务器下
[3] SpringBoot项目集成阿里云对象存储服务实现文件上传
[5] 强烈推荐一个技术栈丰富的微电商项目luban-mall
本度前端修改过的代码已全部上传到笔者个人的gitee仓库,感兴趣的小伙伴关注笔者的微信公众号【阿福谈Java技术栈】,发送关键词【vue-element-admin】,已关注的读者在微信公众号中找到【阿福谈Java技术栈】后直接发送关键此消息即可获得前端代码仓库地址。
本文较长,属于万字长文,可能读者很难有耐心一次性看完,觉得对你的工作有帮助的读者建议先收藏。本文首发个人微信公众号【阿福谈Java技术栈】,欢迎读者关注笔者的微信公众号,更过干货文章让我们一起在技术的路上共同成长!
---END---