/**
 * 自定义注解,后端接口资源的鉴权
 */
@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>