/**
* 自定义注解,后端接口资源的鉴权
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreAuthorize {
/**
* 权限标识符
*/
public String value() default "";
}
这段注解通常放在anno包下,当然也可以自定义,其中
@Target(ElementType.METHOD) 指定了这个注解可以应用在方法上
@Retention(RetentionPolicy.RUNTIME) 指定了这个注解在运行时保留,这意味着这个注解可以通过反射机制在运行时被读取和使用
@Documented 注解通常用于标记那些希望在生成的文档中有所体现的注解,以便提供更好的文档和使用说明。
注解创建完成之后,创建对应切面类 在方法上加上@Aspect,同时加上@Component注解交给容器管理
在切面类中 配置切入点
可以通过@annotation来配置
//配置切入点 , @annotation
@Pointcut("@annotation(com.product.anno.PreAuthorize)")
public void authorizePointCut(){
}
这表示 被这个注解标识的方法,都是要进行增强的方法
这里选择环绕通知 那为什么不选择前置通知呢?
先来看看两者区别
如何在环绕通知控制目标方法的执行呢,可以通过连接点 ProceedingJoinPoint 来实现
具体实现步骤
@Component
@Aspect
public class AuthorizeAspect {
@Autowired
SysMenuMapper sysMenuMapper; // 执行的关联查询 获取当前用户的权限列表
@Autowired
HttpServletRequest request;
@Autowired
HttpServletResponse response;
//配置切入点 , @annotation
@Pointcut("@annotation(com.product.anno.PreAuthorize)")
public void authorizePointCut(){
}
/**
* 后端接口资源鉴权:
* 1. 查询用户的所有接口资源权限列表
* 2. 获取当前接口资源的访问权限标识符
* 3. 比较
* @return
*/
@Around("authorizePointCut()")
public Object handle(ProceedingJoinPoint pjp) throws Throwable {
//1. 查询用户的所有接口资源权限列表
//请求头中模拟用户信息:user_id
String userId = request.getHeader("user_id");
//基于RBAC模型,获取用户的权限列表
List<String> perms = sysMenuMapper.selectMenuPermsByUserId(Long.parseLong(userId));
//2. 获取当前接口资源的访问权限标识符
MethodSignature signature = (MethodSignature) pjp.getSignature();//获取方法签名对象
Method method = signature.getMethod();//获取目标方法对象(后端接口资源)
PreAuthorize annotation = method.getAnnotation(PreAuthorize.class);
String methodPermission = annotation.value();
//3. 比较
boolean result = perms.contains(methodPermission);
//4. 如果当前用户角色没有当前接口资源的权限:403/亲,无权访问哦!
if(!result){
response.setContentType("application/json;charset=UTF-8");
response.setStatus(403);//没有访问权限
response.getWriter().write("亲,无权访问哦!");
return null; // 可以不加,最好加上,结束掉当前方法
}
//5.如果具有当前资源的访问权限,正常目标方法
return pjp.proceed();//执行目标方法
}
}
之后执行controller的方法
@PreAuthorize("clues:clue:list")
@GetMapping("/clues/list")
public AjaxResult list(TbClue tbClue) {
List<TbClue> list = tbClueService.selectTbClueList(tbClue);
return AjaxResult.success(list);
}
这样不管是A用户来访问还是B用户来访问,当执行这个方法之前会发现这个方法被增强了,它的底层动态代理最终执行的是增强之后代理对象的方法,而代理对象的方法在执行的时候执行的是增强的逻辑,执行目标方法之前,先执行增强方法。再执行目标方法 即 return pjp.proceed();//执行目标方法
之后不管A还是B,到底能不能执行controller这个方法,得先执行这个鉴权流程,如果走到了最后,说明有权,如果没有,就没有权
那如果想实现权限数据过滤呢,即:你只能看到属于你自己的数据;你只能看到你有权限的数据,我只能看到我有权限的数据。
数据范围权限划分思路梳理与功能落地
使用自定义注解+AOP,核心是动态SQL拼接
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {
}
因为需要在用户登录的时候就查询用户的权限列表并在前端显示,所以需要在请求到达Controller之前执行,这里使用AOP的前置通知
定义切面类,完成用户数据权限鉴定逻辑
@Aspect
@Component
public class DataScopeAspect {
//编写切入点
@Pointcut("@annotation(com.itheima.anno.DataScope)")
public void dataScopePointCut(){}
@Autowired
HttpServletRequest request;
//通知类型:前置通知
@Before("dataScopePointCut()")
public void doBefore(JoinPoint jp){
//连接点:封装了目标方法的信息
//1.获取注解(判断是否存在注解)
MethodSignature signature = (MethodSignature) jp.getSignature();
DataScope annotation = signature.getMethod().getAnnotation(DataScope.class);
if(annotation == null){
return;
}
//2. 获取当前用户:请求头中模拟了用户信息
String userId = request.getHeader("user_id");
//3. 如果不是超管,则过滤数据:给目标方法第一个参数的实体对象,设置属性params[Map 集合],设置不同的过滤条件
/**
* 权限的设计:
* 1. 超管:所有数据都可以查询
* 2. 线索专员:只能查询属于自己的线索信息
* 3. 仅部门
* 4. 部门及其子部门
* 5. 自定义权限(自定义权限表)
* ... ...
*/
// 假设超级管理员userId为1
if(!"1".equals(userId)){
//不是超管
//获取目标方法的第一个参数:entity (未来要补全占位符),SQL增强
Object param = jp.getArgs()[0];//获取TbClue
if(param!=null && param instanceof BaseEntity){
BaseEntity baseEntity = (BaseEntity) param;
//设置条件
baseEntity.getParams().put("dataScope","user_id="+userId);
}
}
}
}
对应的实体类
/**
* 线索管理对象 tb_clue
* @date 2021-04-02
*/
@Data
public class TbClue extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 线索id
*/
private Long id;
/** 客户姓名 */
private String name;
/** 手机号 */
private String phone;
/** 渠道 */
private String channel;
/** 活动id */
private Long activityId;
/** 活动名称 */
private String activityName;
/** 活动名称 */
private String activityInfo;
/** 1 男 0 女 */
private String sex;
/** 年龄 */
private Integer age;
/** 微信 */
private String weixin;
/** qq */
private String qq;
/** 意向等级 */
private String level;
/** 意向学科 */
private String subject;
/** 状态(已分配1 进行中2 回收3 伪线索4) */
private String status;
/** 分配人 */
private String assignBy;
/** 分配时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private Date assignTime;
/** 所属人 */
private String owner;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date ownerTime;
/** 伪线索失败次数(最大数3次) */
private int falseCount;
/** 下次跟进时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private Date nextTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
private Date endTime;
这里发现并没有param这个属性,定义在了父类
@Data
public class BaseEntity implements Serializable
{
private static final long serialVersionUID = 1L;
/** 请求参数 */
@JsonIgnore
private Map<String, Object> params;
/** 搜索值 */
@JsonIgnore
private String searchValue;
/** 创建者 */
private String createBy;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 备注 */
@JsonIgnore
private String remark;
上述代码是把当前用户的userId添加进了param Map集合 baseEntity.getParams().put("dataScope","user_id="+userId);
他的key是dataScope,动态SQL拼接条件为
<!-- 数据范围过滤 -->
<if test="params.dataScope != null and params.dataScope != ''">
AND (${params.dataScope} )
</if>