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:
- Each method actually declares a configuration parameter;
- The name of the method is the name of the parameter;
- The return value type is the type of the parameter (the return value type can only be basic types, Class, String, Enum);
- 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:
- validateToken() is the alias of the pointcut mentioned above, indicating the place where this method (around) is woven;
- @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:
-
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.
-
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.