Custom Token verification annotation #4

Custom Token verification annotation #4

The annotations for custom Token verification feel quite powerful, which taught me a lesson for those who don't understand AOP very well.

code example

TokenValidate annotation

/**
 * @author cynic
 * @Description: token Validation annotations are limited to methods of the controller class;
 *
 * value - If the token is in the request header (default), the value can directly use the default value. For other parameter transfer methods, you need to modify the value value. Refer to the enumeration class com.fh.iasp.app.cuxiao.enums.MetaDataTypeEnum
 * tokenVariableName - The default token parameter name is tokenDup. If you need to customize it, you can define the attribute value yourself;
 * msgReturnType  -  The default response body structure ApiResponse can be adjusted to Response_Plaintext according to the method response method
 * duration  -   Lock duration unit seconds Default value 30
 *
 * @date 2021-06-17
 */
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenValidate {

    MetaDataTypeEnum value() default MetaDataTypeEnum.HEADER; // By default, the anti-duplication verification token is placed in the header

    String tokenVariableName() default CuxiaoConstants.TOKEN_VARIABLE_NAME; //token parameter name can be customized

    String msgReturnType() default CuxiaoConstants.API_RESPONSE; //Return value response type temporarily supports ApiResponse / Response_Plaintext

    long duration() default 30l;   //Lock duration in seconds

}

Token location enumeration class

public enum MetaDataTypeEnum {

    HEADER("1"), //request header
    MAIN_BODY("2"), //request body
    FORM_MULTIPART("3"); //FORM 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 aspect class

/**
 * @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)) {
            //Due to the client version problem, the token is not passed, and it is skipped by default.
            return pjp.proceed();
        }
        logger.info("Anti-duplication token check token={}", token);
        String keyStr = String.format(CuxiaoConstants.TOKEN_KEY_TEMPLATE, token);
        //The lock time takes the annotation custom attribute, the default is 30s
        String lockToken = redisComplexLock.tryLock(keyStr, tokenValidate.duration(), TimeUnit.SECONDS);
        if (StringUtil.isEmpty(lockToken)) {
            logger.info("Duplicate commit occurred,token:{}", token);
            return msgErrorReturnRepeatToken("Do not submit the request repeatedly", tokenValidate.msgReturnType());
        }
        Object obj;
        try {
            obj = pjp.proceed();
        } catch (Exception e) {
            //When using a custom exception handler, this kind of business exception will be thrown; if it has been processed in the business itself, it will not enter the code here; if you use the @tokenValidate annotation and include other custom exception handling, please synchronize here expand
            if (e instanceof BizException || e instanceof LogicException || e instanceof IllegalArgumentException || e instanceof ServiceException) {
                throw e;
            }
            // log
            logger.error("produces an uncaught exception,", e);
            //In the case of an error, the entity is returned, but in fact it needs to be processed according to the return type
            return msgErrorReturn(BaseMsgEnum.SYSTEM_ERROR.getMsgContent(), tokenValidate.msgReturnType());
        } finally {
            //It is considered that the same token can only be used once and is not released manually;
//            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();
        //If the token is placed in the request header
        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;
                        // get class object
                        Class clazz = var1.getClass();
                        //Get all properties set in the class
                        Field[] fs = clazz.getDeclaredFields();
                        for (int i = 0; i < fs.length; i++) {
                            Field f = fs[i];
                            f.setAccessible(true); // Setting some properties is accessible
                            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);
    }

}

Knowledge point

@interface

@interface is used to define annotations and is a function added after JDK1.5. When an annotation is defined with @interface, the java.lang.annotation.Annotation interface is automatically inherited.

// The target scope, where METHOD means that the annotation acts on the method
@Target(ElementType.METHOD)
// Annotation life cycle, where RUNTIME means that the annotation still exists at runtime
@Retention(RetentionPolicy.RUNTIME)
public @interface QiyuancAnnotation {
    
    int id() default 723;

    String msg() default "Hello";

}

In a custom annotation:

  1. Each method actually declares a configuration parameter;
  2. The name of the method is the name of the parameter;
  3. The return value type is the type of the parameter (the return value type can only be basic types, Class, String, Enum);
  4. The default value of a parameter can be declared by default.

@Aspect

@Aspect marks the current class as an aspect class for the container to read.

Other aspect annotations can be used in aspect classes:

@Pointcut: marks the pointcut

@Around: surround enhancement, equivalent to MethodInterceptor

@AfterReturning: identifies the return enhancement method, equivalent to AfterReturningAdvice, which is executed when the method exits normally

@Before: identifies the pre-enhancement method, equivalent to BeforeAdvice

@AfterThrowing: identifies the exception enhancement method, equivalent to ThrowsAdvice

@After: identifies the post-enhancement method, equivalent to AfterAdvice, which will be executed whether an exception is thrown or a normal exit

@Pointcut

Pointcut is the trigger for implanting Advice. The definition of each Pointcut consists of two parts: one is the expression, and the other is the method signature. The method signature must be of type public void. The method in Pointcut can be regarded as a mnemonic referenced by Advice, because the expression is not intuitive, so we can name this expression by the method signature. So the methods in Pointcut only need the method signature without writing actual code in the method body.

Pointcuts can be matched against execution expressions:

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

Indicates that the entry point is all methods in the com.qiyuanc.aop.Message class;

In addition to matching pointcuts based on execution expressions, pointcuts can also be matched based on annotations. For example, in the above code, pointcuts are defined as:

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

Indicates that the pointcut is all methods that use @TokenValidate. The following validateToken method without a method body can be considered an alias for the pointcut expression.

@Around

@Around surround notification (Advice) integrates @Before, @AfterReturing, @AfterThrowing, @After four notifications. But the difference between @Around and the other four notification annotations is that the methods in the interface must be manually reflected before the methods in the interface can be executed, that is, @Around is actually a dynamic proxy.

The location of each notification in @Around:

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

See the example in the code above:

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

There are two parameters in the value property of @Around:

  1. validateToken() is the alias of the pointcut mentioned above, indicating the place where this method (around) is woven;
  2. @annotation(tokenValidate) indicates that the around method has an annotation parameter TokenValidate tokenValidate, if this is missing, it will report an error: unbound pointcut parameter 'tokenValidate' .

The method around also has two parameters:

  1. The ProceedingJoinPoint pjp parameter contains a lot of information related to the pointcut, such as the pointcut object, method, property, etc., which can be obtained through reflection.

    As in the code example, the parameters of the pointcut method are obtained through pjp:

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

    ProceedingJoinPoint inherits JoinPoint and exposes the proceed() method on the basis of JoinPoint. This method is a part of the AOP proxy chain and is used to start the execution of the target method. In wraparound advice, pre-advice happens before proceed() and return advice happens after proceed().

    As shown in the code example:

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

    where obj is the return value of the pointcut method.

  2. TokenValidate The tokenValidate parameter is an annotation parameter, from which the attributes of the annotation corresponding to the entry point can be obtained. It is very simple, not much to say.

Get the Token in the form through reflection

That is, this section of the example code:

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;
            // get class object
            Class clazz = var1.getClass();
            //Get all properties set in the class
            Field[] fs = clazz.getDeclaredFields();
            for (int i = 0; i < fs.length; i++) {
                Field f = fs[i];
                f.setAccessible(true); // Setting some properties is accessible
                if (tokenValidate.tokenVariableName().equals(f.getName())) {
                    try {
                        token = (String) f.get(var1);
                    } catch (Exception e) {
                        //ignore
                    }
                    hasTokenValue = true;
                    break;
                }
            }
            if (hasTokenValue) {
                break;
            }
        }
    }
}

Mainly look at how to use reflection to get attributes, no one really puts the Token in the form.

Tags: Java Spring

Posted by lajocar on Fri, 14 Oct 2022 11:12:39 +0530