0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

SpringBoot 接口签名算法代码设计

jf_ro2CN3Fa 来源:csdn 2023-11-07 15:11 次阅读

1概念

开放接口

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

验签

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

2接口验签调用流程

1. 约定签名算法

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

2. 颁发非对称密钥对

签名算法约定后之后,原系统会为每一个调用方系统专门生成一个专属的非对称密钥对(RSA密钥对)。私钥颁发给调用方系统,公钥由原系统持有。

注意,调用方系统需要保管好私钥(存到调用方系统的后端)。因为对于原系统而言,调用方系统是消息的发送方,其持有的私钥唯一标识了它的身份是原系统受信任的调用方。调用方系统的私钥一旦泄露,调用方对原系统毫无信任可言。

3. 生成请求参数签名

签名算法约定后之后,生成签名的原理如下(活动图)。

abc46c7e-7567-11ee-939d-92fbcf53809c.png

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

4. 请求携带签名调用

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

3代码设计

1. 签名配置类

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

abd6939a-7567-11ee-939d-92fbcf53809c.png

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

importcn.hutool.crypto.asymmetric.SignAlgorithm;
importlombok.Data;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
importorg.springframework.boot.context.properties.ConfigurationProperties;
importorg.springframework.stereotype.Component;

importjava.util.Map;

/**
*签名的相关配置
*/
@Data
@ConditionalOnProperty(value="secure.signature.enable",havingValue="true")//根据条件注入bean
@Component
@ConfigurationProperties("secure.signature")
publicclassSignatureProps{
privateBooleanenable;
privateMapkeyPair;

@Data
publicstaticclassKeyPairProps{
privateSignAlgorithmalgorithm;
privateStringpublicKeyPath;
privateStringpublicKey;
privateStringprivateKeyPath;
privateStringprivateKey;
}
}

2. 签名管理类

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

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

importcn.hutool.core.io.IoUtil;
importcn.hutool.core.io.resource.ResourceUtil;
importcn.hutool.core.util.HexUtil;
importcn.hutool.crypto.SecureUtil;
importcn.hutool.crypto.asymmetric.Sign;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnBean;
importorg.springframework.stereotype.Component;
importorg.springframework.util.ObjectUtils;
importtop.ysqorz.signature.model.SignatureProps;

importjava.nio.charset.StandardCharsets;

@ConditionalOnBean(SignatureProps.class)
@Component
publicclassSignatureManager{
privatefinalSignaturePropssignatureProps;

publicSignatureManager(SignaturePropssignatureProps){
this.signatureProps=signatureProps;
loadKeyPairByPath();
}

/**
*验签。验证不通过可能抛出运行时异常CryptoException
*
*@paramcallerID调用方的唯一标识
*@paramrawData原数据
*@paramsignature待验证的签名(十六进制字符串)
*@return验证是否通过
*/
publicbooleanverifySignature(StringcallerID,StringrawData,Stringsignature){
Signsign=getSignByCallerID(callerID);
if(ObjectUtils.isEmpty(sign)){
returnfalse;
}

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

/**
*生成签名
*
*@paramcallerID调用方的唯一标识
*@paramrawData原数据
*@return签名(十六进制字符串)
*/
publicStringsign(StringcallerID,StringrawData){
Signsign=getSignByCallerID(callerID);
if(ObjectUtils.isEmpty(sign)){
returnnull;
}
returnsign.signHex(rawData);
}

publicSignaturePropsgetSignatureProps(){
returnsignatureProps;
}

publicSignatureProps.KeyPairPropsgetKeyPairPropsByCallerID(StringcallerID){
returnsignatureProps.getKeyPair().get(callerID);
}

privateSigngetSignByCallerID(StringcallerID){
SignatureProps.KeyPairPropskeyPairProps=signatureProps.getKeyPair().get(callerID);
if(ObjectUtils.isEmpty(keyPairProps)){
returnnull;//无效的、不受信任的调用方
}
returnSecureUtil.sign(keyPairProps.getAlgorithm(),keyPairProps.getPrivateKey(),keyPairProps.getPublicKey());
}

/**
*加载非对称密钥对
*/
privatevoidloadKeyPairByPath(){
//支持类路径配置,形如: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())){
thrownewRuntimeException("Nopublicandprivatekeyfilesconfigured");
}
});
}

privateStringloadKeyByPath(Stringpath){
if(ObjectUtils.isEmpty(path)){
returnnull;
}
returnIoUtil.readUtf8(ResourceUtil.getStream(path));
}
}

3. 自定义验签注解

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

importjava.lang.annotation.*;


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

验签逻辑不能放在拦截器中,因为拦截器中不能直接读取body的输入流,否则会造成后续@RequestBody的参数解析器读取不到body。

由于body输入流只能读取一次,因此需要使用ContentCachingRequestWrapper包装请求,缓存body内容(见第5点),但是该类的缓存时机是在@RequestBody的参数解析器中。

因此,满足2个条件才能获取到ContentCachingRequestWrapper中的body缓存:

接口的入参必须存在@RequestBody

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

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

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

importjavax.annotation.Resource;
importjavax.servlet.http.HttpServletRequest;
importjava.nio.charset.StandardCharsets;
importjava.util.Map;

@ConditionalOnBean(SignatureProps.class)
@Component
@Slf4j
@Aspect
publicclassRequestSignatureAspectimplementsPointCutDef{
@Resource
privateSignatureManagersignatureManager;

@Pointcut("@annotation(top.ysqorz.signature.enumeration.VerifySignature)")
publicvoidannotatedMethod(){
}

@Pointcut("@within(top.ysqorz.signature.enumeration.VerifySignature)")
publicvoidannotatedClass(){
}

@Before("apiMethod()&&(annotatedMethod()||annotatedClass())")
publicvoidverifySignature(){
HttpServletRequestrequest=SpringContextHolder.getRequest();

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

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

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

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

//@RequestParam
MapparamMap=request.getParameterMap();

//@PathVariable
ServletWebRequestwebRequest=newServletWebRequest(request,null);
MapuriTemplateVarNap=(Map)webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE,RequestAttributes.SCOPE_REQUEST);

returnCommonUtils.extractRequestParams(body,paramMap,uriTemplateVarNap);
}

/**
*验证请求参数的签名
*/
publicvoidverifySignature(StringcallerID,StringrequestParamsStr,Stringsignature){
try{
booleanverified=signatureManager.verifySignature(callerID,requestParamsStr,signature);
if(!verified){
thrownewCryptoException("Thesignatureverificationresultisfalse.");
}
}catch(Exceptionex){
log.error("Failedtoverifysignature",ex);
thrownewAuthorizationException(SignStatusCode.REQUEST_SIGNATURE_INVALID);//转换为业务异常抛出
}
}
}
importorg.aspectj.lang.annotation.Pointcut;

publicinterfacePointCutDef{
@Pointcut("execution(public*top.ysqorz..controller.*.*(..))")
defaultvoidcontrollerMethod(){
}

@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
defaultvoidpostMapping(){
}

@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
defaultvoidgetMapping(){
}

@Pointcut("@annotation(org.springframework.web.bind.annotation.PutMapping)")
defaultvoidputMapping(){
}

@Pointcut("@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
defaultvoiddeleteMapping(){
}

@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
defaultvoidrequestMapping(){
}

@Pointcut("controllerMethod()&&(requestMapping()||postMapping()||getMapping()||putMapping()||deleteMapping())")
defaultvoidapiMethod(){
}
}

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

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

importlombok.NonNull;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnBean;
importorg.springframework.stereotype.Component;
importorg.springframework.web.filter.OncePerRequestFilter;
importorg.springframework.web.util.ContentCachingRequestWrapper;
importtop.ysqorz.signature.model.SignatureProps;

importjavax.servlet.FilterChain;
importjavax.servlet.ServletException;
importjavax.servlet.http.HttpServletRequest;
importjavax.servlet.http.HttpServletResponse;
importjava.io.IOException;

@ConditionalOnBean(SignatureProps.class)
@Component
publicclassRequestCachingFilterextendsOncePerRequestFilter{
/**
*This{@codedoFilter}implementationstoresarequestattributefor
*"alreadyfiltered",proceedingwithoutfilteringagainifthe
*attributeisalreadythere.
*
*@paramrequestrequest
*@paramresponseresponse
*@paramfilterChainfilterChain
*@see#getAlreadyFilteredAttributeName
*@see#shouldNotFilter
*@see#doFilterInternal
*/
@Override
protectedvoiddoFilterInternal(@NonNullHttpServletRequestrequest,@NonNullHttpServletResponseresponse,@NonNullFilterChainfilterChain)
throwsServletException,IOException{
booleanisFirstRequest=!isAsyncDispatch(request);
HttpServletRequestrequestWrapper=request;
if(isFirstRequest&&!(requestinstanceofContentCachingRequestWrapper)){
requestWrapper=newContentCachingRequestWrapper(request);
}
filterChain.doFilter(requestWrapper,response);
}
}

注册过滤器

importorg.springframework.boot.autoconfigure.condition.ConditionalOnBean;
importorg.springframework.boot.web.servlet.FilterRegistrationBean;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importtop.ysqorz.signature.model.SignatureProps;

@Configuration
publicclassFilterConfig{
@ConditionalOnBean(SignatureProps.class)
@Bean
publicFilterRegistrationBeanrequestCachingFilterRegistration(
RequestCachingFilterrequestCachingFilter){
FilterRegistrationBeanbean=newFilterRegistrationBean<>(requestCachingFilter);
bean.setOrder(1);
returnbean;
}
}

6. 自定义工具类

importcn.hutool.core.util.StrUtil;
importorg.springframework.lang.Nullable;
importorg.springframework.util.ObjectUtils;

importjava.util.Arrays;
importjava.util.Map;
importjava.util.stream.Collectors;

publicclassCommonUtils{
/**
*提取所有的请求参数,按照固定规则拼接成一个字符串
*
*@parambodypost请求的请求体
*@paramparamMap路径参数(QueryString)。形如:name=zhangsan&age=18&label=A&label=B
*@paramuriTemplateVarNap路径变量(PathVariable)。形如:/{name}/{age}
*@return所有的请求参数按照固定规则拼接成的一个字符串
*/
publicstaticStringextractRequestParams(@NullableStringbody,@NullableMapparamMap,
@NullableMapuriTemplateVarNap){
//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
StringparamStr=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);
returnentry.getKey()+"="+joinStr(",",sortedValue);
})
.collect(Collectors.joining("&"));
}

//路径变量
///{name}/{age}=>/zhangsan/18=>zhangsan,18
StringuriVarStr=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
returnjoinStr("#",body,paramStr,uriVarStr);
}

/**
*使用指定分隔符,拼接字符串
*
*@paramdelimiter分隔符
*@paramstrs需要拼接的多个字符串,可以为null
*@return拼接后的新字符串
*/
publicstaticStringjoinStr(Stringdelimiter,@NullableString...strs){
if(ObjectUtils.isEmpty(strs)){
returnStrUtil.EMPTY;
}
StringBuildersbd=newStringBuilder();
for(inti=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();
    }
}
编辑:黄飞
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 接口
    +关注

    关注

    33

    文章

    8422

    浏览量

    150668
  • 算法
    +关注

    关注

    23

    文章

    4585

    浏览量

    92443
  • SpringBoot
    +关注

    关注

    0

    文章

    173

    浏览量

    157

原文标题:SpringBoot 接口签名校验实践

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    基于椭圆曲线算法的数字签名技术研究

    基于椭圆曲线算法的数字签名技术的基本原理及其安全性,展望了公钥密码体制未来的发展方向。【关键词】:椭圆曲线算法;;数字签名;;网络安全【DOI】:CNKI:SUN:GSKJ.0.201
    发表于 04-23 11:29

    SpringBoot知识总结

    SpringBoot干货学习总结
    发表于 08-01 10:40

    怎样去使用springboot

    怎样去使用springboot呢?学习springboot需要懂得哪些?
    发表于 10-25 07:13

    在哪里可以找到用于导出AN10957上显示的结果的确切CMAC签名/mac代码算法

    得到与 AN10957 不匹配的结果 (mac)。有谁知道我在哪里可以找到用于导出 AN10957 上显示的结果的确切 CMAC 签名/mac 代码算法
    发表于 04-06 06:25

    什么是数字签名算法(DSA)

    什么是数字签名算法(DSA) DSA(Digital Signature Algorithm,数字签名算法,用作数字签名标准的一部分),它
    发表于 04-03 16:01 3504次阅读

    一种错误签名混合筛选算法

    针对分级身份密码( HIBC)批验签过程中的错误签名快速识别问题,设计实现了一种错误签名混合筛选算法。针对HIBC签名算法不完全聚合的特点,
    发表于 12-07 15:36 0次下载

    如何使用ECDSA算法生成数字签名

    。 区块链中的数字签名 ECDSA算法 从A点到B点在椭圆曲线上的切线 根据wiki ECDSA为: 椭圆曲线密码体制是一种基于有限域椭圆曲线代数结构的公钥密码体制。与非对称密码学相比
    发表于 12-27 14:12 9096次阅读
    如何使用ECDSA<b class='flag-5'>算法</b>生成数字<b class='flag-5'>签名</b>

    MuSig签名方案可替代当前比特币的ECDSA签名算法

    当前,比特币和其他区块链普遍采用的是ECDSA签名验证算法。这显然是中本聪在2008年根据当时广泛使用和未授权的数字签名系统所做出的技术决定。然而,ECDSA签名存在一些严重的技术限制
    发表于 02-20 13:34 1554次阅读

    Schnorr签名和ECDSA签名技术介绍

    Schnorr签名是一个使BCH区块链实现技术领先的强大功能,因为Schnorr签名方案直接促进了BCH的隐私性和交易能力。Schnorr签名算法是由著名的密码学家Claus Schn
    发表于 05-16 10:32 2774次阅读

    schnorr签名算法相比ECDSA具有哪些优势

    schnorr 签名算法相比 ECDSA 来讲,对于上述的优点,除了尚未标准化之外几乎没有缺点。而且由于两种算法都基于同一个椭圆曲线,整个关于签名的升级成本也是很低的。
    发表于 08-08 11:22 3400次阅读

    基于ECDSA原理的FISCO BCOS交易签名算法解析

    FISCO BCOS交易签名算法基于ECDSA原理进行设计,ECDSA也是比特币和以太坊采用的交易签名算法
    发表于 02-19 16:46 1846次阅读
    基于ECDSA原理的FISCO BCOS交易<b class='flag-5'>签名</b><b class='flag-5'>算法</b>解析

    数据签名的双向签名和重签名的原理和资料简介

    什么是数据签名代码签名) 1.计算出需要校验的数据HASH值 2.将校验HASH值进行RSA加密 3.这部分利用RSA加密过后的HASH值,我们称之为“数字签名
    发表于 11-02 08:00 14次下载
    数据<b class='flag-5'>签名</b>的双向<b class='flag-5'>签名</b>和重<b class='flag-5'>签名</b>的原理和资料简介

    基于ElGamal数字签名算法的区块链共识算法

    联盟链是一种允许授权节点加入网络的区块链,当存在网络状况不理想等状况时,会出现节点动态加入退出的问题。为此,在环签名理论、 Elgamal数字签名算法与PBFT算法的基础上,提出一种
    发表于 05-19 11:51 10次下载

    SpringBoot如何实现启动过程中执行代码

    目前开发的SpringBoot项目在启动的时候需要预加载一些资源。而如何实现启动过程中执行代码,或启动成功后执行,是有很多种方式可以选择,我们可以在static代码块中实现,也可以在构造方法里实现,也可以使用@PostConst
    的头像 发表于 06-20 17:32 1401次阅读

    什么是 SpringBoot

    本文从为什么要有 `SpringBoot`,以及 `SpringBoot` 到底方便在哪里开始入手,逐步分析了 `SpringBoot` 自动装配的原理,最后手写了一个简单的 `start` 组件,通过实战来体会了 `
    的头像 发表于 04-07 11:28 1248次阅读
    什么是 <b class='flag-5'>SpringBoot</b>?