SpringBoot interface signature verification practice

Thisisacommunitythatmaybeusefultoyou

One-to-onecommunication/interviewbrochure/resumeoptimization/jobsearchquestions,welcometojointhe”YudaoRapidDevelopmentPlatform“KnowledgePlanet.ThefollowingissomeinformationprovidedbyPlanet:

  • “ProjectPractice(Video)”:Learnfrombooks,“practice”frompastevents

  • “InternetHighFrequencyInterviewQuestions”:Studyingwithyourresume,springblossoms

  • “ArchitecturexSystemDesign”:Overcomingdifficultiesandmasteringhigh-frequencyinterviewscenarioquestions

  • “AdvancingJavaLearningGuide”:systematiclearning,themainstreamtechnologystackoftheInternet

  • “Must-readJavaSourceCodeColumn”:Knowwhatitisandwhyitisso

Thisisanopensourceprojectthatmaybeusefultoyou

DomesticStarisa100,000+opensourceproject.Thefront-endincludesmanagementbackend+WeChatapplet,andtheback-endsupportsmonomerandmicroservicearchitecture.

FunctionscoverRBACpermissions,SaaSmulti-tenancy,datapermissions,mall,payment,workflow,large-screenreports,WeChatpublicaccount,etc.:

  • Bootaddress:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloudaddress:https://gitee.com/zhijiantianya/yudao-cloud

  • Videotutorial:https://doc.iocoder.cn

Source:blog.csdn.net/qq_43290318
/article/details/131516099

  • 1concept

    • openinterface

    • Verification

  • 2Interfacesignatureverificationcallingprocess

    • 1.Agreedsignaturealgorithm

    • 2.Issueanasymmetrickeypair

    • 3.Generaterequestparametersignature

    • 4.Requesttocallwithsignature

  • 3codedesign

    • 1.Signatureconfigurationclass

    • 2.Signaturemanagementclass

    • 3.Customizesignatureverificationannotations

    • 4.AOPimplementssignatureverificationlogic

    • 5.Solvetheproblemthattherequestbodycanonlybereadonce

    • 6.Customtoolclass

1Concept

Openinterface

Openinterfacesrefertointerfacesthatareallowedtobecalledbythird-partysystemswithoutrequiringlogincredentials.Inordertopreventopeninterfacesfrombeingcalledmaliciously,openinterfacesgenerallyrequiresignatureverificationbeforetheycanbecalled.Systemsthatprovideopeninterfacesarecollectivelyreferredtoas”originalsystems”below.

Signatureverification

Signatureverificationmeansthatbeforecallingtheinterface,thethird-partysystemneedstogenerateasignature(string)basedonallrequestparametersaccordingtotherulesoftheoriginalsystem,andcarrythesignaturewhencallingtheinterface.Theoriginalsystemwillverifythevalidityofthesignature.Onlyifthesignatureverificationisvalidcantheinterfacebecallednormally,otherwisetherequestwillberejected.

Backendmanagementsystem+userappletimplementedbasedonSpringBoot+MyBatisPlus+Vue&Element,supportingRBACdynamicpermissions,multi-tenancy,datapermissions,workflow,three-partylogin,payment,SMS,mallandotherfunctions

  • Projectaddress:https://github.com/YunaiV/ruoyi-vue-pro

  • Videotutorial:https://doc.iocoder.cn/video/

2Interfacesignatureverificationcallingprocess

1.Agreedsignaturealgorithm

Asthecaller,thethird-partysystemneedstonegotiatethesignaturealgorithmwiththeoriginalsystem(theSHA256withRSAsignaturealgorithmisusedasanexamplebelow).Atthesametime,aname(callerID)isagreedupontouniquelyidentifythecallingsystemintheoriginalsystem.

2.Issuanceofasymmetrickeypair

Afterthesignaturealgorithmisagreedupon,theoriginalsystemwillgenerateanexclusiveasymmetrickeypair(RSAkeypair)foreachcallersystem.Theprivatekeyisissuedtothecallingsystem,andthepublickeyisheldbytheoriginalsystem.

Notethatthecallersystemneedstokeeptheprivatekey(storedinthebackendofthecallersystem).Becausefortheoriginalsystem,thecallersystemisthesenderofthemessage,andtheprivatekeyitholdsuniquelyidentifiesitasatrustedcalleroftheoriginalsystem.Oncetheprivatekeyofthecaller’ssystemisleaked,thecallerhasnotrustintheoriginalsystem.

3.Generaterequestparametersignature

Afterthesignaturealgorithmisagreedupon,theprincipleofgeneratingsignaturesisasfollows(activitydiagram).

Inordertoensurethattheprocessingdetailsofgeneratingsignaturesmatchthesignatureverificationlogicoftheoriginalsystem,theoriginalsystemgenerallyprovidesjarpackagesorcodesnippetstothecallertogeneratesignatures.Otherwise,thegeneratedsignaturesmaybeinvalidduetoinconsistenciesinsomeprocessingdetails..

4.Requesttocallwithsignature

PuttheagreedcallerIDinthepathparameter,andputthesignaturegeneratedbythecallerintherequestheader.

Backendmanagementsystem+userappletimplementedbasedonSpringCloudAlibaba+Gateway+Nacos+RocketMQ+Vue&Element,supportingRBACdynamicpermissions,multi-tenancy,datapermissions,workflow,three-partylogin,payment,SMS,mallandotherfunctions

  • Projectaddress:https://github.com/YunaiV/yudao-cloud

  • Videotutorial:https://doc.iocoder.cn/video/

3CodeDesign

1.Signatureconfigurationclass

Therelevantcustomymlconfigurationisasfollows.ThepublicandprivatekeysofRSAcanbegeneratedusingtheSecureUtiltoolclassofhugool.Notethatthepublicandprivatekeysarebase64-encodedstrings.

Defineaconfigurationclasstostoretheaboverelatedcustomymlconfiguration

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;

/**
*Signaturerelatedconfiguration
*/
@Data
@ConditionalOnProperty(value="secure.signature.enable",havingValue="true")//Injectbeansbasedonconditions
@Component
@ConfigurationProperties("secure.signature")
publicclassSignatureProps{
privateBooleanenable;
privateMap<String,KeyPairProps>keyPair;

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

2.Signaturemanagementclass

Defineamanagementclassthatholdstheaboveconfigurationandexposesmethodsforgeneratingsignaturesandverifyingsignatures.

Notethatthegeneratedsignatureisahexadecimal-encodedstringofbytearrays.Whenverifyingthesignature,youneedtohexadecimal-decodethesignaturestringintoabytearray.

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();
}

/**
*Verification.FailuretopassverificationmaythrowaruntimeexceptionCryptoException
*
*@paramcallerIDuniqueidentifierofthecaller
*@paramrawDataoriginaldata
*@paramsignaturesignaturetobeverified(hexstring)
*@returnwhethertheverificationispassed
*/
publicbooleanverifySignature(StringcallerID,StringrawData,Stringsignature){
Signsign=getSignByCallerID(callerID);
if(ObjectUtils.isEmpty(sign)){
returnfalse;
}

//Usepublickeytoverifysignature
returnsign.verify(rawData.getBytes(StandardCharsets.UTF_8),HexUtil.decodeHex(signature));
}

/**
*Generatesignature
*
*@paramcallerIDuniqueidentifierofthecaller
*@paramrawDataoriginaldata
*@returnsignature(hexstring)
*/
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;//Invalid,untrustedcaller
}
returnSecureUtil.sign(keyPairProps.getAlgorithm(),keyPairProps.getPrivateKey(),keyPairProps.getPublicKey());
}

/**
*Loadasymmetrickeypair
*/
privatevoidloadKeyPairByPath(){
//Supportclasspathconfiguration,intheform:classpath:secure/public.txt
//Thepublickeyandprivatekeyarebothbase64encodedstrings
signatureProps.getKeyPair()
.forEach((key,keyPairProps)->{
//IfXxxKeyPathisconfigured,XxxKeyPathtakesprecedence
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.Customizedsignatureverificationannotations

Someinterfacesrequiresignatureverification,butsomeinterfacesdonot.Inordertoflexiblycontrolwhichinterfacesrequiresignatureverification,customizeasignatureverificationannotation.

importjava.lang.annotation.*;

/**
*ThisannotationismarkedonthemethodoftheControllerclass,indicatingthattheparametersoftherequestneedtobeverifiedforsignature
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public@interfaceVerifySignature{
}

4.AOPimplementssignatureverificationlogic

Thesignatureverificationlogiccannotbeplacedintheinterceptor,becausetheinterceptorcannotdirectlyreadtheinputstreamofthebody,otherwisethesubsequent@RequestBodyparameterparserwillnotbeabletoreadthebody.

Sincethebodyinputstreamcanonlybereadonce,youneedtouseContentCachingRequestWrappertowraptherequestandcachethebodycontent(seepoint5),butthecachetimingofthisclassisin@RequestBodyintheparameterparser.

Therefore,twoconditionsmustbemettoobtainthebodycacheinContentCachingRequestWrapper:

  • Theinputparametersoftheinterfacemustexist@RequestBody

  • Thetimingofreadingthebodycachemustbeaftertheparametersof@RequestBodyareparsed,forexample,withinthelogicoftheAOPandControllerlayers.Notethatthetimingoftheinterceptorisbeforeparameterparsing

Tosumup,theinputparametersofthecontrollayermethodannotatedwith@VerifySignaturemustexistin@RequestBody,sothatthebodycachecanbeobtainedduringsignatureverificationinAOP!

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);//Untrustedcaller
}

//Extractthesignaturefromtherequestheader,ifthereisnodirectrejection
Stringsignature=request.getHeader(BaseConstant.X_REQUEST_SIGNATURE);
if(ObjectUtils.isEmpty(signature)){
thrownewParamInvalidException(SignStatusCode.REQUEST_SIGNATURE_INVALID);//Invalidsignature
}

//Extractrequestparameters
StringrequestParamsStr=extractRequestParams(request);
//Verifysignature.Ifthesignatureverificationfails,abusinessexceptionwillbethrown.
verifySignature(callerID,requestParamsStr,signature);
}

@SuppressWarnings("unchecked")
publicStringextractRequestParams(HttpServletRequestrequest){
//@RequestBody
Stringbody=null;
//Thesignatureverificationlogiccannotbeplacedintheinterceptor,becausetheinterceptorcannotdirectlyreadtheinputstreamofthebody,otherwisethesubsequent@RequestBodyparameterparserwillnotbeabletoreadthebody.
//Sincethebodyinputstreamcanonlybereadonce,ContentCachingRequestWrapperneedstobeusedtowraptherequestandcachethebodycontent.However,thecachetimingofthisclassisintheparameterparserof@RequestBody.
//Therefore,twoconditionsmustbemettousethebodycacheinContentCachingRequestWrapper.
//1.Theinputparameteroftheinterfacemustexist@RequestBody
//2.Thetimingofreadingthebodycachemustbeaftertheparametersof@RequestBodyareparsed,forexample:withinthelogicoftheAOPandControllerlayers.Notethatthetimingoftheinterceptorisbeforeparameterparsing
if(requestinstanceofContentCachingRequestWrapper){
ContentCachingRequestWrapperrequestWrapper=(ContentCachingRequestWrapper)request;
body=newString(requestWrapper.getContentAsByteArray(),StandardCharsets.UTF_8);
}

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

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

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

/**
*Verifythesignatureofrequestparameters
*/
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);//Converttobusinessexceptionandthrow
}
}
}
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.Solvetheproblemthattherequestbodycanonlybereadonce

Thesolutionistowraptherequestandcachetherequestbody.SpringBootalsoprovidesContentCachingRequestWrappertosolvethisproblem.However,asalsodescribedindetailinpoint4,duetoitscachingtiming,itsusehasrestrictions.Youcanalsorefertoonlinesolutionsandimplementarequestwrapperclasstocachetherequestbody.

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);
}
}

Registerfilter

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
publicFilterRegistrationBean<RequestCachingFilter>requestCachingFilterRegistration(
RequestCachingFilterrequestCachingFilter){
FilterRegistrationBean<RequestCachingFilter>bean=newFilterRegistrationBean<>(requestCachingFilter);
bean.setOrder(1);
returnbean;
}
}

6.Customtoolclass

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

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

publicclassCommonUtils{
/**
*Extractallrequestparametersandconcatenatethemintoastringaccordingtofixedrules
*
*@parambodyTherequestbodyofthepostrequest
*@paramparamMappathparameter(QueryString).Intheform:name=zhangsan&age=18&label=A&label=B
*@paramuriTemplateVarNappathvariable(PathVariable).Intheform:/{name}/{age}
*@returnAllrequestparametersaresplicedintoastringaccordingtofixedrules
*/
publicstaticStringextractRequestParams(@NullableStringbody,@NullableMap<String,String[]>paramMap,
@NullableMap<String,String>uriTemplateVarNap){
//body:{userID:"xxx"}

//pathparameters
//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->{
//Makeacopyandsorttheminascendinglexicographicorder
String[]sortedValue=Arrays.stream(entry.getValue()).sorted().toArray(String[]::new);
returnentry.getKey()+"="+joinStr(",",sortedValue);
})
.collect(Collectors.joining("&"));
}

//pathvariable
///{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);
}

/**
*Usethespecifieddelimitertoconcatenatestrings
*
*@paramdelimiterdelimiter
*@paramstrsMultiplestringstobespliced,canbenull
*@returnthenewstringafterconcatenation
*/
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);
}
}
returnsbd.toString();
}
}
Codeofthisarticle

https://github.com/passerbyYSQ/DemoRepository

Welcometojoinmyknowledgeplanetandcomprehensivelyimproveyourtechnicalcapabilities.

Tojoin,Longpress”or“Scan”theQRcodebelow:

Planet’scontentincludes:projectpractice,interviewsandrecruitment,sourcecodeanalysis,andlearningroutes.

Ifthearticleishelpful,pleasereaditandforwardit.
Thankyouforyoursupport(*^__^*)

Theknowledgepointsofthearticlematchtheofficialknowledgefiles,andyoucanfurtherlearnrelatedknowledge.JavaSkillTreeHomepageOverview138474peoplearelearningthesystem