最详细的SpringBoot实现接口校验签名调用

这篇具有很好参考价值的文章主要介绍了最详细的SpringBoot实现接口校验签名调用。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

代码地址:GitHub - passerbyYSQ/DemoRepository: 各种开发小demo

目录

概念

开放接口

验签

接口验签调用流程

1. 约定签名算法

2. 颁发非对称密钥对

3. 生成请求参数签名

4. 请求携带签名调用

代码设计

1. 签名配置类

2. 签名管理类

3. 自定义验签注解

4. AOP实现验签逻辑

5. 解决请求体只能读取一次

6. 自定义工具类

概念

开放接口

        开放接口是指不需要登录凭证就允许被第三方系统调用的接口。为了防止开放接口被恶意调用,开放接口一般都需要验签才能被调用。提供开放接口的系统下面统一简称为"原系统"。

验签

      验签是指第三方系统在调用接口之前,需要按照原系统的规则根据所有请求参数生成一个签名(字符串),在调用接口时携带该签名。原系统会验证签名的有效性,只有签名验证有效才能正常调用接口,否则请求会被驳回。

接口验签调用流程

1. 约定签名算法

        第三方系统作为调用方,需要与原系统协商约定签名算法(下面以SHA256withRSA签名算法为例)。同时约定一个名称(callerID),以便在原系统中来唯一标识调用方系统。

2. 颁发非对称密钥对

        签名算法约定后之后,原系统会为每一个调用方系统专门生成一个专属的非对称密钥对(RSA密钥对)。私钥颁发给调用方系统,公钥由原系统持有。注意,调用方系统需要保管好私钥(存到调用方系统的后端)。因为对于原系统而言,调用方系统是消息的发送方,其持有的私钥唯一标识了它的身份是原系统受信任的调用方。调用方系统的私钥一旦泄露,调用方对原系统毫无信任可言。

3. 生成请求参数签名

        签名算法约定后之后,生成签名的原理如下(活动图)。为了确保生成签名的处理细节与原系统的验签逻辑是匹配的,原系统一般都提供jar包或者代码片段给调用方来生成签名,否则可能会因为一些处理细节不一致导致生成的签名是无效的。

springboot接口加签验签,# SpringBoot,spring boot,后端,java,签名,验签,RSA,加密

4. 请求携带签名调用

路径参数中放入约定好的callerID,请求头中放入调用方自己生成的签名

代码设计

1. 签名配置类

相关的自定义yml配置如下。RSA的公钥和私钥可以使用hutool的SecureUtil工具类来生成,注意公钥和私钥是base64编码后的字符串

springboot接口加签验签,# SpringBoot,spring boot,后端,java,签名,验签,RSA,加密

定义一个配置类来存储上述相关的自定义yml配置

import cn.hutool.crypto.asymmetric.SignAlgorithm;
import lombok.Data;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 签名的相关配置
 */
@Data
@ConditionalOnProperty(value = "secure.signature.enable", havingValue = "true")  // 根据条件注入bean
@Component
@ConfigurationProperties("secure.signature")
public class SignatureProps {
    private Boolean enable;
    private Map<String, KeyPairProps> keyPair;

    @Data
    public static class KeyPairProps {
        private SignAlgorithm algorithm;
        private String publicKeyPath;
        private String publicKey;
        private String privateKeyPath;
        private String privateKey;
    }
}

2. 签名管理类

定义一个管理类,持有上述配置,并暴露生成签名和校验签名的方法。

注意,生成的签名是将字节数组进行十六进制编码后的字符串,验签时需要将签名字符串进行十六进制解码成字节数组

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.asymmetric.Sign;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import top.ysqorz.signature.model.SignatureProps;

import java.nio.charset.StandardCharsets;

@ConditionalOnBean(SignatureProps.class)
@Component
public class SignatureManager {
    private final SignatureProps signatureProps;

    public SignatureManager(SignatureProps signatureProps) {
        this.signatureProps = signatureProps;
        loadKeyPairByPath();
    }

    /**
     * 验签。验证不通过可能抛出运行时异常CryptoException
     *
     * @param callerID  调用方的唯一标识
     * @param rawData   原数据
     * @param signature 待验证的签名(十六进制字符串)
     * @return 验证是否通过
     */
    public boolean verifySignature(String callerID, String rawData, String signature) {
        Sign sign = getSignByCallerID(callerID);
        if (ObjectUtils.isEmpty(sign)) {
            return false;
        }

        // 使用公钥验签
        return sign.verify(rawData.getBytes(StandardCharsets.UTF_8), HexUtil.decodeHex(signature));
    }

    /**
     * 生成签名
     *
     * @param callerID 调用方的唯一标识
     * @param rawData  原数据
     * @return 签名(十六进制字符串)
     */
    public String sign(String callerID, String rawData) {
        Sign sign = getSignByCallerID(callerID);
        if (ObjectUtils.isEmpty(sign)) {
            return null;
        }
        return sign.signHex(rawData);
    }

    public SignatureProps getSignatureProps() {
        return signatureProps;
    }

    public SignatureProps.KeyPairProps getKeyPairPropsByCallerID(String callerID) {
        return signatureProps.getKeyPair().get(callerID);
    }

    private Sign getSignByCallerID(String callerID) {
        SignatureProps.KeyPairProps keyPairProps = signatureProps.getKeyPair().get(callerID);
        if (ObjectUtils.isEmpty(keyPairProps)) {
            return null; // 无效的、不受信任的调用方
        }
        return SecureUtil.sign(keyPairProps.getAlgorithm(), keyPairProps.getPrivateKey(), keyPairProps.getPublicKey());
    }

    /**
     * 加载非对称密钥对
     */
    private void loadKeyPairByPath() {
        // 支持类路径配置,形如:classpath:secure/public.txt
        // 公钥和私钥都是base64编码后的字符串
        signatureProps.getKeyPair()
                .forEach((key, keyPairProps) -> {
                    // 如果配置了XxxKeyPath,则优先XxxKeyPath
                    keyPairProps.setPublicKey(loadKeyByPath(keyPairProps.getPublicKeyPath()));
                    keyPairProps.setPrivateKey(loadKeyByPath(keyPairProps.getPrivateKeyPath()));
                    if (ObjectUtils.isEmpty(keyPairProps.getPublicKey()) ||
                            ObjectUtils.isEmpty(keyPairProps.getPrivateKey())) {
                        throw new RuntimeException("No public and private key files configured");
                    }
                });
    }

    private String loadKeyByPath(String path) {
        if (ObjectUtils.isEmpty(path)) {
            return null;
        }
        return IoUtil.readUtf8(ResourceUtil.getStream(path));
    }
}

3. 自定义验签注解

有些接口需要验签,但有些接口并不需要,为了灵活控制哪些接口需要验签,自定义一个验签注解

import java.lang.annotation.*;

/**
 * 该注解标注于Controller类的方法上,表明该请求的参数需要校验签名
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface VerifySignature {
}

4. AOP实现验签逻辑

        验签逻辑不能放在拦截器中,因为拦截器中不能直接读取body的输入流,否则会造成后续@RequestBody的参数解析器读取不到body。
        由于body输入流只能读取一次,因此需要使用ContentCachingRequestWrapper包装请求,缓存body内容(见第5点),但是该类的缓存时机是在@RequestBody的参数解析器中。
        因此,满足2个条件才能获取到ContentCachingRequestWrapper中的body缓存:

  • 接口的入参必须存在@RequestBody
  • 读取body缓存的时机必须在@RequestBody的参数解析之后,比如说:AOP、Controller层的逻辑内。注意拦截器的时机是在参数解析之前的

        综上,注意,标注了@VerifySignature注解的controlle层方法的入参必须存在@RequestBody,AOP中验签时才能获取到body的缓存!

import cn.hutool.crypto.CryptoException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.util.ContentCachingRequestWrapper;
import top.ysqorz.common.constant.BaseConstant;
import top.ysqorz.config.SpringContextHolder;
import top.ysqorz.config.aspect.PointCutDef;
import top.ysqorz.exception.auth.AuthorizationException;
import top.ysqorz.exception.param.ParamInvalidException;
import top.ysqorz.signature.model.SignStatusCode;
import top.ysqorz.signature.model.SignatureProps;
import top.ysqorz.signature.util.CommonUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.Map;

@ConditionalOnBean(SignatureProps.class)
@Component
@Slf4j
@Aspect
public class RequestSignatureAspect implements PointCutDef {
    @Resource
    private SignatureManager signatureManager;

    @Pointcut("@annotation(top.ysqorz.signature.enumeration.VerifySignature)")
    public void annotatedMethod() {
    }

    @Pointcut("@within(top.ysqorz.signature.enumeration.VerifySignature)")
    public void annotatedClass() {
    }

    @Before("apiMethod() && (annotatedMethod() || annotatedClass())")
    public void verifySignature() {
        HttpServletRequest request = SpringContextHolder.getRequest();

        String callerID = request.getParameter(BaseConstant.PARAM_CALLER_ID);
        if (ObjectUtils.isEmpty(callerID)) {
            throw new AuthorizationException(SignStatusCode.UNTRUSTED_CALLER); // 不受信任的调用方
        }

        // 从请求头中提取签名,不存在直接驳回
        String signature = request.getHeader(BaseConstant.X_REQUEST_SIGNATURE);
        if (ObjectUtils.isEmpty(signature)) {
            throw new ParamInvalidException(SignStatusCode.REQUEST_SIGNATURE_INVALID); // 无效签名
        }

        // 提取请求参数
        String requestParamsStr = extractRequestParams(request);
        // 验签。验签不通过抛出业务异常
        verifySignature(callerID, requestParamsStr, signature);
    }

    @SuppressWarnings("unchecked")
    public String extractRequestParams(HttpServletRequest request) {
        // @RequestBody
        String body = null;
        // 验签逻辑不能放在拦截器中,因为拦截器中不能直接读取body的输入流,否则会造成后续@RequestBody的参数解析器读取不到body
        // 由于body输入流只能读取一次,因此需要使用ContentCachingRequestWrapper包装请求,缓存body内容,但是该类的缓存时机是在@RequestBody的参数解析器中
        // 因此满足2个条件才能使用ContentCachingRequestWrapper中的body缓存
        // 1. 接口的入参必须存在@RequestBody
        // 2. 读取body缓存的时机必须在@RequestBody的参数解析之后,比如说:AOP、Controller层的逻辑内。注意拦截器的时机是在参数解析之前的
        if (request instanceof ContentCachingRequestWrapper) {
            ContentCachingRequestWrapper requestWrapper = (ContentCachingRequestWrapper) request;
            body = new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
        }

        // @RequestParam
        Map<String, String[]> paramMap = request.getParameterMap();

        // @PathVariable
        ServletWebRequest webRequest = new ServletWebRequest(request, null);
        Map<String, String> uriTemplateVarNap = (Map<String, String>) webRequest.getAttribute(
                HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);

        return CommonUtils.extractRequestParams(body, paramMap, uriTemplateVarNap);
    }

    /**
     * 验证请求参数的签名
     */
    public void verifySignature(String callerID, String requestParamsStr, String signature) {
        try {
            boolean verified = signatureManager.verifySignature(callerID, requestParamsStr, signature);
            if (!verified) {
                throw new CryptoException("The signature verification result is false.");
            }
        } catch (Exception ex) {
            log.error("Failed to verify signature", ex);
            throw new AuthorizationException(SignStatusCode.REQUEST_SIGNATURE_INVALID); // 转换为业务异常抛出
        }
    }
}
import org.aspectj.lang.annotation.Pointcut;

public interface PointCutDef {
    @Pointcut("execution(public * top.ysqorz..controller.*.*(..))")
    default void controllerMethod() {
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    default void postMapping() {
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    default void getMapping() {
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
    default void putMapping() {
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
    default void deleteMapping() {
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    default void requestMapping() {
    }

    @Pointcut("controllerMethod() && (requestMapping() || postMapping() || getMapping() || putMapping() || deleteMapping())")
    default void apiMethod() {
    }
}

5. 解决请求体只能读取一次

        解决方案就是包装请求,缓存请求体。SpringBoot也提供了ContentCachingRequestWrapper来解决这个问题。但是第4点中也详细描述了,由于它的缓存时机,所以它的使用有限制条件。也可以参考网上的方案,自己实现一个请求的包装类来缓存请求体

import lombok.NonNull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import top.ysqorz.signature.model.SignatureProps;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@ConditionalOnBean(SignatureProps.class)
@Component
public class RequestCachingFilter extends OncePerRequestFilter {
    /**
     * This {@code doFilter} implementation stores a request attribute for
     * "already filtered", proceeding without filtering again if the
     * attribute is already there.
     *
     * @param request     request
     * @param response    response
     * @param filterChain filterChain
     * @see #getAlreadyFilteredAttributeName
     * @see #shouldNotFilter
     * @see #doFilterInternal
     */
    @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);
        }
        filterChain.doFilter(requestWrapper, response);
    }
}

注册过滤器文章来源地址https://www.toymoban.com/news/detail-696909.html

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.ysqorz.signature.model.SignatureProps;

@Configuration
public class FilterConfig {
    @ConditionalOnBean(SignatureProps.class)
    @Bean
    public FilterRegistrationBean<RequestCachingFilter> requestCachingFilterRegistration(
            RequestCachingFilter requestCachingFilter) {
        FilterRegistrationBean<RequestCachingFilter> bean = new FilterRegistrationBean<>(requestCachingFilter);
        bean.setOrder(1);
        return bean;
    }
}

6. 自定义工具类

import cn.hutool.core.util.StrUtil;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

public class CommonUtils {
    /**
     * 提取所有的请求参数,按照固定规则拼接成一个字符串
     *
     * @param body              post请求的请求体
     * @param paramMap          路径参数(QueryString)。形如:name=zhangsan&age=18&label=A&label=B
     * @param uriTemplateVarNap 路径变量(PathVariable)。形如:/{name}/{age}
     * @return 所有的请求参数按照固定规则拼接成的一个字符串
     */
    public static String extractRequestParams(@Nullable String body, @Nullable Map<String, String[]> paramMap,
                                              @Nullable Map<String, String> uriTemplateVarNap) {
        // body: { userID: "xxx" }

        // 路径参数
        // name=zhangsan&age=18&label=A&label=B
        // => ["name=zhangsan", "age=18", "label=A,B"]
        // => name=zhangsan&age=18&label=A,B
        String paramStr = null;
        if (!ObjectUtils.isEmpty(paramMap)) {
            paramStr = paramMap.entrySet().stream()
                    .sorted(Map.Entry.comparingByKey())
                    .map(entry -> {
                        // 拷贝一份按字典序升序排序
                        String[] sortedValue = Arrays.stream(entry.getValue()).sorted().toArray(String[]::new);
                        return entry.getKey() + "=" + joinStr(",", sortedValue);
                    })
                    .collect(Collectors.joining("&"));
        }

        // 路径变量
        // /{name}/{age} => /zhangsan/18 => zhangsan,18
        String uriVarStr = null;
        if (!ObjectUtils.isEmpty(uriTemplateVarNap)) {
            uriVarStr = joinStr(",", uriTemplateVarNap.values().stream().sorted().toArray(String[]::new));
        }

        // { userID: "xxx" }#name=zhangsan&age=18&label=A,B#zhangsan,18
        return joinStr("#", body, paramStr, uriVarStr);
    }

    /**
     * 使用指定分隔符,拼接字符串
     *
     * @param delimiter 分隔符
     * @param strs      需要拼接的多个字符串,可以为null
     * @return 拼接后的新字符串
     */
    public static String joinStr(String delimiter, @Nullable String... strs) {
        if (ObjectUtils.isEmpty(strs)) {
            return StrUtil.EMPTY;
        }
        StringBuilder sbd = new StringBuilder();
        for (int i = 0; i < strs.length; i++) {
            if (ObjectUtils.isEmpty(strs[i])) {
                continue;
            }
            sbd.append(strs[i].trim());
            if (!ObjectUtils.isEmpty(sbd) && i < strs.length - 1 && !ObjectUtils.isEmpty(strs[i + 1])) {
                sbd.append(delimiter);
            }
        }
        return sbd.toString();
    }
}

到了这里,关于最详细的SpringBoot实现接口校验签名调用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • SpringBoot调用第三方WebService接口的两种实现方式

    WebService接口的发布通常一般都是使用WSDL(web service descriptive language)文件的样式来发布的,该文档包含了请求的参数信息,返回的结果信息,我们需要根据WSDL文档的信息来编写相关的代码进行调用WebService接口。接下来我将采用常见的两种方式调用WebService接口。 目前我需要

    2024年02月12日
    浏览(69)
  • Springboot实现上传文件,并实现调用第三方接口post请求多文件上传文件

    项目过程中,经常会有和第三方接口打交道的过程,今天实现调用第三方上传文件的接口!! 通常拿到第三方的接口文档的时候,不是第一时间先写代码,而是详细阅读接口文档。若接口需要第三方提供的基本参数,例如signkey, secrect等,也可以是其他的,查看文档里是否提

    2024年02月16日
    浏览(48)
  • SpringBoot第13讲:SpringBoot接口如何参数校验国际化

    本文是SpringBoot第13讲,上文我们学习了如何对SpringBoot接口进行参数校验,但是如果需要有国际化的信息(比如返回校验结果有中英文),应该如何优雅处理呢? 软件的国际化 :软件开发时,要使它能同时应对世界不同地区和国家的访问,并针对不同地区和国家的访问,提供相

    2024年02月12日
    浏览(42)
  • java springboot 整合webSocket接入调用chatGPT3.5接口实现自由返回

    java springboot 中使用webSocket接入openAI接口调用chatGPT3.5接口实现自由返回 @Component @Anonymous @ServerEndpoint(“/websocket/{id}”) // 访问路径: ws://localhost:8080/websocket public class WebSocketServer { // try { // sendMessage(“WebSocket连接成功”); // } catch (Exception e) { // // } } /** * 发送消息 * @param message 要

    2024年02月14日
    浏览(59)
  • Forest-声明式HTTP客户端框架-集成到SpringBoot实现调用第三方restful api并实现接口数据转换

    声明式HTTP客户端API框架,让Java发送HTTP/HTTPS请求不再难。它比OkHttp和HttpClient更高层, 是封装调用第三方restful api client接口的好帮手,是retrofit和feign之外另一个选择。 通过在接口上声明注解的方式配置HTTP请求接口。 官网: Forest   代码地址: forest: 声明式HTTP客户端API框架,让

    2024年02月04日
    浏览(115)
  • SpringBoot 调用外部接口

    一个系统肯定少不了要和外部系统进行通信,所以就必须得访问外部接口。 本次演示的是使用的是 高德天气api接口 使用插件方式,比如自带的HttpClient,或者OkHttp,甚至是原生的HttpURLConnection 等等,这里以HttpClient为例。 1、封装工具类 简单封装的get请求 2、测试方法 3、结果

    2024年02月09日
    浏览(45)
  • 【SpringBoot18】SpringBoot 调用外部接口的三种方式

    SpringBoot不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程。在Spring-Boot项目开发中,存在着本模块的代码需要访问外面模块接口,或外部url链接的需求, 比如在apaas开发过程中需要封装接口在接口中调用apaas提供的接口(

    2023年04月11日
    浏览(77)
  • springboot @Async 异步调用接口处理数据

    @Async 异步背景 新增的数据需要分发给下游业务系统,由于下游业务系统状态未知,所以需要异步发送数据给下游业务系统。 系统生效按钮---controller新增--异步调用servcie---数据集成 在springboot框架中实现步骤 首先在启动类上加上 @EnableAsync 注解开启项目的异步调用功能,其次

    2024年02月16日
    浏览(42)
  • springboot使用restTemplate调用webservice接口

    1.首先确定wsdl的http地址,使用postman测试接口是否成功  在浏览器输入webservice地址可以找到相应的请求和响应示例。    如果postman返回了正确的数据,就说明测试成功! 2.接下来代码:

    2024年01月16日
    浏览(60)
  • SpringBoot 使用RestTemplate来调用https接口

    使用RestTemplate来访问第三方https接口,接口类型为POST,返回数据为JSON,需要忽略https证书,因此对RestTemplate 进行配置,通过HttpClient的请求工厂(HttpComponentsClientHttpRequestFactory)进行构建。HttpComponentsClientHttpRequestFactory使用一个信任所有站点的HttpClient就可以解决问题了。 中间

    2024年02月13日
    浏览(51)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包