aop实现权限及流程讲解(优化)

使用场景:

基本上每个项目都离不开权限设计,这里我研究了下XX的基础框架,理解了aop权限在项目中的真实场景及思路


代码描述:

``
自定义认证+权限注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
    /**
     * 该属性是用来校验权限的
     */
    boolean verify() default true;
	/**
	* 该属性可加可不加,可以扩展为接口的自定义权限值
	*/
	//String value();
}

``
自定义切面类

@Slf4j
@Aspect
@Component
public class AuthAop {

    @Autowired
    private TokenService tokenService;
    /**
     *
     * @param jp
     * @param aaa 变量名可随意,可以理解为一个注解类型的对象
     */
    @Before("@annotation(aaa)")
    public void auth(JoinPoint jp,Auth aaa){
    	//封装的工具类,基于ThreadLocal获取当前请求
        HttpServletRequest httpServletRequest = GlobalParamUtils.currentRequest();
        // 获取请求的地址,即是协议+ip+端口后面的资源路径
        String requestURI = httpServletRequest.getRequestURI();
        // 利用注解@Slf4j打印信息
        log.info("请求url:{}",requestURI);
        // 获取请求头
        String token = httpServletRequest.getHeader("token");
		// 通过断言工具类检验token是否为null,为null就抛出异常,等待全局异常捕获处理
        AssertUtil.hasText(token, TokenExceptionEnum.TOKEN_NONE);
        // 根据token从redis中获取authInfo,里面包含了对token的判断
        AuthInfoVO authInfo = tokenService.userInfoFromToken(token);
        // 断言判断是否为null,为null就抛出异常,等待全局异常捕获处理
        AssertUtil.notNull(authInfo, TokenExceptionEnum.TOKEN_EXPIRE);
        // 根据请求参数,来决定是否设置authInfo
        boolean flag = setTokenInfo(jp, authInfo);
        // 如果参数中没有BaseAuthVO入参,或者没有AuthInfoVO类型的属性,那就将authInfoVO存入该线程
        if (!flag) {
            log.info("由于没有认证参数,我需要将authInfoVO存入该线程");
            // TokenInfoHolder下面会说
            TokenInfoHolder.set(authInfo);
        }
        // 判断是否需要权限校验,跟请求uri比较,如果没有权限抛出权限异常,然后捕获返回
        if (aaa.verify()) {
        	// 从redis中根据用户id取出权限列表,权限信息不会存在token或者authInfo中
            Set<String> functions = tokenService.getFunctions(authInfo.getUserId());
            log.info("用户功能权限有:{}",functions);
            String authStr = requestURI.toLowerCase();
            log.info(authStr);
            // 判断权限集合中是否有该请求路径,数据库中存入的就是请求uri
            AssertUtil.contain(functions, authStr, TokenExceptionEnum.MEMBER_TYPE_INVALIDATE);
        }
        // 刷新redis中token为1小时,即重新设置时间
        tokenService.renewalToken(token);
    }

    /**
     * 看请求参数中是否有基础认证入参,如果有,就给该参数设置authInfo,
     * 注意:无论如何,当前访问的用户信息都会存储,要么存在入参参数中,要么存在线程中,
     * 目的就是为了保证在数据库操作时能记录用户信息
     * @param jp
     * @param authInfo
     * @return
     */
    private boolean setTokenInfo(JoinPoint jp, AuthInfoVO authInfo) {
    	// 获取被拦截请求的参数列表
        Object[] args = jp.getArgs();
        boolean flag = false;
        log.info("用户ID[{}]", authInfo.getUserId());
        // 如果是文件参数类型,或者HttpServletRequest|HttpServletResponse就跳过当前循环
        for (Object arg : args) {
            if (arg instanceof MultipartFile
                    || arg instanceof HttpServletRequest
                    || arg instanceof HttpServletResponse) {
                continue;
            }
            log.info("请求参数{}", arg);
            // BaseAuthVO交互类只有一个AuthInfoVO类属性
            if (arg instanceof BaseAuthVO) {
                BaseAuthVO reqVO = (BaseAuthVO) arg;
                reqVO.setAuthInfoVO(authInfo);
                flag = true;
            } else {
                if (Objects.isNull(arg)) {
                    continue;
                }
                // 比如其他的对象继承了BaseAuthVO,因此也有这个AuthInfoVO类属性,
                // 于是就可以遍历字段属性,看字段的类型是不是AuthInfoVO类,
                // 如果有,就将从redis取出来的authInfo设置到该字段上,注意:
                // 发送该请求时的携带的@RequestBody参数不用带上authInfoVO喔,
                // 因为是反射技术,他可以拿到该类的所有属性的
                Class<?> clazz = arg.getClass();
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    if (Objects.equals(field.getGenericType().toString(), "class com.shidai.vo.AuthInfoVO")) {
                        field.setAccessible(true);
                        try {
                            field.set(arg, authInfo);
                            flag = true;
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        return flag;
    }
	/**
	* 通过ThreadLocal.remove()清除,防止内存溢出
	*/
    @After(value = "@annotation(auth)")
    public void clearSuccess(Auth auth) {
        TokenInfoHolder.clear();
    }
	/**
	* 通过ThreadLocal.remove()清除,防止内存溢出
	*/
    @AfterThrowing(value = "@annotation(auth)")
    public void clearError(Auth auth) {
        TokenInfoHolder.clear();
    }
}
/**
 * TokenInfoHolder----就是基于ThreadLocal存储用户信息的,保证了线程安全
 *
 * @author roger
 * @date 2021/6/29 13:48
 */
public class TokenInfoHolder {
    /**
     * 一个线程内部共享,不同线程之间是隔离的,每个线程只能get()到自己的变量
     * ThreadLocal保证了每个线程都有一个独立实例副本,避免线程安全问题
     * 一个线程可以有多个TreadLocal来存放不同类型的对象的,都将放到你当前线程的ThreadLocalMap(threadLocals)-是一个数组结构
     * Entry[] ThreadLocal - k; Object - v
     * 应用场景:spring的事务管理用过(保证拿到同一个连接对象);还有springmvc的RequestContextHolder,因此你在aop中业务中都能拿到安全的对应请求对象
     *
     */
    private static final ThreadLocal<String> AUTH = new ThreadLocal<>();

    private TokenInfoHolder() {

    }

    /**
     * 将authInfo转换成JSon String存储
     *
     * @param authInfo 前端传入的AuthInfoVO对象
     *                 设置到线程中的ThreadLocalMaps中的ThreadLocal(AUTH)对象中
     */
    public static void set(AuthInfoVO authInfo) {
        String json = JSON.toJSONString(authInfo);
        AUTH.set(json);
    }

    /**
     * 通过ThreadLocal.get()从获取AuthInfo对象
     *
     * @return
     */
    public static AuthInfoVO authInfo() {
        return authInfo(AuthInfoVO.class);
    }

    private static <T> T authInfo(Class<T> clazz) {
        String info = AUTH.get();
        return JSON.parseObject(info, clazz);
    }

    /**
     * 通过ThreadLocal.remove()清除,防止内存溢出
     */
    public static void clear() {
        AUTH.remove();
    }
}

``
登录认证

@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Resource
    UserMapper userMapper;
    @Resource
    RoleMapper roleMapper;
    @Resource
    PermissionMapper permissionMapper;
    @Resource
    TokenService tokenService;
    @Override
    public BaseResultVO<UserPO> login(AuthInfoVO auth) {
        LambdaQueryWrapper<UserPO> lambdaQueryWrapper = new LambdaQueryWrapper<UserPO>().eq(UserPO::getUserName, auth.getUsername());
        UserPO user = userMapper.selectOne(lambdaQueryWrapper);
        AssertUtil.notNull(user,"该用户不存在");
        log.info("用户信息为:{}",user);
        auth.setUserId(String.valueOf(user.getUserId()));
        // 查询到所属角色
        String rid= roleMapper.getRoleListById(user.getUserId());
        // 根据角色再去查询所有的权限,存入redis中
        Set<String> func = permissionMapper.funcAuth(Integer.parseInt(rid));
        tokenService.setFunctions(String.valueOf(user.getUserId()),func);
        // 生成token,存入缓存,然后返回给前端
        String token = tokenService.setTokenInfo(auth);
        // 获取响应对象,设置token
        HttpServletResponse response = GlobalParamUtils.currentResponse();
        response.setHeader("token",token);
        return BaseResultVO.success("登录认证成功");
    }
}

图象辅助:

登录后redis中,用户信息就是token值
在这里插入图片描述
在这里插入图片描述


总结思路:

1.用户登录是放行不加认证权限校验的
2.登陆时做两件事:a.通过工具类根据用户信息(不包含功能权限)生成token并存入redis中,然后返回给前端;b.将功能权限也存入redis中
3.用户下次访问请求根据@Auth注解决定是否携带token请求头,服务器拿到该token值先去校验是否有效,然后拼接key,根据key获取redis中token值(authInfo),将authInfo通过json序列化转换为AuthInfo对象,然后根据请求参数对应的类是否包含了该AuthInfo属性,包含了就将authInfo设置到属性上,不包含就设置到ThreadLocal中,反正请求中都会拥有用户信息,方便数据库操作记录用户信息。访问了之后又给token续xx时间

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: AOP实现自定义权限注解可以通过使用Spring AOP实现。首先,你需要定义一个自定义注解,用于标记需要进行权限控制的方法。然后,你可以使用AOP的方式,在方法执行前或执行后进行权限验证。具体实现可以参考以下步骤: 1. 定义自定义注解:你可以使用@PreventRepeat注解来标记需要进行权限控制的方法。 2. 创建切面:你需要创建一个切面类,使用@Aspect注解标记,并在该类中定义一个切点,用于匹配被@PreventRepeat注解标记的方法。 3. 实现权限验证逻辑:在切面类中,你可以使用@Before或@After注解来定义权限验证的逻辑。在方法执行前或执行后,你可以进行相应的权限验证操作。 4. 配置AOP:最后,你需要在Spring配置文件中配置AOP,将切面类和切点与目标对象关联起来。 通过以上步骤,你就可以实现自定义权限注解的AOP实现了。这样,在被@PreventRepeat注解标记的方法执行前或执行后,你可以进行相应的权限验证操作。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* *3* [java-使用spring AOP实现自定义注解](https://blog.csdn.net/weixin_43846708/article/details/129547120)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值