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

这篇具有很好参考价值的文章主要介绍了超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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 {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(signInterceptor())
                .addPathPatterns("/openapi/**");//只拦截openapi前缀的接口
    }
	//交给spring管理 SignInterceptor bean 
	//不然下边 private OpenApiApplyMapper applyMapper;注入为null
    @Bean
    public 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]; // 生成的字节数要除以2
        random.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";//随机值


    @Resource
    private OpenApiApplyMapper applyMapper;


    @Override
    public 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的参数了,会报错

通过过滤器解决文章来源地址https://www.toymoban.com/news/detail-815826.html

@Component
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
@Order(10000)
public class HttpServletRequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public 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);
        }
    }

    @Override
    public void destroy() {

    }
}

到了这里,关于超详细!完整版!基于spring对外开放接口的签名认证方案(拦截器方式)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 如何实现服务器对外开放?路由器端口映射怎么设置?

    使用路由器后,Internet用户无法访问到局域网内的主机,因此不能访问内网搭建的Web、FTP、Mail等服务器。路由器端口映射功能可以实现将内网的服务器映射到Internet,从而实现服务器对外开放。路由器端口映射怎么设置? 下面给大家介绍一下具体设置步骤。 第一步:设置虚拟

    2024年02月16日
    浏览(54)
  • 腾讯云向量数据库正式对外全量开放公测

    11月1日,腾讯云对外宣布向量数据库正式全量开放公测,同时性能层面带来巨大提升。腾讯云数据库副总经理罗云表示,除了公测之外,腾讯云向量数据库单索引已经支持百亿级向量规模,支持百万级QPS毫秒级查询延迟,领先行业平均水平1.5倍以上,计算成本低于行业水平

    2024年02月06日
    浏览(46)
  • 面向WEB3.0提升存储性能,蚂蚁鲸探底层区块链存储引擎正式对外开放

    11月3日,蚂蚁集团数字科技在云栖大会上宣布,其历经4年的关键技术攻关与测试验证的区块链存储引擎LETUS(Log-structured Efficient Trusted Universal Storage),首次对外开放。该产品主要用于可信数据在区块链上的存储,LETUS今年在蚂蚁数字藏品平台“鲸探”成功应用,真实业务环境

    2024年02月12日
    浏览(45)
  • Linux系统firewalld防火墙的应用实操(对外端口开放使用,对内端口限制ip地址使用,不使用端口默认关闭)

    本文直接进行Linux系统firewalld防火墙的应用实操 对外端口开放使用 对内端口限制ip地址使用 不使用端口默认关闭 基础知识请查阅:Linux系统firewalld防火墙的基本操作 进阶知识请查阅:Linux系统firewalld防火墙的进阶操作(日志保存 IP网段 ssh服务) 应用实操请查阅:Linux系统f

    2024年02月05日
    浏览(65)
  • Spring Boot入门(23):基于AOP实现自定义注解拦截接口日志并保存入库 | 超级详细,建议收藏

            在上两期中,我们着重介绍了如何集成使用 Logback 与 log4j2 日志框架的使用,今天我们讲解的主题依旧跟日志有关,不过不是使用何种开源框架,而是自己动手造。         Spring的核心之一AOP;AOP翻译过来叫面向切面编程, 核心就是这个切面. 切面表示从业务逻辑中

    2024年02月11日
    浏览(49)
  • 如何保证对外接口的安全?

    1.什么是安全接口? 通常来说要将暴露在外网的 API 接口视为安全接口,需要实现防篡改和防重放的功能。 1.1 什么是篡改问题? 由于 HTTP 是一种无状态协议,服务端无法确定客户端发送的请求是否合法,也不了解请求中的参数是否正确。以一个充值接口为例: 如果非法用户

    2024年03月08日
    浏览(49)
  • 如何设计一个安全的对外接口 ?

    最近有个项目需要对外提供一个接口,提供公网域名进行访问,而且接口和交易订单有关,所以安全性很重要;这里整理了一下常用的一些安全措施以及具体如何去实现。 个人觉得安全措施大体来看主要在两个方面,一方面就是如何保证数据在传输过程中的安全性,另一个方

    2024年02月08日
    浏览(79)
  • 如何设计一个安全的对外接口

    非对称加密:服务端会生成一对密钥,私钥存放在服务器端,公钥可以发布给任何人使用;优点就是比起对称加密更加安全,但是加解密的速度比对称加密慢太多了;广泛使用的是RSA算法; 两种方式各有优缺点,而https的实现方式正好是结合了两种加密方式,整合了双方的优

    2024年04月16日
    浏览(61)
  • docker+jenkins+gitee+shell 自动化部署微服务(基于若依)【详细完整版】

    提示:需基本熟悉docker命令、shell脚本、微服务部署、git命令、服务器基本命令、node、maven 服务器环境:Linux-EulerOS(华为自主研发服务器系统,与CentOS基本类似) 安装组件:docker20.10.19、openjdk1.8.0_191、maven3.8.6、git2.33.0、node16.17.1、npm9.1.2、jenkinsci/blueocean、docker-compose 远程工

    2023年04月11日
    浏览(45)
  • 业务开发时,接口不能对外暴露怎么办?

    在业务开发的时候,经常会遇到某一个接口不能对外暴露,只能内网服务间调用的实际需求。面对这样的情况,我们该如何实现呢? 今天,我们就来理一理这个问题,从几个可行的方案中,挑选一个来实现。 推荐一个开源免费的 Spring Boot 实战项目: https://github.com/javastacks

    2024年02月12日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包