自定义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";
}
在自定义的注解中:
- 每个方法实际上是声明了一个配置参数;
- 方法的名称就是参数的名称;
- 返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、Enum);
- 可以通过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 属性中有两个参数:
validateToken()
即上面所说的切入点的别名,表明这个方法(around)被织入的地点;@annotation(tokenValidate)
表明 around 方法具有一个注解参数TokenValidate tokenValidate
,若缺少了这个则会报错:未绑定的切入点形参 'tokenValidate' 。
方法 around 也有两个参数:
-
ProceedingJoinPoint pjp
参数包含了切入点相关的很多信息,如切入点的对象,方法,属性等,通过反射就能获取这些状态和信息。如代码实例中,通过 pjp 获取了切入点方法的参数:
String token = getToken(tokenValidate, pjp.getArgs());
ProceedingJoinPoint 继承了 JoinPoint,在 JoinPoint 的基础上暴露出 proceed() 方法,这个方法是 AOP 代理链的一环,用于启动目标方法执行。在环绕通知中,前置通知发生在 proceed() 之前,返回通知发生在 proceed() 之后。
如代码实例中所示:
Object obj; try { obj = pjp.proceed(); }
其中 obj 即切入点方法的返回值。
-
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