前言
对外开放的接口,需要验证请求方发送过来的数据确实是由发送方发起的,并且中途不能被篡改和伪造,所以才会对接口的访问进行签名验证,以保证双方获取到的原来的信息是没有经过篡改的。
实现方法
对请求的信息内容,通过MD5运算或者其他算法(必须是不可逆的签名算法)生成签名标识,后端拿到请求的信息内容,经过同样的算法得到签名标识,比对和接收到的签名一致,则验证为真
签名规则
这里我自己只用到了timestamp加入签名,实际开发种可能会用到appId、pwd等参数,按实际需求加入到规则中即可,签名规则根据实际变更,此处只作参考
1. 所有的参数按参数名升序排序
2. 按参数名及参数值互相连接组成一个请求参数串(paramStr),格式如下:
body内容#createName=default#createUser=10000#reportUser=张三#state=0#
注:参数有3种,body直接在后面拼接#,前2种按参数名及参数值互相连接,拼接顺序:body#queryParams#pathUrl#,每个参数后面以#结束
①URL占位符中的参数
②QueryParams参数,普通请求参数
③body参数
3. 将secretKey(服务器密钥:ee4xxxxxxxxxxxx3e)和timestamp,拼接到请求参数串的头部,得到签名字符串(signStr)
secretKey="+secretKey#timestamp="+timestamp+"#paramStr+"
4. 将signStr通过MD5加密,得到签名字符串(sign)
5. timestamp 、sign 放到 Headers中,与其他接口请求参数一起发送给服务端
步骤1:自定义签名验证注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME )
public @interface CheckSign {
}
步骤2:自定义验证签名切面AOP,实现签名验证
@Aspect //定义一个切面
@Configuration
@Slf4j
public class CheckSignAspect {
@Value("${sign.expireTime}")
private long expireTime;//接口签名验证超时时间
@Value("${sign.secretKey}")
private String secretKey;//接口签名唯一密钥
// 定义切点Pointcut
@Pointcut("@annotation(com.jxzx.verify.common.annotation.CheckSign)")
public void excudeService() {
}
@Around("excudeService()")
public Object doAround(ProceedingJoinPoint joinPoint) {
log.info("开始验证签名");
try {
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = Objects.requireNonNull(sra).getRequest();
String timestamp = request.getHeader("timestamp");//获取timestamp参数
String sign = request.getHeader("sign");//获取sign参数
if (StrUtil.isBlank(timestamp) || StrUtil.isBlank(sign)) {
return RestResult.failed("timestamp和sign参数不能为空");
}
long requestTime = Long.valueOf(timestamp);
long now = System.currentTimeMillis() / 1000;
log.info("now={}", now);
// 请求发起时间与当前时间超过expireTime,则接口请求过期
if (now - requestTime > expireTime) {
return RestResult.failed("接口请求过期");
}
String generatedSign = generatedSignature(request, timestamp);
if (!generatedSign.equals(sign)) {
return RestResult.failed("签名校验错误");
}
Object result = joinPoint.proceed();
return result;
} catch (Throwable t) {
return RestResult.failed("签名校验异常");
}
}
//获取请求参数并生成签名
private String generatedSignature(HttpServletRequest request, String timestamp) {
//获取RequestBody参数,此处需要配合过滤器处理request后才能获取
String bodyParam = null;
if (request instanceof ContentCachingRequestWrapper) {
bodyParam = new String(((ContentCachingRequestWrapper) request).getContentAsByteArray(), StandardCharsets.UTF_8);
}
//获取RequestParam参数
Map<String, String[]> requestParameterMap = request.getParameterMap();
//获取PathVariable参数
ServletWebRequest webRequest = new ServletWebRequest(request, null);
Map<String, String> requestPathMap = (Map<String, String>) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
return SignUtil.sign(bodyParam, requestParameterMap, requestPathMap, secretKey, timestamp);
}
}
注意:
获取RequestBody参数,@RequestBody读取参数主要是通过request中的ServletInputStream传输,SpirngMvc通过@RequestBody读取流中的数据封装到对象中。因为stream只能被读取一次,如果这里我们通过request读取,后面的SpringMvc就读取不到了,因此通过Filter对request进行处理。文章来源:https://www.toymoban.com/news/detail-729797.html
新增一个过滤器
public class RequestCachingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
boolean isFirstRequest = !isAsyncDispatch(request);
HttpServletRequest requestWrapper = request;
if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
requestWrapper = new ContentCachingRequestWrapper(request);
}
try {
filterChain.doFilter(requestWrapper, response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
配置过滤器
@Configuration
public class FilterConfig {
@Bean
public RequestCachingFilter requestCachingFilter() {
return new RequestCachingFilter();
}
@Bean
public FilterRegistrationBean requestCachingFilterRegistration(
RequestCachingFilter requestCachingFilter) {
FilterRegistrationBean bean = new FilterRegistrationBean(requestCachingFilter);
bean.setOrder(1);
return bean;
}
}
生成签名的工具类
public class SignUtil {
/**
* 使用 Map按key进行排序
*
* @param map
* @return
*/
public static Map<String, String> sortMapByKey(Map<String, String> map) {
if (map == null || map.isEmpty()) {
return null;
}
//升序排序
Map<String, String> sortMap = new TreeMap<>(String::compareTo);
sortMap.putAll(map);
return sortMap;
}
public static String sign(String body, Map<String, String[]> params, Map<String, String> requestPathMap, String secretKey, String timestamp) {
StringBuilder sb = new StringBuilder();
if (CharSequenceUtil.isNotBlank(body)) {
sb.append(body).append('#');
}
if (!CollectionUtils.isEmpty(params)) {
params.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.forEach(paramEntry -> {
String paramValue = String.join(",", Arrays.stream(paramEntry.getValue()).sorted().toArray(String[]::new));
sb.append(paramEntry.getKey()).append("=").append(paramValue).append('#');
});
}
if (ArrayUtil.isNotEmpty(requestPathMap)) {
for (String key : requestPathMap.keySet()) {
String value = requestPathMap.get(key);
sb.append(key).append("=").append(value).append('#');
}
}
return SecureUtil.md5(String.join("#", secretKey, timestamp, sb.toString()));
}
}
步骤3:使用
在我们需要验证签名的controller上加上 @CheckSign 即可文章来源地址https://www.toymoban.com/news/detail-729797.html
@ApiOperation(value = "验证签名")
@PostMapping("/checkSign")
@CheckSign
public RestResult postTestPdf(
@PathVariable("name") String name,
@PathVariable("age") String age,
@ApiParam(value = "搜索条件", required = true) String orderId,
@ApiParam(value = "搜索条件1", required = true) String orderNo,
@RequestBody(required = true) @Validated JSONArray jsonArray
) {
return RestResult.success(jsonArray);
}
到了这里,关于三方开放接口,Springboot通过AOP实现API接口的签名验证的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!