最易于使用的java接口签名实现

2023-11-01 05:10

本文主要是介绍最易于使用的java接口签名实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 一、签名需要解决哪些安全问题?
  • 二、分析
    • 1.思路
  • 三、实现
    • 1.项目地址
    • 2.使用示例
    • 3.被调用方开发
    • 4.调用方开发
    • 4. 防止请求重复发送
  • 总结


前言

在Java项目中,提供对外接口是非常常见的。如何保证接口的安全性呢?本文介绍了使用签名方式来确保接口的安全性。


一、签名需要解决哪些安全问题?

签名机制主要解决以下安全问题:

验密功能:确保请求方和被请求方之间的通信数据是安全的。
抓包篡改请求:防止黑客通过抓包工具对请求数据进行篡改。
抓包重复发送请求:防止黑客通过抓包工具对请求数据进行重复发送。

二、分析

1.思路

在这里插入图片描述

三、实现

1.项目地址

被调用方代码github链接
被调用方代码gitee链接
调用方demo代码github链接
调用方demo代码gitee链接

2.使用示例

  • 配置yml文件
signature:secretGroup:- code: TEST_CODE1accessKey:test-key1: L5nqjXlcziKIDa6btest-key2: mSlUAzz5ff9ViP2H- code: TEST_CODE2accessKey:testKeyId21: testKeySecret21testKeyId22: testKeySecret22
  • 配置类
@Configuration
@EnableConfigurationProperties(SignatureProperties.class)
public class SignatureConfig {@Bean@ConditionalOnMissingBeanpublic RequestCachingFilter requestCachingFilter() {return new RequestCachingFilter();}@Bean@ConditionalOnMissingBeanpublic SignatureAspect signatureAspect(SignatureProperties signatureProperties) {return new SignatureAspect(signatureProperties);}
}
  • 在Controller方法中加入注解@Signature(“TEST_CODE1”)
@RestController
public class TestController {@PostMapping("testSignature/{id}")@Signature("TEST_CODE1")public String testSignature(@RequestBody UserEntity userEntity, @PathVariable("id") String id, @RequestParam String companyName) {System.out.println(userEntity);System.out.println("id:" + id);System.out.println("companyName:" + companyName);return "success";}@Datastatic class UserEntity {private String username;private Integer age;}
}

这样对接口的验签就生效了,非常简单吧,接下来看看怎么实现的

3.被调用方开发

  • 考虑到同一接口可能需要被多个用户调用,各用户使用不同的ID和SECRET,这里采用如下属性存储配置
@ConfigurationProperties(prefix = "signature")
@Data
public class SignatureProperties {private Set<AccessCodeEntity> secretGroup;@Datastatic public class AccessCodeEntity {private String code;private Map<String, String> accessKey;}
}
  • 编写自定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Signature {/*** 签名的配置代码*/@AliasFor("signatureCode")String value() default "";/*** 签名的配置代码*/@AliasFor("value")String signatureCode() default "";
}
  • 通过AOP切面拦截带有注解的请求
@Aspect
@Slf4j
public class SignatureAspect {@Around("execution(* com..controller..*.*(..)) " +"&& @annotation(signature) " +"&& (@annotation(org.springframework.web.bind.annotation.RequestMapping)" +"|| @annotation(org.springframework.web.bind.annotation.GetMapping)" +"|| @annotation(org.springframework.web.bind.annotation.PostMapping)" +"|| @annotation(org.springframework.web.bind.annotation.PutMapping)" +"|| @annotation(org.springframework.web.bind.annotation.DeleteMapping)" +"|| @annotation(org.springframework.web.bind.annotation.PatchMapping))")public Object doAround(ProceedingJoinPoint pjp, Signature signature) throws Throwable {// 验签,这里传入的是注解配置的属性值this.checkSign(StrUtil.isBlank(signature.signatureCode()) ? signature.value() : signature.signatureCode());return pjp.proceed();}
}
  • 接下来就是验签的实现拉
private void checkSign(String signatureCode) throws Exception {HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();String headAccessKeyId = request.getHeader(SignatureConstant.SIGNATURE_ACCESS_KEY_ID_KEY);String timestamp = request.getHeader(SignatureConstant.SIGNATURE_TIMESTAMP_KEY);String sign = request.getHeader(SignatureConstant.SIGNATURE_SIGN_KEY);// 系统中读取accessKeySecretMap<String, String> signatureAccessKeyMap = this.signatureAccessKeyGroupMap.get(signatureCode);// 系统中读取accessKeySecretString accessKeySecret = signatureAccessKeyMap.get(headAccessKeyId);if (StringUtils.isBlank(accessKeySecret)) {throw new SignException("验签失败,无效的accessKeyId");}// 校验签名的头信息是否合法checkAccessKeyHeaders(headAccessKeyId, signatureAccessKeyMap, timestamp, sign);//获取body(对应@RequestBody)String body = getBodyString(request);//获取parameters(对应@RequestParam)Map<String, String[]> params = getParamsMap(request);//获取path variable(对应@PathVariable)Collection<String> paths = getPaths(request);// 验证签名SignUtil.checkSign(body, params, paths, headAccessKeyId, accessKeySecret, Long.parseLong(timestamp), sign);
}
  • 首先读取到我们yml中的配置,这里做了一点转换,方便我们后续使用
    private final Map<String, Map<String, String>> signatureAccessKeyGroupMap;public SignatureAspect(SignatureProperties signatureProperties) {this.signatureAccessKeyGroupMap = signatureProperties.getSecretGroup().stream().collect(Collectors.toMap(SignatureProperties.AccessCodeEntity::getCode, SignatureProperties.AccessCodeEntity::getAccessKey));}
  • 签名读取请求头中的参数,同时通过上一步的signatureAccessKeyGroupMap,根据注解的配置读取到我们系统中保存的accessKeyId和accessKeySecret的所有组合
  • 校验请求头信息 checkAccessKeyHeaders方法,校验参数必须完整,请求时间合法
    /*** 请求过期时间 10分钟*/public static final int EXPIRE_TIME = 10 * 60 * 1000;/*** 服务器误差时间 2分钟*/public static final int ERROR_LIMIT = -2 * 60 * 1000;private void checkAccessKeyHeaders(String headAccessKeyId, Map<String, String> signatureAccessKeyMap, String timestamp, String sign) {if (StringUtils.isAnyBlank(headAccessKeyId, timestamp, sign)) {throw new SignException("未获取到完整签名信息");}if (signatureAccessKeyMap == null || !signatureAccessKeyMap.containsKey(headAccessKeyId)) {throw new SignException("验证失败,错误的accessKeyId:" + headAccessKeyId);}long timestampLongVal;try {timestampLongVal = Long.parseLong(timestamp);} catch (NumberFormatException e) {throw new SignException("不支持的时间戳格式");}long l = System.currentTimeMillis() - timestampLongVal;// 允许服务求误差2分钟if (l < ERROR_LIMIT || l >= EXPIRE_TIME) {throw new SignException("请求签名已过期");}}
  • 获取所有的请求参数,进行自定义规则的拼接。请求参数分为3类@RequestBody、@RequestParam、@PathVariable。其中@RequestParam、@PathVariable直接从request中读取,这里我就不再赘述了,我们主要看一下@RequestBody。
    private Collection<String> getPaths(HttpServletRequest request) {Collection<String> paths = null;ServletWebRequest webRequest = new ServletWebRequest(request, null);@SuppressWarnings("unchecked")Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);if (!CollectionUtils.isEmpty(uriTemplateVars)) {paths = uriTemplateVars.values();}return paths;}private Map<String, String[]> getParamsMap(HttpServletRequest request) {Map<String, String[]> params = null;if (!CollectionUtils.isEmpty(request.getParameterMap())) {params = request.getParameterMap();}return params;}
  • @RequestBody读取参数主要是通过request中的ServletInputStream传输,SpirngMvc通过@RequestBody读取流中的数据封装到对象中。因为stream只能被读取一次,如果这里我们通过request读取,后面的SpringMvc就读取不到了。所以这里对原生request做一下增强
@Slf4j
@Order(1)
public class RequestCachingFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, FilterChain filterChain) {try {HttpServletRequest customRequest = request;String contentType = request.getContentType();// 这里必须判断,否则上传文件时会出现异常if(StringUtils.isNotBlank(contentType)&&contentType.contains(MediaType.APPLICATION_JSON_VALUE)){customRequest = new BodyReaderRequestWrapper(request);}filterChain.doFilter(customRequest, response);} catch (IOException | ServletException e) {log.error("RequestCachingFilter异常:", e);printRequest(request);}}
}
  • 接下来看看BodyReaderRequestWrapper的实现。非常简单,构建时将流转为字节数组保存在本地。每次读取时重新封装一个Stream
public class BodyReaderRequestWrapper extends HttpServletRequestWrapper {private final byte[] bodyBuffer;public BodyReaderRequestWrapper(HttpServletRequest request) throws IOException {super(request);bodyBuffer = IoUtil.readBytes(request.getInputStream());}@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(this.getInputStream()));}@Overridepublic ServletInputStream getInputStream() {final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyBuffer);return new ServletInputStream() {@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}@Overridepublic int read() {return byteArrayInputStream.read();}};}
}
  • 接下来就是重中之重了,验签。
public static void checkSign(String body, Map<String, String[]> params, Collection<String> paths, String accessKeyId, String accessKeySecret, long timestamp, String sign) {String allParamsString = getAllParamsString(body, params, paths, accessKeyId, timestamp);String newSign = generatorSign(allParamsString, accessKeySecret);if (!StrUtil.equals(sign, newSign)) {throw new RuntimeException("签名验证失败");}
}
  • 先看getAllParamsString,就是把所有参数按照我们的顺序拼接起来,这里要注意顺序问题。必须保证调用方和我们是同样的参数顺序,所以数组的参数要排序一下
private static String getAllParamsString(String body, Map<String, String[]> params, Collection<String> paths, String accessKeyId, long timestamp) {StringBuilder sb = new StringBuilder();if (StrUtil.isNotBlank(body)) {sb.append(body).append('#');}if (CollectionUtil.isNotEmpty(params)) {params.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(paramEntry -> {String paramValue = Arrays.stream(paramEntry.getValue()).sorted().collect(Collectors.joining(","));sb.append(paramEntry.getKey()).append("=").append(paramValue).append('#');});}if (CollectionUtil.isNotEmpty(paths)) {String pathValues = String.join(",", paths);sb.append(pathValues).append('#');}// 拼接secret和时间戳sb.append("accessKeyId=").append(accessKeyId).append("#timestamp=").append(timestamp);return sb.toString();
}
  • 现在生成签名,我们这里用的HmacSha256算法.用的hutool的工具类,非常方便。
public static String generatorSign(String allParamsString, String accessKeySecret) {HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, accessKeySecret.getBytes(StandardCharsets.UTF_8));return hMac.digestHex(allParamsString);
}
  • 比较调用方传过来的签名完成验签了

4.调用方开发

  • 生成签名的方法和被调用方相同.这里就直接贴代码拉
public class TestClient {public static void main(String[] args) {
//        String accessKeyId = "test-key1";
//        String accessKeySecret = "L5nqjXlcziKIDa6b";String accessKeyId = "test-key2";String accessKeySecret = "mSlUAzz5ff9ViP2H";// @RequestBody 读取的JSON参数Map<String, Object> bodyMap = new HashMap<>();bodyMap.put("username", "张三");bodyMap.put("age", 18);String companyName = "一家很强的公司";String bodyJson = JSONUtil.toJsonStr(bodyMap);// @PathVariable 读取的参数int id = 3;// @RequestParam (问号拼接的参数)Map<String, String[]> params = new HashMap<>();params.put("companyName", new String[]{companyName});// 当前时间的时间戳long timestamp = System.currentTimeMillis();// 生成签名字符串String sign = SignUtil.generatorSign(bodyJson, params, CollectionUtil.newArrayList(String.valueOf(id)), accessKeyId, accessKeySecret, timestamp);// 请求urlString url = "http://localhost:8080/testSignature/" + id + "?companyName=" + companyName;HttpResponse httpResponse = HttpRequest.post(url).header("timestamp", String.valueOf(timestamp)).header("accessKeyId", accessKeyId).header("sign", sign).form("companyName",companyName).body(bodyJson).execute();System.out.println(httpResponse);}
}

4. 防止请求重复发送

上面我们没有开发防止重复发送的功能,但是实现也很简单。就是在我们请求头中增加一个随机字符串的参数,要求短时间内唯一即可。被调用方验签成功后,方法调用完成没有异常时缓存这个随机串,设置一定时间后过期。每次验签前校验一下缓存中是否已经存在这个随机串,如果存在说明是重复调用。后续我会在项目gitee上更新上防止重复调用的功能。

总结

验签功能最重要的是生成签名,和解决@RequestBody流读取后就没有的问题。如果对文章有什么疑问,欢迎评论区留言

这篇关于最易于使用的java接口签名实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/320695

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Linux join命令的使用及说明

《Linuxjoin命令的使用及说明》`join`命令用于在Linux中按字段将两个文件进行连接,类似于SQL的JOIN,它需要两个文件按用于匹配的字段排序,并且第一个文件的换行符必须是LF,`jo... 目录一. 基本语法二. 数据准备三. 指定文件的连接key四.-a输出指定文件的所有行五.-o指定输出

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

Linux jq命令的使用解读

《Linuxjq命令的使用解读》jq是一个强大的命令行工具,用于处理JSON数据,它可以用来查看、过滤、修改、格式化JSON数据,通过使用各种选项和过滤器,可以实现复杂的JSON处理任务... 目录一. 简介二. 选项2.1.2.2-c2.3-r2.4-R三. 字段提取3.1 普通字段3.2 数组字段四.

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Linux kill正在执行的后台任务 kill进程组使用详解

《Linuxkill正在执行的后台任务kill进程组使用详解》文章介绍了两个脚本的功能和区别,以及执行这些脚本时遇到的进程管理问题,通过查看进程树、使用`kill`命令和`lsof`命令,分析了子... 目录零. 用到的命令一. 待执行的脚本二. 执行含子进程的脚本,并kill2.1 进程查看2.2 遇到的

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node