【AOP控制接口】java使用aop切面方式控制接口访问

235 阅读7分钟

开发环境:sdk11、springboot、springframework、aspectj、security

前提纪要:项目中要求提供开放接口供以访问,于是参照微信支付的验证方式进行签名与验证

为了实现通用与便于调用,使用注解类+aop实现请求的验证与响应的签名

首先实现@interface类,HttpMethod方法仅用于辨别请求类型(GET,POST,PUT等),可使用String代替

@Target({ElementType.METHOD}) // METHOD 说明该注解只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // RUNTIME 说明该注解在运行时生效
public @interface ExternalVerify {
  /** 请求类型 */
  HttpMethod requestType() default HttpMethod.GET;

  /** 请求的绝对URL */
  String absoluteUrl() default "";
}

然后是建立一个切面,与其前置通知和环绕通知

@Aspect
@Component
public class ExternalVerifyAspect {
  /**
   * @Pointcut :切入点声明,即切入到哪些目标方法。value 属性指定切入点表达式,默认为 ""。
   * 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
   *
   * 切入点表达式常用格式举例如下: - * com.wmx.aspect.EmpService.*(..)):表示 com.wmx.aspect.EmpService 类中的任意方法 -
   * * com.wmx.aspect.*.*(..)):表示 com.wmx.aspect 包(不含子包)下任意类中的任意方法 - * com.wmx.aspect..*.*(..)):表示
   * com.wmx.aspect 包及其子包下任意类中的任意方法 value 的 execution 可以有多个,使用 || 隔开.
   */
  @Pointcut("@annotation(ExternalVerify)"+ "@within(ExternalVerify)")
  public void evPointCut() {}
  
  
/**
 * 前置通知:目标方法执行之前执行以下方法体的内容。
 * value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
 */
  @Before("evPointCut()")
  public void before(JoinPoint joinPoint) {
  }
  
/**
 * 环绕通知
 * 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
 * 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
 * 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响目标方法的事务回滚
 * 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发
 *
 * @param joinPoint
 * @return
 * @throws Throwable
 */
  @Around(value = "evPointCut()")
  public Object handleControllerMethod(ProceedingJoinPoint joinPoint){
  }

在Controller中定位到具体接口方法,在方法上添加@ExternalVerify注解,注解中absoluteUrl为该接口的绝对路径,即去除域名部分的路径

@ExternalVerify(
    requestType = HttpMethod.POST,
    absoluteUrl = "/openApi/getTestList")
@PostMapping("/getTestList")
public List getTestList(
    @RequestBody Map<String, Object> params,
    HttpServletRequest request,
    HttpServletResponse response) {
  return new ArrayList<>();
}

完成该接口后,开始在前置通知中进行接口的鉴权

// 需参与签名的参数
// 注解内的请求类型
String requestType = null;
// 注解内的请求绝对地址
StringBuilder absoluteUrl = new StringBuilder();
// 请求的参数
StringBuilder params = new StringBuilder();
// 请求头的签名
String authorization = null;
Object[] args = joinPoint.getArgs();
// 获取方法,此处可将signature强转为MethodSignature
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();

/* 判断系统本身参数是否有缺失 */
// 方法注解,1维是注解
Annotation[] annotations = method.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
  // 这里判断当前注解是否为ExternalVerify.class
  if (annotation.annotationType().equals(ExternalVerify.class)) {
    // 校验该参数,验证一次退出该循环
    // TODO 校验参数
    ExternalVerify verify = (ExternalVerify) annotation;
    requestType = verify.requestType().name();
    absoluteUrl = new StringBuilder(verify.absoluteUrl());
    break;
  }
}
// @ExternalVerify中缺失参数
if (StringUtils.isEmpty(requestType) || StringUtils.isEmpty(absoluteUrl.toString())) {
  throw new Exception("系统内部参数缺失,请检查后重试");
}

/* 判断请求头是否有误并拼接post/put参数或在Get路径中拼接参数 */
for (Object arg : args) {
  if (StringUtils.isNull(arg)) continue;
  if (arg instanceof HttpServletRequest) {
    // 拿到请求头中的Authorization,假设请求的认证类型为Token 
    HttpServletRequest request = (HttpServletRequest) arg;
    authorization = request.getHeader("Authorization");
    if (StringUtils.isNotEmpty(authorization)
        && authorization.startsWith("Token ")) {
      authorization = authorization.replace("Token ", "");
    } else {
      throw new Exception("请求头Authorization错误,请检查后重试");
    }
  } else if (arg instanceof HttpServletResponse) {
    // 响应参数不做处理
    continue;
  } else {
    if ("GET".equals(requestType)) {
      if (arg instanceof Map) {
        try {
          absoluteUrl
              .append("?")
              .append(URLDecoder.decode(mapAsUrlParams((Map) arg), "utf-8"));
        } catch (UnsupportedEncodingException e) {
          throw new Exception("系统内部参数异常");
        }
      }
    } else {
      if (arg instanceof List || arg instanceof Map) {
        // 转为json类型的字符串
        params.append(JsonUtils.toJsonString(arg));
      } else if (arg.getClass().isArray()) {
        // 转为json类型的字符串
        params.append(JsonUtils.toJsonString(arg));
      }
    }
  }
}

// 进行验签
if (!checkToken(authorization, requestType, absoluteUrl.toString(), params.toString())) {
  throw new Exception("验签失败,请检查后重试");
}

拼接GET请求的方法

public static String mapAsUrlParams(Map<String, Object> source) {
  Iterator<String> it = source.keySet().iterator();
  StringBuilder paramStr = new StringBuilder();
  while (it.hasNext()) {
    String key = it.next();
    Object value = source.get(key);
    if (StringUtils.isNull(value)) {
      continue;
    }
    try {
      // URL 编码
      value = URLEncoder.encode(value.toString(), "utf-8");
    } catch (UnsupportedEncodingException e) {
      // do nothing
    }
    paramStr.append("&").append(key).append("=").append(value);
  }
  // 去掉第一个&
  return paramStr.substring(1);
}
  • 验签用到的方法,publicKeyprivateKey 为rsa非对称算法中的公私钥,请自行生成
  • 在前置通知中发现签名异常则可抛出,可以将接口停止并返回错误信息
  • 如需进一步的异常处理可使用 @AfterThrowing 进行处理
private static final String privateKey = "";
private static final String publicKey = "";
/**
 * @description TODO (检查token是否正确)
 * @params [authorization 收到的token数据, requestType HTTP请求方法, absoluteUrl 请求绝对路径,并去除域名部分得到参与签名的URL,
 *     params 发送信息主体]
 * @return java.lang.Boolean
 */
private static Boolean checkToken(
    String authorization, String requestType, String absoluteUrl, String params)
    throws Exception {
  String timestamp = "";
  String nonceStr = "";
  String oldSignature = "";
  try {
    String[] oldSignatures = authorization.split(",");
    /* 截取需要验证的值 */
    for (String old : oldSignatures) {
      if (old.contains("timestamp")) {
        timestamp = old.substring(old.indexOf(""") + 1, old.lastIndexOf("""));
      } else if (old.contains("nonce_str")) {
        nonceStr = old.substring(old.indexOf(""") + 1, old.lastIndexOf("""));
      } else if (old.contains("signature")) {
        oldSignature = old.substring(old.indexOf(""") + 1, old.lastIndexOf("""));
        oldSignature = new String(Base64.getDecoder().decode(oldSignature));
      }
    }
  } catch (Exception e) {
    throw new Exception("请求头分隔异常,请检查后重试");
  }
  /* 使用redis工具进行防重放攻击 */
  String redisKey = "TYPE_" + requestType + "_URL_" + absoluteUrl + "_NONCE_" + nonceStr;
  if (redisCache.getCacheObject(redisKey) == null) {
    redisCache.setCacheObject(redisKey, new Date().getTime(), 1000 > 60 * 30 * 1000, TimeUnit.MILLISECONDS);
  } else {
    throw new Exception( "请勿重复请求");
  }
  if (new Date().getTime() - Long.parseLong(timestamp) * 1000 > 60 * 30 * 1000)
    throw new Exception("请求发起时间过久,请检查后重试");

  String signature = getSign(requestType, absoluteUrl, timestamp, nonceStr, params);
  // newSignature能够获取到新的签名值,有需要可以自行输出查看
  //    String newSignature = "";
  //    try {
  //      newSignature = RsaUtils.encryptByPublicKey(publicKey, signature);
  //    } catch (GeneralSecurityException e) {
  //      e.printStackTrace();
  //    }
  //    newSignature = Base64.getEncoder().encodeToString(newSignature.getBytes());
  //    System.out.println(newSignature);
  if (StringUtils.isEmpty(signature))
    throw new Exception("签名值计算失败,请检查后重试");
  try {
    oldSignature = RsaUtils.decryptByPrivateKey(privateKey, oldSignature);
  } catch (GeneralSecurityException e) {
    e.printStackTrace();
    throw new Exception("签名值计算失败,请检查后重试");
  }
  return oldSignature.equals(signature);
}

/**
 * 获取签名内容
 *
 * @description TODO (/)
 * @params [requestType, absoluteUrl, timestamp, nonceStr, params]
 * @return java.lang.String
 */
private static String getSign(
    String requestType, String absoluteUrl, String timestamp, String nonceStr, String params) {
  String message = buildMessage(requestType, absoluteUrl, timestamp, nonceStr, params);
  return sign(message.getBytes(StandardCharsets.UTF_8));
}

/**
 * @description TODO (构建签名串)
 * @params [requestType HTTP请求方法, absoluteUrl 请求绝对路径,并去除域名部分得到参与签名的URL, timestamp 获取发起请求时的系统当前时间戳,
 *     nonceStr 请求随机串, body 发送信息主体]
 * @return java.lang.String
 */
private static String buildMessage(
    String requestType, String absoluteUrl, String timestamp, String nonceStr, String body) {
  return requestType
      + "\n"
      + absoluteUrl
      + "\n"
      + timestamp
      + "\n"
      + nonceStr
      + "\n"
      + body
      + "\n";
}

/**
 * @description TODO (计算签名值)
 * @params [message]
 * @return java.lang.String
 */
private static String sign(byte[] message) {
  try {
    MessageDigest crypt = MessageDigest.getInstance("SHA-256");
    crypt.reset();
    crypt.update(message);
    return getEncoder().encodeToString(crypt.digest());
  } catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
    return null;
  }
}
  • 响应签名则需要得到返回结果后进行签名
  • 但由于 @AfterReturning 是在方法返回后执行,无法达到在响应中返回的需求,因此选择 @Around 进行处理
/*
 * 环绕通知
 * 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式
 * 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.
 * 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响目标方法的事务回滚
 * 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发
 *
 * @param joinPoint
 * @return
 * @throws Throwable
 */
@Around(value = "evPointCut()")
public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
  StopWatch stopWatch = StopWatch.createStarted();
  Object proceed = joinPoint.proceed(joinPoint.getArgs());
  stopWatch.stop();
  // 等待到接口返回则继续执行
  for (Object arg : joinPoint.getArgs()) {
    if (StringUtils.isNull(arg)) continue;
    if (arg instanceof HttpServletResponse) {
      // 创建响应签名
      createReturnSign((HttpServletResponse) arg, proceed);
    }
  }
  return proceed;
}

/**
 * @description TODO (响应内设置返回签名串)
 * @params [response, body]
 * @return void
 */
private static void createReturnSign(HttpServletResponse response, Object body) {
  String timestamp = String.valueOf(Long.valueOf(new Date().getTime() / 1000));
  String nonceStr = WeCharPayUtils.getRandomString(64);
  String returnJson = StringUtils.isNotNull(body) ? JsonUtils.toJsonString(body) : "";
  String message = buildReturnMessage(timestamp, nonceStr, returnJson);

  String signature = sign(message.getBytes(StandardCharsets.UTF_8));
  String newSignature = "";
  // 得到rsa加密后的签名值
  try {
    newSignature = RsaUtils.encryptByPrivateKey(privateKey, signature);
  } catch (GeneralSecurityException e) {
    e.printStackTrace();
  }
  newSignature = Base64.getEncoder().encodeToString(newSignature.getBytes());
  response.addHeader("Timestamp", timestamp);
  response.addHeader("NonceStr", nonceStr);
  response.addHeader("Signature", newSignature);
}

private static String buildReturnMessage(String timestamp, String nonceStr, String body) {
  return timestamp + "\n" + nonceStr + "\n" + body + "\n";
}

/**
 * 生成指定位数的随机数
 * @param length
 * @return
 */
public static String getRandomString(int length){
    String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    Random random=new Random();
    StringBuffer sb=new StringBuffer();
    for(int i=0;i<length;i++){
        int number=random.nextInt(str.length());
        sb.append(str.charAt(number));
    }
    return sb.toString();
}

作者刚接触java不久,很多地方还有待提高,欢迎各位同行大拿交流指正

如需转载,请联系作者本人