首页 > 其他分享 >自定义Token校验注解 #4

自定义Token校验注解 #4

时间:2022-10-14 15:33:12浏览次数:69  
标签:return String 自定义 Token 校验 token tokenValidate 注解 方法

自定义Token校验注解 #4

感觉挺厉害的自定义 Token 校验的注解,给不是很懂 AOP 的我上了一课。

代码实例

TokenValidate注解

/**
 * @author cynic
 * @Description: token校验注解  限用于controller类的方法上;
 *
 * value - 如果token在请求头中(默认),value直接使用默认值即可,其他传参方式,则需要修改value值,参考枚举类com.fh.iasp.app.cuxiao.enums.MetaDataTypeEnum
 * tokenVariableName - 默认token参数名tokenDup,如需自定义,可自己定义该属性值;
 * msgReturnType  -  默认响应体结构 ApiResponse ,  根据方法响应方式,可调整为Response_Plaintext
 * duration  -   锁时长 单位seconds  默认值30
 *
 * @date 2021-06-17
 */
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenValidate {

    MetaDataTypeEnum value() default MetaDataTypeEnum.HEADER; // 默认将防重复校验token放在header中

    String tokenVariableName() default CuxiaoConstants.TOKEN_VARIABLE_NAME; //token 参数名 可自定义

    String msgReturnType() default CuxiaoConstants.API_RESPONSE; //返回值响应类型  暂支持ApiResponse / Response_Plaintext

    long duration() default 30l;   //锁时长 单位seconds

}

Token位置枚举类

public enum MetaDataTypeEnum {

    HEADER("1"), //请求头
    MAIN_BODY("2"), //请求正文
    FORM_MULTIPART("3"); //FORM表单 multipart/form-data

    private String type;

    MetaDataTypeEnum(String type) {
        this.type = type;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

}

TokenValidate切面类

/**
 * @author cynic
 * @Description: token validate aspect
 * @date 2021-06-17
 */
@Aspect
@Component
public class TokenValidateAspect {

    private static final Logger logger = LoggerFactory.getLogger(TokenValidateAspect.class);

    @Autowired
    RedisComplexLock redisComplexLock;

    @Pointcut("@annotation(com.fh.iasp.app.cuxiao.annonation.TokenValidate)")
    public void validateToken() {
    }


    @Around(value = "validateToken() && @annotation(tokenValidate)")
    public Object around(ProceedingJoinPoint pjp, TokenValidate tokenValidate) throws Throwable {
        String token = getToken(tokenValidate, pjp.getArgs());
        if (StringUtil.isEmpty(token)) {
            //因客户端版本问题,不传token,默认略过
            return pjp.proceed();
        }
        logger.info("防重复token校验 token={}", token);
        String keyStr = String.format(CuxiaoConstants.TOKEN_KEY_TEMPLATE, token);
        //加锁时间取注解自定义属性,默认30s
        String lockToken = redisComplexLock.tryLock(keyStr, tokenValidate.duration(), TimeUnit.SECONDS);
        if (StringUtil.isEmpty(lockToken)) {
            logger.info("发生重复提交,token:{}", token);
            return msgErrorReturnRepeatToken("请勿重复提交请求", tokenValidate.msgReturnType());
        }
        Object obj;
        try {
            obj = pjp.proceed();
        } catch (Exception e) {
            //使用自定义异常处理器时,抛出此类业务异常;如已在业务自行处理,也不会进到此处代码;  如使用@tokenValidate注解的同时包含其他自定义异常处理,请在此处同步扩展
            if (e instanceof BizException || e instanceof LogicException || e instanceof IllegalArgumentException || e instanceof ServiceException) {
                throw e;
            }
            // log
            logger.error("产生未捕获异常,", e);
            //报错情况下 返回该实体  但实际上要根据返回类型做具体处理
            return msgErrorReturn(BaseMsgEnum.SYSTEM_ERROR.getMsgContent(), tokenValidate.msgReturnType());
        } finally {
            //认为同一token只可使用一次,不手动释放;
//            if (StringUtil.isNotEmpty(lockToken)) {
//                redisComplexLock.releaseLock(keyStr, lockToken);
//            }
        }
        return obj;
    }

    private String getToken(TokenValidate tokenValidate, Object[] args) {
        String token = "";
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
        //如果token放在请求头中
        if (tokenValidate.value().equals(MetaDataTypeEnum.HEADER)) {
            token = request.getHeader(tokenValidate.tokenVariableName());
        } else if (tokenValidate.value().equals(MetaDataTypeEnum.MAIN_BODY)) {
            token = request.getParameter(tokenValidate.tokenVariableName());
        } else {
            if (!(args == null || args.length == 0)) {
                for (Object var1 : args) {
                    if (var1 instanceof HashMap) {
                        token = (String) ((HashMap) var1).get(tokenValidate.tokenVariableName());
                        if (StringUtil.isNotEmpty(token)) {
                            break;
                        }
                    } else {
                        boolean hasTokenValue = false;
                        // 得到类对象
                        Class clazz = var1.getClass();
                        //得到类中的所有属性集合
                        Field[] fs = clazz.getDeclaredFields();
                        for (int i = 0; i < fs.length; i++) {
                            Field f = fs[i];
                            f.setAccessible(true); // 设置些属性是可以访问的
                            if (tokenValidate.tokenVariableName().equals(f.getName())) {
                                try {
                                    token = (String) f.get(var1);
                                } catch (Exception e) {
                                    //ignore
                                }
                                hasTokenValue = true;
                                break;
                            }
                        }
                        if (hasTokenValue) {
                            break;
                        }
                    }
                }
            }
        }
        return token;
    }

    private Object msgErrorReturn(String msg, String responseType) {
        if (CuxiaoConstants.API_RESPONSE.equals(responseType)) {
            return ApiResponse.failed(ApiResponse.CODE_FAIL_DEFAULT, msg);
        } else if (CuxiaoConstants.RESPONSE_PLAINTEXT.equals(responseType)) {
            ActionUtil.responsePlainText(JsonUtil.getErrMsg(msg));
            return null;
        }
        return ApiResponse.failed(ApiResponse.CODE_FAIL_DEFAULT, msg);
    }

    private Object msgErrorReturnRepeatToken(String msg, String responseType) {
        if (CuxiaoConstants.API_RESPONSE.equals(responseType)) {
            return ApiResponse.failed(CuxiaoConstants.TOKEN_REPEAT_CODE, msg);
        } else if (CuxiaoConstants.RESPONSE_PLAINTEXT.equals(responseType)) {
            ActionUtil.responsePlainText(JsonUtil.fastReturn(JsonUtil.CODE_BIZ, msg).toJSONString());
            return null;
        }
        return ApiResponse.failed(CuxiaoConstants.TOKEN_REPEAT_CODE, msg);
    }

}

知识点

@interface

@interface 用于定义注解,是在 JDK1.5 之后加入的功能。使用 @interface 定义注解时,自动继承了 java.lang.annotation.Annotation 接口。

// 目标作用域,此处 METHOD 表示该注解作用于方法上
@Target(ElementType.METHOD)
// 注解生命周期,此处 RUNTIME 表示运行时该注解仍存在
@Retention(RetentionPolicy.RUNTIME)
public @interface QiyuancAnnotation {
    
    int id() default 723;

    String msg() default "Hello";

}

在自定义的注解中:

  1. 每个方法实际上是声明了一个配置参数;
  2. 方法的名称就是参数的名称;
  3. 返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、Enum);
  4. 可以通过default来声明参数的默认值。

@Aspect

@Aspect 将当前类标注为切面类,供容器读取。

在切面类中就可以使用其他的切面注解:

@Pointcut:标记切入点

@Around:环绕增强,相当于MethodInterceptor

@AfterReturning:标识返回增强方法,相当于AfterReturningAdvice,方法正常退出时执行

@Before:标识前置增强方法,相当于BeforeAdvice

@AfterThrowing:标识异常增强方法,相当于ThrowsAdvice

@After: 标识后置增强方法,相当于AfterAdvice,不管是抛出异常或者正常退出都会执行

@Pointcut

Pointcut 是植入 Advice 的触发条件。每个 Pointcut 的定义包括两部分:一是表达式,二是方法签名。方法签名必须是 public void 型,可以将 Pointcut 中的方法看作是一个被 Advice 引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此 Pointcut 中的方法只需要方法签名,而不需要在方法体内编写实际代码。

切入点可以根据 execution 表达式进行匹配:

@Pointcut("execution(* com.qiyuanc.aop.Message.*(..))")

表示切入点为 com.qiyuanc.aop.Message 类中的所有方法;

除了根据 execution 表达式匹配切入点外,也可以根据注解匹配切入点,如在上面的代码中切点定义为:

@Pointcut("@annotation(com.fh.iasp.app.cuxiao.annonation.TokenValidate)")
public void validateToken() {
}

表示切入点为所有使用了 @TokenValidate 的方法,下面这个没有方法体的 validateToken 方法可以认为是切点表达式的别名。

@Around

@Around 环绕通知(Advice)集成了 @Before、@AfterReturing、@AfterThrowing、@After 四个通知。但 @Around 和其他四个通知注解的区别是:要先手动进行接口内方法的反射,才能执行接口中的方法,即 @Around 其实就是一个动态代理。

@Around 中各通知所处的位置:

try{
    @Before
    Result = method.invoke(obj, args);
    // Result = pjp.proceed();
    @AfterReturing
}catch(e){
    @AfterThrowing
}finally{
    @After
}

看上面的代码中的实例:

@Around(value = "validateToken() && @annotation(tokenValidate)")
public Object around(ProceedingJoinPoint pjp, TokenValidate tokenValidate) throws Throwable {
    //...
}

在 @Around 的 value 属性中有两个参数:

  1. validateToken() 即上面所说的切入点的别名,表明这个方法(around)被织入的地点;
  2. @annotation(tokenValidate) 表明 around 方法具有一个注解参数 TokenValidate tokenValidate,若缺少了这个则会报错:未绑定的切入点形参 'tokenValidate' 。

方法 around 也有两个参数:

  1. ProceedingJoinPoint pjp 参数包含了切入点相关的很多信息,如切入点的对象,方法,属性等,通过反射就能获取这些状态和信息。

    如代码实例中,通过 pjp 获取了切入点方法的参数:

    String token = getToken(tokenValidate, pjp.getArgs());
    

    ProceedingJoinPoint 继承了 JoinPoint,在 JoinPoint 的基础上暴露出 proceed() 方法,这个方法是 AOP 代理链的一环,用于启动目标方法执行。在环绕通知中,前置通知发生在 proceed() 之前,返回通知发生在 proceed() 之后。

    如代码实例中所示:

    Object obj;
    try {
        obj = pjp.proceed();
    }
    

    其中 obj 即切入点方法的返回值。

  2. TokenValidate tokenValidate 参数是一个注解参数,可以从这个参数中获取切入点对应注解的属性,很简单,不多说。

通过反射获取表单中的Token

即实例代码中的这一段:

if (!(args == null || args.length == 0)) {
    for (Object var1 : args) {
        if (var1 instanceof HashMap) {
            token = (String) ((HashMap) var1).get(tokenValidate.tokenVariableName());
            if (StringUtil.isNotEmpty(token)) {
                break;
            }
        } else {
            boolean hasTokenValue = false;
            // 得到类对象
            Class clazz = var1.getClass();
            //得到类中的所有属性集合
            Field[] fs = clazz.getDeclaredFields();
            for (int i = 0; i < fs.length; i++) {
                Field f = fs[i];
                f.setAccessible(true); // 设置些属性是可以访问的
                if (tokenValidate.tokenVariableName().equals(f.getName())) {
                    try {
                        token = (String) f.get(var1);
                    } catch (Exception e) {
                        //ignore
                    }
                    hasTokenValue = true;
                    break;
                }
            }
            if (hasTokenValue) {
                break;
            }
        }
    }
}

主要看看怎么用反射获取属性,不会真有人把 Token 放表单里吧。

标签:return,String,自定义,Token,校验,token,tokenValidate,注解,方法
From: https://www.cnblogs.com/qiyuanc/p/work4.html

相关文章