前些天做课设了解到了一些关于权限控制的知识,开始想着看看若依框架是怎么实现的,奈何级别不够看完还是不很了解,于是上网搜搜相关文章,然后通过借助AI理解了其前后端的大致流程,最后还是基于自己的理解实现了一个差不多RBAC框架,在以后的项目中可以引用一下...
基于角色的权限控制(Role-Based Access Control, RBAC)是一种广泛使用的访问控制机制,它通过定义角色并将权限分配给角色,再将角色分配给用户来管理系统权限。
RBAC 核心概念
-
用户(User): 系统的使用者
-
角色(Role): 权限的集合,如"管理员"、"编辑"、"访客"等
-
权限(Permission): 对特定资源的具体操作权限,如"创建文章"、"删除用户"
-
会话(Session): 用户激活角色的过程
RBAC 前后端交互流程
基于角色的权限控制,用户拥有不同的角色,不同角色有不同的权限,用户根据角色获得权限:
首先,后端在用户登录时,根据用户的角色查询相应的权限。其次,将权限集合返回给前端。接着,前端拿到权限集合将其出入到一个useAuthStore的持久化存储中,并且里面有一个方法hasPermission(requiredPerms)判断是否拥有此权限。然后,定义路由的规则,路由守卫检查权限,如果有权限则可以跳转到路由,这样就实现了页面级的路由权限;那么如果想要实现组件级的页面权限,首先,还要定义一个组件函数permission,里面根据useAuthStore.hasPermission(requiredPerms)是否拥有此权限来判断是否因隐藏此组件;不止是前端进行权限校验,后端也需要进行权限校验:首先定义一个注解@RequiresPermission ,与PermissionAspect 切面类进行绑定,切面类中进行权限校验:获取用户的权限列表,若没有该接口要求的权限,则禁止访问。
-
后端权限控制
-
✔️ 登录时查询角色权限 → 返回权限集合
-
✔️ 使用
@RequiresPermission
注解 + AOP切面校验 -
✔️ 后端做最终权限防线(最关键)
-
-
前端权限控制
-
✔️ 登录后存储权限到
useAuthStore
(Pinia持久化) -
✔️ 路由守卫做页面级拦截
-
✔️
permission
组件做元素级控制
-
基于Springboot+Vue3的RBAC实现
1.数据库表设计
这里我没有使用物理外键,其中用户和角色多对多关系,角色和权限多对多关系。
现在我模拟角色vip1有look1权限,vip2有look1和look2权限,vip3有look1、look2和look3权限;一个用户hl拥有vip1和vip2角色:
2.后端登录返回权限集合逻辑
后端在登录成功后,生成jwt令牌,并随着用户所拥有的角色集合和对应的权限集合一起存入Redis中,并返回给前端
public Result<UserVO> login(@RequestBody User user) {
log.info("用户登录:{}", user);
User u=userService.login(user);
//判断用户是否存在
if(Objects.isNull(u)) {
throw new RuntimeException("用户不存在!");
}
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put("userId", u.getUserId());
String token = JwtUtil.createJWT(
jwtProperties.getUserSecretKey(),
jwtProperties.getUserTtl(),
claims);
// 查询权限集合
List<String> permissions = userService.permissionQuery(u.getUserId());
// 查询角色集合
List<String> roles = userService.roleQuery(u.getUserId());
UserVO userVO = UserVO.builder()
.userName(user.getUserName())
.permissions(permissions)
.roles(roles)
.token(token)
.build();
//将token存入redis
String key = "login:"+u.getUserId();
redisCache.setCacheObject(key, userVO);
return Result.success(userVO);
}
3.前端持久化存储
我使用pinia持久化存储,将登录后返回的用户信息存储,并增加了一个方法hasPermission(requiredPerms)判断是否拥有此权限:
import {defineStore} from 'pinia'
import {ref} from 'vue'
const useUserInfoStore = defineStore('userInfo',()=>{
//定义状态相关的内容
const info = ref({})
const setInfo = (newInfo)=>{
info.value = newInfo
}
const removeInfo = ()=>{
info.value = {}
}
// 判断用户权限
const hasPermission = (permission) => {
return info.value.permissions?.includes(permission)
}
return {info,setInfo,removeInfo,hasPermission}
},{persist:true})
export default useUserInfoStore;
4.页面级路由权限
定义路由守卫检查权限,如果有权限则可以跳转到路由,这里我创建了6个页面,其中V1是登录后进入的页面,teacher、student和manager是点击V1中的按钮进行跳转的页面:
以下是V1页面:
以下是关于路由的配置,这里我设置teacher、student和manager都是需要权限才能进行页面跳转:
//导入vue-router
import {createRouter, createWebHistory} from 'vue-router'
//导入组件
import StudentVue from '@/views/student.vue'
import Teacher from "@/views/teacher.vue";
import Manage from "@/views/Manage.vue";
import V1 from "@/views/V1.vue";
import Error from "@/views/Error.vue";
import useUserInfoStore from '@/stores/userInfo.js';
import Login from "@/views/Login.vue";
//定义路由关系
const routes = [
{path: '/login',name: 'login', component: Login},
{path: '/',name: 'v1', component: V1},
{path: '/teacher',name: 'teacher', component: Teacher, meta: {requiresAuth: true,permissions: ['look1']}},
{path: '/student',name: 'student', component: StudentVue, meta: {requiresAuth: true,permissions: ['look2']}},
{path: '/manage',name: 'manage', component: Manage, meta: {requiresAuth: true,permissions: ['look3']}},
{path: '/error', name: 'error', component: Error}
]
//创建路由器
const router = createRouter({
history: createWebHistory(),
routes: routes
});
router.beforeEach((to, from, next) => {
const userInfoStore = useUserInfoStore();
// 如果目标路由不是登录页且没有token,则跳转到登录页
if (to.name !== 'login' && !userInfoStore.info.token ) {
next({ name: 'login' });
return;
}
// 如果路由不需要权限,直接放行
if (!to.meta.requiresAuth) {
next();
return;
}
// 需要权限的路由:检查用户是否有对应权限
const requiredPermissions = to.meta.permissions;
// 遍历检查用户是否拥有所有必需的权限
const hasPermission = requiredPermissions.every(permission =>
userInfoStore.hasPermission(permission)
);
if (hasPermission) {
next(); // 有权限,放行
} else {
next({ name: 'error' }); // 无权限,跳转到错误页或其他提示
}
})
export default router
通过V1页面我们可以看到当前用户只有look1和look2权限,根据路由的配置我们是不能访问manager页面的,当我们点击前往去校长界面按钮时,就会跳转到错误页面:
5.组件级页面权限
组件级页面权限就是我们根据用户的权限来隐藏一些组件。
要实现组件级的页面权限,首先,还要定义一个组件函数permission,里面根据useAuthStore.hasPermission(requiredPerms)是否拥有此权限来判断是否因隐藏此组件。
下面是注册指令permission:
import useUserInfoStore from '@/stores/userInfo.js'
export default {
mounted(el, binding) {
const requiredPermission = binding.value; // 获取传入的权限字符串
const userInfoStore = useUserInfoStore();
// 如果没有权限,则移除该元素
if (!userInfoStore.hasPermission(requiredPermission)) {
el.parentNode?.removeChild(el)
}
}
}
import permission from './directives/permission.js'
// 注册权限指令
app.directive('permission', permission)
使用指令给我们的组件加上权限访问,如下我给点击去校长界面的按钮加上了需要look3的权限,可以看到刚才的按钮被移除了:
<div>
<el-button type="primary" @click="handleClick1">点击去老师界面</el-button>
</div>
<div>
<el-button type="primary" @click="handleClick2">点击去学生界面</el-button>
</div>
<div>
<el-button type="primary" v-permission="'look3'" @click="handleClick3">点击去校长界面</el-button>
</div>
6.后端权限校验
前后端一起进行权限校验才会更加安全,这部分逻辑将使用AOP来完成。
首先定义一个注解@RequiresPermission ,与PermissionAspect 切面类进行绑定,切面类中进行权限校验:获取用户的权限列表,若没有该接口要求的权限,则禁止访问。
6.1 基于角色校验
以下是@RequiresRoles注解,定义在方法上,用于判断是否有权限访问该接口:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresRoles {
String[] value() default {};
}
**
* 用于角色权限校验
* */
@Slf4j
@Aspect
@Component
public class RolesAuthAspect {
@Autowired
private RedisCache redisCache;
@Before("@annotation(requiresRoles)")
public void checkRolesPermission(JoinPoint jp, RequiresRoles requiresRoles) {
// 获取当前用户
Long userId = BaseContext.getCurrentId();
UserVO userVO = redisCache.getCacheObject("login:" + userId);
// 获取用户角色
List<String> roles = userVO.getRoles();
// 检查权限
String[] reRoles = requiresRoles.value();
boolean hasPermission = Arrays.stream(reRoles).allMatch(roles::contains);
if (!hasPermission){
throw new BaseException("权限不足!");
}
}
}
下面我们使用apifiox进行验证,我定义了三个接口,其中vip3是我们当前用户不能访问的:
@RequiresRoles({"vip1"})
@GetMapping("/vip1")
public Result vip1() {
log.info("vip1");
return Result.success("vip1成功");
}
@RequiresRoles({"vip2"})
@GetMapping("/vip2")
public Result vip2() {
log.info("vip2");
return Result.success("vip2成功");
}
@RequiresRoles({"vip3"})
@GetMapping("/vip3")
public Result vip3() {
log.info("vip3");
return Result.success("vip3成功");
}
6.2 基于权限校验
以下是@RequiresPermissions注解,定义在方法上,用于判断是否有权限访问该接口:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiresPermissions {
String[] value() default {};
}
/**
* 用于角色分配的权限校验
* */
@Slf4j
@Aspect
@Component
public class PermissionsAuthAspect {
@Autowired
private RedisCache redisCache;
@Before("@annotation(requiresPermissions)")
public void checkRolesPermission(JoinPoint jp, RequiresPermissions requiresPermissions) {
// 获取当前用户
Long userId = BaseContext.getCurrentId();
UserVO userVO = redisCache.getCacheObject("login:" + userId);
// 获取用户角色权限
List<String> permissions = userVO.getPermissions();
// 检查权限
String[] rePermissions = requiresPermissions.value();
boolean hasPermission = Arrays.stream(rePermissions).allMatch(permissions::contains);
if (!hasPermission){
throw new BaseException("权限不足!");
}
}
}
下面我们使用apifiox进行验证,我定义了三个接口,其中look3是我们当前用户不能访问的:
@RequiresPermissions({"look1"})
@GetMapping("/look1")
public Result look1() {
log.info("look1");
return Result.success("look1成功");
}
@RequiresPermissions({"look2"})
@GetMapping("/look2")
public Result look2() {
log.info("look2");
return Result.success("look2成功");
}
@RequiresPermissions({"look3"})
@GetMapping("/look3")
public Result look3() {
log.info("look3");
return Result.success("look3成功");
}