整体思路
会话开始之初,先初始化一个只有登录路由的Vue实例,在根组件通过 handleLogin() 将路由定向到登录页,再拉取登录框的数据,让每个请求携带token-- [‘X-Litemall-Admin-Token’]实现用户鉴权.
然后获取当前用户的权限数据,用户登录成功之后,会在全局钩子router.beforeEach中拦截路由,判断是否已获得token,在获得token之后就要去获取用户的基本信息了,主要包括路由权限和资源权限。
之后动态添加路由,路由分为全局路由和异步路由,异步路由需要加上权限信息,再从后台获取权限匹配,根据不同角色拥有的权限动态生成不同的菜单,实现全局权限验证方法,并为axios实例添加请求拦截器,完成权限控制。动态加载路由后,路由组件将随之加载并渲染,而后展现前端界面。
前端实现方式:
第一步:编写登录窗口
index.vue
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
</span>
<el-input :type="passwordType" v-model="loginForm.password" name="password" auto-complete="on" placeholder="password" @keyup.enter.native="handleLogin" />
<span class="show-pwd" @click="showPwd">
<svg-icon icon-class="eye" />
</span>
</el-form-item>
<el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">登录</el-button>
在使用vue写的登录窗口中通过 @keyup.enter.native=“handleLogin” 绑定handleLogin事件
第二步:编写 handleLogin() 事件
handleLogin() {
this.$refs.loginForm.validate(valid => {
if (valid && !this.loading) {
this.loading = true
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.loading = false
this.$router.push({ path: this.redirect || '/' })
}).catch(response => {
this.$notify.error({
title: '失败',
message: response.data.errmsg
})
this.loading = false
})
} else {
return false
}
})
}
这里重点关注两行代码
this.$store.dispatch('LoginByUsername', this.loginForm).then(() => {
this.$router.push({ path: this.redirect || '/' })
这两行代码意思是将请求转发到 LoginByUsername 事件,并且转发发送请求后加上 ‘/’ 。
第三步:使用watch监听
watch: {
$route: {
handler: function(route) {
this.redirect = route.query && route.query.redirect
},
immediate: true
}
},
第四步:编写第二步绑定的js事件 loginByUsername
export function loginByUsername(username, password) {
const data = {
username,
password
}
return request({
url: '/auth/login',
method: 'post',
data
})
}
注意:在这里,请求方式为post,请求路径为 /auth/login 。
第五步:token验证(重点关注)
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done()
} else {
if (store.getters.perms.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetUserInfo').then(res => { // 拉取user_info
const perms = res.data.data.perms // note: perms must be a array! such as: ['GET /aaa','POST /bbb']
store.dispatch('GenerateRoutes', { perms }).then(() => { // 根据perms权限生成可访问的路由表
router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓
if (hasPermission(store.getters.perms, to.meta.perms)) {
next()
} else {
next({ path: '/401', replace: true, query: { noGoBack: true }})
}
// 可删 ↑
}
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next(`/login?redirect=${to.path}`) // 否则全部重定向到登录页
NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
}
}
在这里需要注意以下几行代码:
store.dispatch('GetUserInfo').then(res => { // 拉取user_info
这里拉取userinfo,肯定要从表单获取,因此要写一个getUserInfo事件如下
export function getUserInfo(token) {
return request({
url: '/auth/info',
method: 'get',
params: { token }
})
}
store.dispatch('GenerateRoutes', { perms }).then(() => {
表示根据perms权限生成可访问的路由表
绑定的GenerateRoutes
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { perms } = data
let accessedRouters
if (perms.includes('*')) {
accessedRouters = asyncRouterMap
} else {
accessedRouters = filterAsyncRouter(asyncRouterMap, perms)
}
commit('SET_ROUTERS', accessedRouters)
resolve()
})
}
第六步:根据权限动态修改路由(重点关注!!!)
import { asyncRouterMap, constantRouterMap } from '@/router'
/**
* @param {
* @param {*} route
*/
/**
* 通过meta.perms判断是否与当前用户权限匹配
* @param perms
* @param route
*/
function hasPermission(perms, route) {
if (route.meta && route.meta.perms) {
return perms.some(perm => route.meta.perms.includes(perm))
} else {
return true
}
}
/**
* 递归过滤异步路由表,返回符合用户角色权限的路由表
* @param routes asyncRouterMap
* @param perms
*/
function filterAsyncRouter(routes, perms) {
const res = []
routes.forEach(route => {
const tmp = { ...route }
if (tmp.children) {
tmp.children = filterAsyncRouter(tmp.children, perms)
if (tmp.children && tmp.children.length > 0) {
res.push(tmp)
}
} else {
if (hasPermission(perms, tmp)) {
res.push(tmp)
}
}
})
return res
}
const permission = {
state: {
routers: constantRouterMap,
addRouters: []
},
mutations: {
SET_ROUTERS: (state, routers) => {
state.addRouters = routers
state.routers = constantRouterMap.concat(routers) // concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。
}
},
actions: {
GenerateRoutes({ commit }, data) {
return new Promise(resolve => {
const { perms } = data
let accessedRouters
if (perms.includes('*')) {
accessedRouters = asyncRouterMap
} else {
accessedRouters = filterAsyncRouter(asyncRouterMap, perms)
}
commit('SET_ROUTERS', accessedRouters) // 保存用户权限标识集合
resolve()
})
}
}
}
export default permission
这里的代码说白了就是干了一件事,通过用户的权限和之前在router.js里面asyncRouterMap的每一个页面所需要的权限做匹配,最后返回一个该用户能够访问路由有哪些,其中,主路由里每一个异步路由都有一个perms标签控制权限信息,如下
{
path: 'issue',
component: () => import('@/views/mall/issue'),
name: 'issue',
meta: {
perms: ['GET /admin/issue/list', 'POST /admin/issue/create', 'GET /admin/issue/read', 'POST /admin/issue/update', 'POST /admin/issue/delete'],
title: '通用问题',
noCache: true
}
},
后台数据库中获取相应的权限信息通过 第六步 的代码进行匹配,成功则返回相应路由,从而达到动态展示菜单的效果
后台代码重点需要观察以下几块
ShiroConfig:
@Configuration
public class ShiroConfig {
@Bean
public Realm realm() {
return new AdminAuthorizingRealm();
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/admin/auth/login", "anon");
filterChainDefinitionMap.put("/admin/auth/401", "anon");
filterChainDefinitionMap.put("/admin/auth/index", "anon");
filterChainDefinitionMap.put("/admin/auth/403", "anon");
filterChainDefinitionMap.put("/admin/index/index", "anon");
filterChainDefinitionMap.put("/admin/**", "authc");
shiroFilterFactoryBean.setLoginUrl("/admin/auth/401");
shiroFilterFactoryBean.setSuccessUrl("/admin/auth/index");
shiroFilterFactoryBean.setUnauthorizedUrl("/admin/auth/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SessionManager sessionManager() {
return new AdminWebSessionManager();
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public static DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
LitemallPermissionService
@Service
public class LitemallPermissionService {
@Resource
private LitemallPermissionMapper permissionMapper;
public Set<String> queryByRoleIds(Integer[] roleIds) { //java中Set集合是一个不包含重复元素的Collection
Set<String> permissions = new HashSet<String>();
if(roleIds.length == 0){
return permissions;
}
LitemallPermissionExample example = new LitemallPermissionExample();
example.or().andRoleIdIn(Arrays.asList(roleIds)).andDeletedEqualTo(false);
List<LitemallPermission> permissionList = permissionMapper.selectByExample(example);
for(LitemallPermission permission : permissionList){
permissions.add(permission.getPermission());
}
return permissions;
}
public Set<String> queryByRoleId(Integer roleId) {
Set<String> permissions = new HashSet<String>();
if(roleId == null){
return permissions;
}
LitemallPermissionExample example = new LitemallPermissionExample();
example.or().andRoleIdEqualTo(roleId).andDeletedEqualTo(false);
List<LitemallPermission> permissionList = permissionMapper.selectByExample(example);
for(LitemallPermission permission : permissionList){
permissions.add(permission.getPermission());
}
return permissions;
}
public boolean checkSuperPermission(Integer roleId) {
if(roleId == null){
return false;
}
LitemallPermissionExample example = new LitemallPermissionExample();
example.or().andRoleIdEqualTo(roleId).andPermissionEqualTo("*").andDeletedEqualTo(false);
return permissionMapper.countByExample(example) != 0;
}
public void deleteByRoleId(Integer roleId) {
LitemallPermissionExample example = new LitemallPermissionExample();
example.or().andRoleIdEqualTo(roleId).andDeletedEqualTo(false);
permissionMapper.logicalDeleteByExample(example);
}
public void add(LitemallPermission litemallPermission) {
litemallPermission.setAddTime(LocalDateTime.now());
litemallPermission.setUpdateTime(LocalDateTime.now());
permissionMapper.insertSelective(litemallPermission);
}
}
这里需要关注以下几行代码:
1.判断是否是超级管理员,是的话赋予赋予所有权限,( “*”)
LitemallPermissionExample example = new LitemallPermissionExample();
example.or().andRoleIdEqualTo(roleId).andPermissionEqualTo("*").andDeletedEqualTo(false);
return permissionMapper.countByExample(example) != 0;
2.查找出对应角色拥有的资源权限信息,遍历并返回权限信息。
注意:这里在数据库表中通过外键管理了角色表和权限表,
LitemallPermissionExample example = new LitemallPermissionExample();
example.or().andRoleIdEqualTo(roleId).andDeletedEqualTo(false);
List<LitemallPermission> permissionList = permissionMapper.selectByExample(example);
for(LitemallPermission permission : permissionList){
permissions.add(permission.getPermission());
}
return permissions;
权限表如下:
这里是引号,但是在后台代码中会转换为“/”,实现代码如下
data.put("roles", roles);
// NOTE
// 这里需要转换perms结构,因为对于前端而已API形式的权限更容易理解
data.put("perms", toApi(permissions));
AdminAuthorizingRealm:
public class AdminAuthorizingRealm extends AuthorizingRealm {
@Autowired
private LitemallAdminService adminService;
@Autowired
private LitemallRoleService roleService;
@Autowired
private LitemallPermissionService permissionService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {//PrincipalCollection是一个身份集合,因为我们可以在Shiro中同时配置多个Realm,所以呢身份信息可能就有多个;因此其提供了PrincipalCollection用于聚合这些身份信息:
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
LitemallAdmin admin = (LitemallAdmin) getAvailablePrincipal(principals);
Integer[] roleIds = admin.getRoleIds();
Set<String> roles = roleService.queryByIds(roleIds);
Set<String> permissions = permissionService.queryByRoleIds(roleIds);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //SimpleAuthenticationInfo 会合并多个Principal为一个PrincipalCollection
info.setRoles(roles);
info.setStringPermissions(permissions);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
String password = new String(upToken.getPassword());
if (StringUtils.isEmpty(username)) {
throw new AccountException("用户名不能为空");
}
if (StringUtils.isEmpty(password)) {
throw new AccountException("密码不能为空");
}
List<LitemallAdmin> adminList = adminService.findAdmin(username);
Assert.state(adminList.size() < 2, "同一个用户名存在两个账户");
if (adminList.size() == 0) {
throw new UnknownAccountException("找不到用户(" + username + ")的帐号信息");
}
LitemallAdmin admin = adminList.get(0);
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if (!encoder.matches(password, admin.getPassword())) {
throw new UnknownAccountException("找不到用户(" + username + ")的帐号信息");
}
return new SimpleAuthenticationInfo(admin, password, getName());
}
}
PrincipalCollection是一个身份集合,因为我们可以在Shiro中同时配置多个Realm, 所以身份信息可能就有多个;因此其提供了PrincipalCollection用于聚合这些身份信息。
参考博客二:https://juejin.im/post/591aa14f570c35006961acac
————————————————
版权声明:本文为CSDN博主「冰阔落DG」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/HR18770171448/article/details/103430555