0
点赞
收藏
分享

微信扫一扫

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

科牛 2022-03-30 阅读 52
springjava

使用场景:

基本上每个项目都离不开权限设计,这里我研究了下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("登录认证成功");
    }
}

图象辅助:


总结思路:

举报

相关推荐

0 条评论