文末有下载链接!!!
在上文《springboot+自定义注解+AOP实现权限控制(一)》中,简单介绍了自定义注解和AOP的作用,本文开始从代码角度详细解释该如何使用。
实现效果
我想要这样一个效果,如:在某个接口加上自定义的权限注解,表示只有拥有此权限的用户才能访问。如A用户访问到add接口,如果有add接口的权限,允许访问,否则返回权限不足信息并不允许访问。其中返回信息为了美化,我通过全局异常来捕获。
设计思路:
1. 用户分为四种权限,管理员,添加和修改员,删除员,普通用户(只有查看权限);
2. 通过访问product类的增删改查接口来验证权限是否成功;
3. 流程:
(1)接口添加对应的注解,如add接口设置@Permission(value=add),表示拥有add权限的人才能访问
(2)用户登录后产生一个token,token包含用户的id信息(此步省略。假设token即userId)。
(3)访问接口,通过AOP拦截到此用户的登录信息,到数据库查询该用户的对应权限permissons
(4)查看permissons中是否包含注解的属性值value=add,
(5)如果包含,允许访问;否则抛出异常,不允许访问。
4. 关键点
(1)自定义权限注解如何设计
(2)AOP的切面如何编写
(3)用户-角色-权限如何设计
一. 搭建基本结构
二. 设计用户-角色-权限表结构
添加测试用户,userId=1为管理员,userId=2可以添加或修改,userId=3可以删除,其他只有查看权限。
用户表-角色表-权限表分别用中间表来关联外键。
三. 自定义注解
首先我定义了几个常量表示CRUD的权限
public class PermissionConsts {
/**
* 查看权限
*/
public static final String R = "R_PERMISSION";
/**
* 添加权限
*/
public static final String C = "C_PERMISSION";
/**
* 修改权限
*/
public static final String U = "U_PERMISSION";
/**
* 删除权限
*/
public static final String D = "D_PERMISSION";
}
自定义注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyPermission {
/**
* 默认只有查看权限
* @return
*/
String value() default PermissionConsts.R;
}
四. AOP切面
@Aspect
@Component
@Slf4j
public class PermissionAspect {
@Resource
private UserDao userDao;
/**
* 目标方法
*/
@Pointcut("@annotation(com.mac.annotation.MyPermission)")
private void permission() {
}
/**
* 目标方法调用之前执行
*/
@Before("permission()")
public void doBefore() {
System.out.println("================== step 2: before ==================");
}
/**
* 目标方法调用之后执行
*/
@After("permission()")
public void doAfter() {
System.out.println("================== step 4: after ==================");
}
/**
* 环绕
* 会将目标方法封装起来
* 具体验证业务数据
*/
@Around("permission()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("================== step 1: around ==================");
long startTime = System.currentTimeMillis();
/*
* 获取当前http请求中的token
* 解析token :
* 1、token是否存在
* 2、token格式是否正确
* 3、token是否已过期(解析信息或者redis中是否存在)
* */
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("非法请求,无效token");
}
// 校验token的业务逻辑
//假设token里只是一个userId,查询到他有删除和查看的权限,没有添加和修改的权限
// 解析token之后,获取当前用户的账号信息,查看它对应的角色和权限信息
//String userId = parse(token);
List<UserPermissionDto> codes = userDao.findPermissionCodeByUserId(token);
List<String> permissionCodes = codes.stream().map(UserPermissionDto::getPermissionCode).collect(Collectors.toList());
/*
* 获取注解的值,并进行权限验证
* */
Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
MyPermission myPermission = method.getAnnotation(MyPermission.class);
String value = myPermission.value();
// 将注解的值和token解析后的值进行对比,查看是否有该权限,如果权限通过,允许访问方法;否则不允许,并抛出异常
if(!permissionCodes.contains(value)){
throw new RuntimeException("对不起,您没有权限访问!");
}
// 执行具体方法
Object result = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
/*
* 记录相关执行结果
* 可以存入MongoDB 后期做数据分析
* */
// 打印请求 url
System.out.println("URL : " + request.getRequestURL().toString());
// 打印 Http method
System.out.println("HTTP Method : " + request.getMethod());
// 打印调用 controller 的全路径以及执行方法
System.out.println("controller : " + proceedingJoinPoint.getSignature().getDeclaringTypeName());
// 调用方法
System.out.println("Method : " + proceedingJoinPoint.getSignature().getName());
// 执行耗时
System.out.println("cost-time : " + (endTime - startTime) + " ms");
return result;
}
}
五. 接口添加注解
@RestController
@RequestMapping(value = "/product")
public class ProductController {
@Resource
private ProductService productService;
@GetMapping(value = "/findAll")
@MyPermission(value = PermissionConsts.R)
public Result<List<ProductDto>> findAll(){
return productService.findAll();
}
@PostMapping(value = "/add")
@MyPermission(value = PermissionConsts.C)
public Result<Boolean> add(@RequestBody ProductAddParam productParam){
return productService.add(productParam);
}
@PostMapping(value = "/update")
@MyPermission(value = PermissionConsts.U)
public Result<Boolean> update(@RequestBody ProductUpdateParam productParam){
return productService.update(productParam);
}
@GetMapping(value = "/delete")
@MyPermission(value = PermissionConsts.D)
public Result<Boolean> delete(@RequestParam Long id){
return productService.delete(id);
}
}
六. 其他代码可以查看文末的下载链接
七. 测试
1. 查看接口。
测试token=1到6(后期可用jwt优化token,这里简单设置为token=userId)的用户,发现数据库每个用户都可以访问。但userId不存在的用户,如token=100,则无法访问接口。
2. 添加/修改接口
添加/修改产品,userId=3的用户无法删除,因为数据库查询到权限不足;只有userId=2的用户可以添加/修改
3. 删除接口
删除productId=1的产品,userId=2的用户无法删除,因为数据库查询到权限不足;只有userId=1或3的用户可以删除