开发环境: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);
}
- 验签用到的方法,publicKey、privateKey 为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不久,很多地方还有待提高,欢迎各位同行大拿交流指正
如需转载,请联系作者本人