SpringBoot2 - AOP - 实现鉴权 [管理员 | 前端 | 匿名用户]

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>> hot3.png

基于SpringBoot2 - AOP - 实现鉴权 [管理员 | 前端 | 匿名用户]

设计RESTFUL API的接口权限鉴别问题,能处理的方式有很多种,你可以直接在controller中鉴权,在调用service以及其它代码,也可以使用interceptor配置哪部分用户能访问哪部分接口,也可以使用shiro以及Spring Security等框架来实现权限,最后还可以采用AOP的方式来实现。

以下描述可能不准,但通俗易懂

  • AOP全称 Aspect Oriented Program,意思是面向切面变成。我来小小地解释一下:

    我们把一个对象考虑成一个棒棒馍,把馍馍切一刀,那么被切的这个地方我们叫切入点(馍馍上每一个可以被切的地方都叫做连接点),这个刀面就叫做切面,切面嵌入了馍馍里面。馍馍里面有只蛆要从馍馍的一端到另一端,就要经过刀面,蛆在到达刀面时,想办法爬上刀,在通过刀(咬个洞钻过去)。

    对象中的代码执行分先后,对象执行过程中里面有方法执行,某一句代码执行等,他们都分顺序执行。这个时候如果有个方法要执行了,我们在这儿切一刀,那这块对象就被切烂了。

    可能描述的有问题,简而言之,我们在要插入其它代码的地方用注解标记一下,那么就可以通过AOP的一系列机制在此处插入一段代码了。详细看看代码就明白了。

那么问题来了!为什么要选择AOP呢??


为什么不选择直接在controller中鉴权?

这一次项目接口爆发面积广,影响作用大,如果在每个controller中写大量类似的鉴权代码将会付出很大的代价,同样采用BaseController的方式把这部分代码抽出以降低复杂度仍然是一件比较麻烦的事情,因此该方案直接跳过。

为什么不选择interceptor呢?

这是之前做一个小练手项目时使用SpringBoot配置的拦截器:

interceptor写起来不算复杂,但是接口过多的时候,要添加好多没有权限的内容放行,这个配置类就会显得非常复杂。相对于AOP来说,AOP实现更加方便快捷。所以当跟AOP对比时我选择了AOP。

为什么不选择shiro和security?

框架一般为重型应用或者较为复杂的认证和权限场景提供解决方案,我的系统中只有管理员,前端,匿名三类角色,使用框架费时费力,相对比于框架,自己实现一个权限控制流程就更加简单了。


综合以上,AOP实现权限是一种更加便捷的方案。


以下就是使用AOP来实现权限的具体内容

功能介绍

前端请求后台时,需要判断是管理员还是普通用户还是匿名用户,后台是restful api,controller本身不作权限验证。在每一个controller方法前面添加一个注解,通过AOP来判断用户header中带过来的token字段是否存在,以及token在redis的session缓存中userId到底是管理员还是普通用户,来鉴别权利。

  • 鉴权通过,则执行controller对应的方法并返回
  • 鉴权失败,返回permission denied错误

东西介绍

  • 首先利用注解在controller上注解一下,表示我将在此处插入一段权限鉴别代码,这段代码判断如果请求者没有权限,就不会执行被标记的这个方法直接返回给用户权限错误了。如图:

  • 然后你得知道,这个注解是怎么实现的,里面有两个参数,一个表示是否开启鉴权,一个表示要执行controller中的方法你需要什么权限。

  • 当然注解本身只能起标记的作用,不能有业务实现。业务实现需要由切面类来实现:

代码浮现:

权限枚举类
package xyz.ruankun.machinemother.util.constant;
public enum  AuthAopConstant {
    /**
     * 管理员 普通用户 匿名者
     */
    ADMIN,
    USER,
    ANON
}
用于在controller上标记的注解

其中pass字段说明该注解是否生效,role字段表名要执行被切的那个controller中的方法需要什么权限。

package xyz.ruankun.machinemother.annotation;
import xyz.ruankun.machinemother.util.constant.AuthAopConstant;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(value = ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Authentication {
    /**
     *  true为启用验证
     *  false为跳过验证
     * @return
     */
    boolean pass() default true;

    AuthAopConstant role() default AuthAopConstant.ANON;
}
切面类

该类就是一个切面,里面定义了一个切入点,就是插入的这个面该从哪里开始执行,以及执行的逻辑等。权限代码看@Around标记的那个方法就行了

package xyz.ruankun.machinemother.aop;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import xyz.ruankun.machinemother.annotation.Authentication;
import xyz.ruankun.machinemother.repository.AdminRepository;
import xyz.ruankun.machinemother.repository.UserRepository;
import xyz.ruankun.machinemother.service.AdminService;
import xyz.ruankun.machinemother.service.UserInfoService;
import xyz.ruankun.machinemother.util.Constant;
import xyz.ruankun.machinemother.util.constant.AuthAopConstant;
import xyz.ruankun.machinemother.vo.ResponseEntity;

import javax.servlet.http.HttpServletRequest;

/**
 * 使用aop完成API请求时的认证和权限
 * made by Jason. Completed by mrruan
 */
@Aspect
@Component
public class AuthenticationAspect {

    public final static Logger logger = LoggerFactory.getLogger(AuthenticationAspect.class);

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private AdminService adminService;

    //去数据库查询权限
    @Autowired
    AdminRepository adminRepository;

    @Autowired
    UserRepository userRepository;

    @Pointcut(value = "@annotation(xyz.ruankun.machinemother.annotation.Authentication)")
    public void pointcut() {}

    /**
     * 与被注释方法正确返回之后执行
     * @param joinPoint 方法执行前的参数
     * @param result 方法返回值 后续观察,是否保存
     */
    @AfterReturning(returning = "result", value = "@annotation(xyz.ruankun.machinemother.annotation.Authentication)")
    public void after(JoinPoint joinPoint, Object result) {
        logger.info("refreshing token");
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof HttpServletRequest) {
                HttpServletRequest request = (HttpServletRequest) arg;
                String token = request.getParameter("token");
                if (token != null) {
                    //通过token获取id值更新token有效期
                    int userId = Integer.valueOf(userInfoService.readDataFromRedis(token));
                    String sessionKey = userInfoService.readDataFromRedis("session_key" + userId);
                    if (null == sessionKey){
                        //管理员是没有sessionkey的哟
                        adminService.updateSession(String.valueOf(userId),token,15);
                    }else
                        userInfoService.updateSession(userId,sessionKey, token,15);
                    logger.info("refreshed token");
                }else{
                    logger.info("not refreshed token");
                }
            }
        }

    }
    @Around("pointcut() && @annotation(authentication)")
    public  Object interceptor(ProceedingJoinPoint proceedingJoinPoint, Authentication authentication){

        boolean pass = authentication.pass();
        //要验证权限
        AuthAopConstant role = authentication.role();
        if(pass && role != AuthAopConstant.ANON){
            //通过拿到的role,我们可以知道能处理这个请求的角色是什么
            //如果是匿名者,直接放行,如果是用户,就需要用户的权限才行,管理员则需要管理员的角色才行
            //规定一致,token放在header中
            HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder
                    .getRequestAttributes()).getRequest();
            String token = request.getHeader("token");
            AuthAopConstant realRole = authenticate(token);
            if (realRole == role) {
                //权限正确,去访问吧
                try {
                    return proceedingJoinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                    ResponseEntity responseEntity = new ResponseEntity();
                    responseEntity.success(Constant.AOP_SERVER_ERROR, "", null);
                    return responseEntity;
                }
            }else{
                //权限错误,返回错误
                ResponseEntity responseEntity = new ResponseEntity();
                responseEntity.success(Constant.AUTH_ERROR, "permission denied,forbidden access", null);
                return responseEntity;
            }
        }else{
            //不验证权限
            try {
                return proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                ResponseEntity responseEntity = new ResponseEntity();
                responseEntity.success(Constant.AOP_SERVER_ERROR, "", null);
                return responseEntity;
            }
        }
    }

    /**
     * 这个方法用于判断该token所属的到底是谁(管理员? 用户? 匿名?)
     * @param token
     * @return
     */
    private AuthAopConstant authenticate(String token){
        String userId = null;
        try {
            userId = userInfoService.readDataFromRedis(token);
        } catch (Exception e) {
            e.printStackTrace();
            //读取userId错误(最大的可能是请求的header中没有token),直接返回匿名错误
            return  AuthAopConstant.ANON;
        }
        if(userId == null){
            //匿名的或者说用户过期的,没有找到session
            return AuthAopConstant.ANON;
        }else{
            Integer id;
            try {
                id = Integer.parseInt(userId);
                logger.info("userId:" + id);
            } catch (Exception e) {
                e.printStackTrace();
                //都抛出了异常了,这个userId是假的,直接匿名者
                return AuthAopConstant.ANON;
            }
            if(adminRepository.findById(id).isPresent()){
                //是管理员
                return AuthAopConstant.ADMIN;
            }else{
                if (userRepository.findById(id).isPresent()){
                    //是用户
                    return AuthAopConstant.USER;
                }else{
                    //没有发现它是用户,假的
                   return AuthAopConstant.ANON;
                }
            }
        }
    }
}
controller长什么样

拦截了一下登录方法,用来测试的,起作用了。注意一下,我的逻辑中是把token放在header中了的。

    /**
     * 只有管理员能干
     * @param img 图片文件
     * @return 返回上传成功
     */
    @PutMapping("/template")
    @Authentication(role = AuthAopConstant.ADMIN)
    public ResponseEntity putOneTemplate(@RequestParam MultipartFile img){
        ResponseEntity responseEntity = new ResponseEntity();
        boolean rs = qrCodeService.putTemplate(img);
        if (rs)
            responseEntity.success(null);
        else
            responseEntity.serverError();
        return  responseEntity;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值