超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)

news/2024/11/22 4:54:11/

文章目录

  • 1、场景
  • 2、接口防御措施
  • 3、签名认证逻辑
  • 4、签名算法规则
  • 5、代码示例
    • 1、sign工具类
    • 2、定义拦截器
    • 3、生成accessKey、secretKey 工具类
    • 4、signInterceptor类
    • 5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错

1、场景

由于项目需要开发第三方接口给多个供应商,为保证Api接口的安全性,遂采用Api接口签名验证。

2、接口防御措施

  1. 请求发起时间得在限制范围内
  2. 请求的用户是否真实存在
  3. 是否存在重复请求
  4. 请求参数是否被篡改

3、签名认证逻辑

1、服务端生成一对 accessKey/secretKey密钥对,将 accessKey公开给客户端,将 secretKey 保密。

2、客户端使用 secretKey和一些请求参数(如时间戳、请求内容等),使用 MD5 算法生成签名。

3、客户端将 accessKey、签名和请求参数一起发送给服务端。

4、服务端使用 和收到的请求参数,使用 MD5 算法生成签名。

5、服务端比较客户端发来的签名和自己生成的签名是否相同,如果相同,则认为请求是可信的,否则认为请求是不可信的。
secretKey不进行网络传输,只用于本地MD5运算

4、签名算法规则

计算步骤
用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。
将<key, value>请求参数对按key进行字典升序排序,得到有序的参数对列表N
将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8
将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)
对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名

注意事项
不同接口要求的参数对不一样,计算签名使用的参数对也不一样
参数名区分大小写,参数值为空不参与签名
URL键值拼接过程value部分需要URL编码

5、代码示例

1、sign工具类

public class SignUtil {/*** 签名算法* 1. 计算步骤* 用于计算签名的参数在不同接口之间会有差异,但算法过程固定如下4个步骤。* 将<key, value>请求参数对按key进行字典升序排序,得到有序的参数对列表N* 将列表N中的参数对按URL键值对的格式拼接成字符串,得到字符串T(如:key1=value1&key2=value2),URL键值拼接过程value部分需要URL编码,URL编码算法用大写字母,例如%E8,而不是小写%e8* 将应用密钥以app_key为键名,组成URL键值拼接到字符串T末尾,得到字符串S(如:key1=value1&key2=value2&app_key=密钥)* 对字符串S进行MD5运算,将得到的MD5值所有字符转换成大写,得到接口请求签名* 2. 注意事项* 不同接口要求的参数对不一样,计算签名使用的参数对也不一样* 参数名区分大小写,参数值为空不参与签名* URL键值拼接过程value部分需要URL编码* @return 签名字符串*/private static String getSign(Map<String, Object> map, String secretKey) {List<Map.Entry<String, Object>> infoIds = new ArrayList<>(map.entrySet());Collections.sort(infoIds, new Comparator<Map.Entry<String, Object>>() {public int compare(Map.Entry<String, Object> arg0, Map.Entry<String, Object> arg1) {return (arg0.getKey()).compareTo(arg1.getKey());}});StringBuffer sb = new StringBuffer();for (Map.Entry<String, Object> m : infoIds) {if(null == m.getValue() || StringUtils.isNotBlank(m.getValue().toString())){sb.append(m.getKey()).append("=").append(URLUtil.encodeAll(m.getValue().toString())).append("&");}}sb.append("secret-key=").append(secretKey);return MD5.create().digestHex(sb.toString()).toUpperCase();}//获取随机值private static String getNonceStr(int length){//生成随机字符串String str="zxcvbnmlkjhgfdsaqwertyuiopQWERTYUIOPASDFGHJKLZXCVBNM1234567890";Random random=new Random();StringBuffer randomStr=new StringBuffer();// 设置生成字符串的长度,用于循环for(int i=0; i<length; ++i){//从62个的数字或字母中选择int number=random.nextInt(62);//将产生的数字通过length次承载到sb中randomStr.append(str.charAt(number));}return randomStr.toString();}//签名验证方法public static boolean signValidate(Map<String, Object> map,String secretKey,String sign){String mySign = getSign(map,secretKey);return mySign.equals(sign);}
}

2、定义拦截器

@Configuration
public class SignInterceptorConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(signInterceptor()).addPathPatterns("/openapi/**");//只拦截openapi前缀的接口}//交给spring管理 SignInterceptor bean //不然下边 private OpenApiApplyMapper applyMapper;注入为null@Beanpublic SignInterceptor signInterceptor(){return new SignInterceptor();}}

3、生成accessKey、secretKey 工具类

public class KeyGenerator {private static final int KEY_LENGTH = 32; // 指定生成的key长度为32字节public static String generateAccessKey() {SecureRandom random = new SecureRandom();byte[] bytes = new byte[KEY_LENGTH / 2]; // 生成的字节数要除以2random.nextBytes(bytes);return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 20);}public static String generateSecretKey() {SecureRandom random = new SecureRandom();byte[] bytes = new byte[KEY_LENGTH];random.nextBytes(bytes);return Base64.getEncoder().encodeToString(bytes).replace("/", "").replace("+", "").substring(0, 40);}
}

4、signInterceptor类

public class SignInterceptor implements HandlerInterceptor {private static final String ACCESSKEY = "access-key";//调用者身份唯一标识private static final String TIMESTAMP = "time-stamp";//时间戳private static final String SIGN = "sign";//签名private static final String NONCE = "nonce";//随机值@Resourceprivate OpenApiApplyMapper applyMapper;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if(checkSign(request, response)){//签名认证return HandlerInterceptor.super.preHandle(request, response, handler);}return false;}/*** 验证签名* @param request* @param response* @return* @throws Exception*/private boolean checkSign(HttpServletRequest request,HttpServletResponse response)throws Exception {response.setContentType("application/json");response.setCharacterEncoding("utf8");String ip = IPUtils.getIpAddr(request);FzyLogUtil.infoSafe("开放接口", "访问时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL());String accessKey = request.getHeader(ACCESSKEY);String timestamp = request.getHeader(TIMESTAMP);String nonce = request.getHeader(NONCE);String sign = request.getHeader(SIGN);if (!StringUtils.isNotBlank(accessKey)) {response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey无效")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey无效");return false;}if (StringUtils.isBlank(sign)) {response.getWriter().write(JSON.toJSONString(ResultUtil.fail("签名无效")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:签名无效");return false;}OpenApiDetailDO openApiDetailDO = applyMapper.selectOneByAccessKey(accessKey);if (openApiDetailDO == null) {response.getWriter().write(JSON.toJSONString(ResultUtil.fail("accessKey不存在")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:accessKey不存在");return false;}if (StringUtils.isNotBlank(openApiDetailDO.getBlackList())) {for (String bIp : openApiDetailDO.getBlackList().split(",")) {if (bIp.equals(ip)) {//黑名单response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:黑名单拒绝请求");return false;}}}if (StringUtils.isNotBlank(openApiDetailDO.getWhiteList())) {boolean flag = false;for (String bIp : openApiDetailDO.getWhiteList().split(",")) {if (bIp.equals(ip)) {//白名单flag = true;break;}}if(!flag){response.getWriter().write(JSON.toJSONString(ResultUtil.fail("拒绝请求")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:白名单未符合拒绝请求");return false;}}if ("0".equals(openApiDetailDO.getInvokeStatus() + "")) {response.getWriter().write(JSON.toJSONString(ResultUtil.fail("访问权限已被冻结")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:访问权限已被冻结");return false;}if (!"1".equals(openApiDetailDO.getApiStatus() + "")) {response.getWriter().write(JSON.toJSONString(ResultUtil.fail("接口异常,暂停访问")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:接口异常,暂停访问");return false;}if (!StringUtils.isNotBlank(timestamp)) {response.getWriter().write(JSON.toJSONString(ResultUtil.fail("时间戳无效")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:时间戳无效");return false;} else if (openApiDetailDO.getTimeOut() != null) {if (System.currentTimeMillis() - Long.valueOf(timestamp) > openApiDetailDO.getTimeOut() * 1000) {response.getWriter().write(JSON.toJSONString(ResultUtil.fail("请求已过期")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:请求已过期");return false;};}Map<String, Object> hashMap = new HashMap<>();String queryStrings = request.getQueryString();//获取url后边拼接的参数if (queryStrings != null) {for (String queryString : queryStrings.split("&")) {String[] param = queryString.split("=");if (param.length == 2) {hashMap.put(param[0], param[1]);}}}hashMap.put(ACCESSKEY, accessKey);hashMap.put(TIMESTAMP, timestamp);if (StringUtils.isNotBlank(nonce)) {hashMap.put(NONCE, nonce);}String secretKey = openApiDetailDO.getSecretKey();String body = new RequestWrapper(request).getBody();if (StringUtils.isNotBlank(body)) {Map<String, Object> map = JSON.parseObject(body);if (map != null) {hashMap.putAll(map);}}if (!SignUtil.signValidate(hashMap, secretKey, sign)) {//认证失败response.getWriter().write(JSON.toJSONString(ResultUtil.fail("认证失败")));FzyLogUtil.errorSafe("开放接口请求失败", "时间:" + LocalDateTime.now() + ",IP:" + ip + ",访问接口:" + request.getRequestURL() + "错误信息:认证失败");return false;}return true;}
}

5、SignInterceptor 获取body里参数后,接口的controller会获取不到body的参数了,会报错

通过过滤器解决

@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
@Order(10000)
public class HttpServletRequestFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;String contentType = request.getContentType();String method = "multipart/form-data";if (contentType != null && contentType.contains(method)) {// 将转化后的 request 放入过滤链中request = new StandardServletMultipartResolver().resolveMultipart(request);}request = new RequestWrapper((HttpServletRequest) servletRequest);//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中// 在chain.doFiler方法中传递新的request对象if(null == request) {filterChain.doFilter(servletRequest, servletResponse);} else {filterChain.doFilter(request, servletResponse);}}@Overridepublic void destroy() {}
}

http://www.ppmy.cn/news/581435.html

相关文章

Linux系统编程(终端和进程的关系)

文章目录 前言一、终端和控制台二、TTY和PTY三、终端的类型四、Gnome Terminal伪终端总结 前言 本篇文章带大家学习终端和进程的关系&#xff0c;终端相信大家都听过&#xff0c;那么真的理解终端是什么吗&#xff1f;应该有很多同学对于终端只是有一个模糊的概念。那么这篇文…

3、数仓之采集工具MaxWell(MaxWell简介、MaxWell原理、MaxWell部署、MaxWell使用)

1、Maxw简介 1.1 MaxWell概述 Maxwell 是由美国Zendesk公司开源&#xff0c;用Java编写的MySQL变更数据抓取软件。它会实时监控Mysql数据库的数据变更操作&#xff08;包括insert、update、delete&#xff09;&#xff0c;并将变更数据以 JSON 格式发送给 Kafka、Kinesi等流数…

AMEYA360报道:智能燃气表市场正快速发展

燃气表是计量燃气能源的重要器具&#xff0c;智能燃气表是在燃气基表上加入智能模块&#xff0c;可实现计量数据传输、远程控制等功能的特殊燃气表。它采用物联网技术&#xff0c;将燃气表的测量数据传输到相应的控制中心&#xff0c;从而使燃气供应更加安全、高效、智能化。 国…

【正点原子STM32连载】第四十章 红外遥控实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html# 第四…

Java-定时任务

文章目录 补充&#xff1a;cron表达式基本知识方式一&#xff1a;使用sleep方法方式二&#xff1a;JDK Timer和TimerTask方式三&#xff1a;JDK ScheduledExecutorService方式四&#xff1a; Spring Task 中 的 Scheduler方法五、Quartz框架方式六&#xff1a;XXL-JOB将xxl-job…

打印机卡纸后不能用

打印机卡纸复原后&#xff0c;不能用&#xff0c;有可能是打印机的驱动掉了&#xff0c;不能响应打印机了。&#xff0c; 点击控制面板-》硬件和声音-》设备和打印机&#xff0c; 删除当前型号&#xff0c;再次插上打印机&#xff0c;就可以看到当前打印机&#xff0c;设置为…

添加网络打印机,进入网络共享,弹出“登录失败: 禁用当前账户”的解决办法

方法1&#xff1a;打开【管理您的凭据】 查看【Windows凭据】&#xff0c;如果有&#xff0c;删掉 方法2&#xff1a;【计算机管理】—【本地用户和组】—【用户】&#xff0c;选中【Administrator】&#xff0c;右键属性&#xff0c;取消【账户已禁用】 方法3&#xff1a;如果…

得实Dascom AR-430K 打印机驱动

得实Dascom AR-430K 打印机驱动是官方提供的一款打印机驱动&#xff0c;本站收集提供高速下载&#xff0c;用于解决打印机与电脑连接不了&#xff0c;无法正常使用的问题&#xff0c;本动适用于&#xff1a;Windows XP / Windows 7 / Windows 8 / Windows 10 32/64位操作系统。…