背景 : 依托着之前的 提供三方API接口、调用第三方接口API接口、模拟API接口(二)通过token实现防止业务接口的重复调用 我们为了使得代码更精简,通过AOP的方式来实现登录后才能提交业务的验证、防止重复提交验证。
接下来,开搞!
自定义注解
package com.atguigu.signcenter.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 原文链接:https://blog.csdn.net/weixin_47560078/article/details/118222785
* 检查表单携带的 token 是否在缓存中
*/
@Target(ElementType.METHOD) // 表示此注解用在方法上
@Retention(RetentionPolicy.RUNTIME) // 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
public @interface ExistsApiToken {
/**
* 参数类型 head 、 form
*/
String type();
}
aop拦截器
package com.atguigu.signcenter.component;
import com.atguigu.signcenter.annotation.ExistsApiToken;
import com.atguigu.signcenter.constant.ReqParameterConstant;
import com.atguigu.signcenter.util.ApiUtil;
import com.atguigu.signcenter.util.TokenUtil;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* 原文链接:https://blog.csdn.net/weixin_47560078/article/details/118222785
* 先定义一个切面,myPointCut,这个切面主要针对 controller 层的方法。
* 前置通知:before,主要用于获取 token。
* 环绕通知:aroundAOP,主要用于判断 token 是否重复。
*
* @author: jd
* @create: 2024-08-04
*/
@Slf4j
@Component
@Aspect
public class ExtApiAop {
@Autowired
private TokenUtil tokenUtil;
/**
* 定义切入面:使用 AOP 环绕通知拦截所有访问,拦截的是 controller 层的所有public方法,任何参数数量都被拦截 并执行环绕通知中的校验逻辑
*/
@Pointcut("execution(public * com.atguigu.signcenter.controller.*.*(..))")
public void myPointCut() {
}
/**
* 环绕通知,主要用于业务逻辑的方法上
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("myPointCut()")
@Synchronized
public Object aroundAOP(ProceedingJoinPoint joinPoint) throws Throwable {
// 判断方法上是否有注解 @ExistsApiToken
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
ExistsApiToken existsApiToken = methodSignature.getMethod().getDeclaredAnnotation(ExistsApiToken.class);
String paramToken = null;
// 如果方法上有此注解,则拦截
if (null != existsApiToken) {
//获取请求在哪里
String type = existsApiToken.type();
log.info("type:{}", type);
// 获取上下文的请求
HttpServletRequest request = getRequest();
if (ReqParameterConstant.HEAD.equals(type)) {
paramToken = request.getHeader("token");
} else if (type.equals(ReqParameterConstant.FORM)) {
paramToken = request.getParameter("token");
}
if (StringUtils.isBlank(paramToken)) {
response("请求失效,请勿重复提交!");
return null;
}
log.info("请求token:{}",paramToken);
// 判断缓存里是否有该 token,如果有,且相同,则执行业务;如果没有,则返回无token,请携带token,如果有,且不同,则返回token无效,请重新提交业务。
String redisToken = tokenUtil.findToken(paramToken);
if(StringUtils.isBlank(redisToken)){
response("未携带token,请携带token后重新提交业务");
log.info("未携带token,请携带token后重新提交业务!");
}
if(!redisToken.equals(paramToken)){
response("携带token和redis存储的token,不一致,请重新获取token后提交业务");
log.info("携带token和redis存储的token,不一致,请重新获取token后提交业务!");
}
if(redisToken.equals(paramToken)){
response("请求有效,删除token[],无法再重复提交");
}
}
// 继续往后执行
Object proceed = joinPoint.proceed();
return proceed;
}
/**
* 获取容器上下文请求
*
* @return
*/
public HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
return request;
}
/**
* 相应错误响应信息
*
* @param msg
*/
private void response(String msg) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = requestAttributes.getResponse();
response.setHeader("Content-type", "text/html;charset=UTF-8");
try {
PrintWriter writer = response.getWriter();
writer.print(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
控制器添加token校验注解
/**
* 模拟高并发重复提交,根据 token 缓存防止重复提交
* 添加自定义 @ExistsApiToken 注解通过 aop 切面进行表单 token 认证
* type 类型为 head ,token 存储在 header 中
* @param data
* @return
*/
@ExistsApiToken(type = ReqParameterConstant.HEAD)
@PostMapping("/form/repeatSubmitTestForAOP")
public Map<String, Object> repeatSubmitTestForAOP(@RequestParam Map<String, String> data) {
Map<String, Object> result = new HashMap<>();
// 模拟提交表单信息
// TODO Something
log.info("做一些业务,业务模拟");
result.put("code", 0);
result.put("msg", "success");
return result;
}
测试
模拟前端获取token: 调用获取token的链接
获取token的代码:
tokenController
/**
*
*
* 获取 token ,这一步骤会向redis中写写入token
* 这个可以理解成登录操作,如果登录了则会向redis中写入一个token,然后下面方法中每一次做业务,都会验证携带的token是否和redis中的一样,一样才可以操作,否则不可以做业务,需要重新登录后再做。
* @return
*/
@GetMapping("/getToken")
public String getToken(){
return tokenUtil.getToken();
}
TokenUtil
/**
* 生成 token
* @return
*/
public String getToken(){
StringBuilder token = new StringBuilder("token_");
token.append(UUID.randomUUID().toString().replaceAll("-",""));
redisTemplateUtil.setString(token.toString(),token.toString(),TIMEOUT);
return token.toString();
}
获取到token,其实就是向redis中投放了一个特定的键值对
重头戏来了,我们访问我们的有token验证的测试接口
我们首先只传参数,但是不带token信息,
验证结果:不通过,提示 :请求失效,请勿重复提交!
先调用获取token接口,并把最新的token携带到token验证的请求头中(日常开发中前台获取token的接口,然后携带这个token做其他业务。)
调用:接口,验证通过
控制台输出了做业务的提示:
当我们携带的token。错误时,则给出提示:
码字不易,不足之处还请大家多多指教~
参考链接:https://blog.csdn.net/weixin_47560078/article/details/118222785