关系经济人类预测化学自然
自然科学
知识物理
化学生物
地理解释
预测理解
本质社会
人类现象
行为研究
经济政治
心理结构
关系指导
人文遗产

老大一个接口加解密任务丢过来,我肝了3天,感觉可以收拾工位了

1月9日 霸王亭投稿
  这日,刚撸完2两代码,正准备掏出手机摸鱼放松放松,只见老大朝我走过来,并露出一个善意的微笑,兴伟呀,xx项目有于安全问题,需要对接口整体进行加密处理,你这方面比较有经验,就给你安排上了哈,看这周内提测行不。。。,额,摸摸头上飘摇着而稀疏的长发,感觉我爱了。
  和产品、前端同学对外需求后,梳理了相关技术方案,主要的需求点如下:尽量少改动,不影响之前的业务逻辑;考虑到时间紧迫性,可采用对称性加密方式,服务需要对接安卓、IOS、H5三端,另外考虑到H5端存储密钥安全性相对来说会低一些,故分针对H5和安卓、IOS分配两套密钥;要兼容低版本的接口,后面新开发的接口可不用兼容;接口有GET和POST两种接口,需要都要进行加解密;
  需求解析:服务端、客户端和H5统一拦截加解密,网上有成熟方案,也可以按其他服务中实现的加解密流程来搞;使用AES放松加密,考虑到H5端存储密钥安全性相对来说会低一些,故分针对H5和安卓、IOS分配两套密钥;本次涉及客户端和服务端的整体改造,经讨论,新接口统一加secret前缀来区分
  按本次需求来简单还原问题,定义两个对象,后面用得着,
  用户类:DatapublicclassUser{privateIprivateSprivateUserTypeuserTypeUserType。COMMON;JsonFormat(patternyyyyMMddHH:mm:ss)privateLocalDateTimeregisterT}复制代码
  用户类型枚举类:GetterJsonFormat(shapeJsonFormat。Shape。OBJECT)publicenumUserType{VIP(VIP用户),COMMON(普通用户);privateSprivateSUserType(Stringtype){this。codename();this。}}复制代码
  构造一个简单的用户列表查询示例:RestControllerRequestMapping(value{user,secretuser})publicclassUserController{RequestMapping(list)ResponseEntityListUserlistUser(){ListUserusersnewArrayList();UserunewUser();u。setId(1);u。setName(boyka);u。setRegisterTime(LocalDateTime。now());u。setUserType(UserType。COMMON);users。add(u);ResponseEntityListUserresponsenewResponseEntity();response。setCode(200);response。setData(users);response。setMsg(用户列表查询成功);}}复制代码
  调用:localhost:8080userlist
  查询结果如下,没毛病:{code:200,data:〔{id:1,name:boyka,userType:{code:COMMON,type:普通用户},registerTime:2022032423:58:39}〕,msg:用户列表查询成功}复制代码
  目前主要是利用ControllerAdvice来对请求和响应体进行拦截,主要定义SecretRequestAdvice对请求进行加密和SecretResponseAdvice对响应进行加密(实际情况会稍微复杂一点,项目中又GET类型请求,自定义了一个Filter进行不同的请求解密处理)。
  好了,网上的ControllerAdvice使用示例非常多,我这把两个核心方法给大家展示看看,相信大佬们一看就晓得了,不需多言。上代码:
  SecretRequestAdvice请求解密:description:author:boykaffdate:202203250025ControllerAdviceOrder(Ordered。HIGHESTPRECEDENCE)Slf4jpublicclassSecretRequestAdviceextendsRequestBodyAdviceAdapter{Overridepublicbooleansupports(MethodParametermethodParameter,Typetype,C?extendsHttpMessageC?aClass){}OverridepublicHttpInputMessagebeforeBodyRead(HttpInputMessageinputMessage,MethodParameterparameter,TypetargetType,C?extendsHttpMessageC?converterType)throwsIOException{如果支持加密消息,进行消息解密。StringhttpBif(Boolean。TRUE。equals(SecretFilter。secretThreadLocal。get())){httpBodydecryptBody(inputMessage);}else{httpBodyStreamUtils。copyToString(inputMessage。getBody(),Charset。defaultCharset());}返回处理后的消息体给messageConvertreturnnewSecretHttpMessage(newByteArrayInputStream(httpBody。getBytes()),inputMessage。getHeaders());}解密消息体paraminputMessage消息体return明文privateStringdecryptBody(HttpInputMessageinputMessage)throwsIOException{InputStreamencryptStreaminputMessage。getBody();StringrequestBodyStreamUtils。copyToString(encryptStream,Charset。defaultCharset());验签过程HttpHeadersheadersinputMessage。getHeaders();if(CollectionUtils。isEmpty(headers。get(clientType))CollectionUtils。isEmpty(headers。get(timestamp))CollectionUtils。isEmpty(headers。get(salt))CollectionUtils。isEmpty(headers。get(signature))){thrownewResultException(SECRETAPIERROR,请求解密参数错误,clientType、timestamp、salt、signature等参数传递是否正确传递);}StringtimestampString。valueOf(Objects。requireNonNull(headers。get(timestamp))。get(0));StringsaltString。valueOf(Objects。requireNonNull(headers。get(salt))。get(0));StringsignatureString。valueOf(Objects。requireNonNull(headers。get(signature))。get(0));StringprivateKeySecretFilter。clientPrivateKeyThreadLocal。get();ReqSecretreqSecretJSON。parseObject(requestBody,ReqSecret。class);StringdatareqSecret。getData();StringnewSif(!StringUtils。isEmpty(privateKey)){newSignatureMd5Utils。genSignature(timestampsaltdataprivateKey);}if(!newSignature。equals(signature)){验签失败thrownewResultException(SECRETAPIERROR,验签失败,请确认加密方式是否正确);}try{StringdecryptEncryptUtils。aesDecrypt(data,privateKey);if(StringUtils。isEmpty(decrypt)){decrypt{};}}catch(Exceptione){log。error(error:,e);}thrownewResultException(SECRETAPIERROR,解密失败);}}复制代码
  SecretResponseAdvice响应加密:ControllerAdvicepublicclassSecretResponseAdviceimplementsResponseBodyAdvice{privateLoggerloggerLoggerFactory。getLogger(SecretResponseAdvice。class);Overridepublicbooleansupports(MethodParametermethodParameter,ClassaClass){}OverridepublicObjectbeforeBodyWrite(Objecto,MethodParametermethodParameter,MediaTypemediaType,ClassaClass,ServerHttpRequestserverHttpRequest,ServerHttpResponseserverHttpResponse){判断是否需要加密BooleanrespSecretSecretFilter。secretThreadLocal。get();StringsecretKeySecretFilter。clientPrivateKeyThreadLocal。get();清理本地缓存SecretFilter。secretThreadLocal。remove();SecretFilter。clientPrivateKeyThreadLocal。remove();if(null!respSecretrespSecret){if(oinstanceofResponseBasic){外层加密级异常if(SECRETAPIERROR((ResponseBasic)o)。getCode()){returnSecretResponseBasic。fail(((ResponseBasic)o)。getCode(),((ResponseBasic)o)。getData(),((ResponseBasic)o)。getMsg());}业务逻辑try{StringdataEncryptUtils。aesEncrypt(JSON。toJSONString(o),secretKey);增加签名longtimestampSystem。currentTimeMillis()1000;intsaltEncryptUtils。genSalt();StringdataNewtimestampsaltdatasecretKStringnewSignatureMd5Utils。genSignature(dataNew);returnSecretResponseBasic。success(data,timestamp,salt,newSignature);}catch(Exceptione){logger。error(beforeBodyWriteerror:,e);returnSecretResponseBasic。fail(SECRETAPIERROR,,服务端处理结果数据异常);}}}}}复制代码
  OK,代码Demo撸好了,试运行一波:请求方法:localhost:8080secretuserlistheader:ContentType:applicationjsonsignature:55efb04a83ca083dd1e6003cde127c45timestamp:1648308048salt:123456clientType:ANDORIDbody体:原始请求体{page:1,size:10}加密后的请求体{data:1ZBecdnDuMocxAiW9UtBrJzlvVbueP9K0MsIxQccmU3OPG92oRinVm0GxBwdlXXJ}加密响应体:{data:fxHYvnIE54eAXDbErdrDryEsIYNvsOOkyEKYB1iBcreQU1wMowHE2BNXje6OP3NlsCtAeDqcp7J1N332el8q2FokixLvdxAPyW5Un9JiT0LQ3MB8pnN23pTSIvh9VS92lCA8KULWg2nViSFL5X1VwKrF0KdcVVZnpw5h227UywP6ezSHjHdAQ0eKZFGTEv3IzNXWqqotx5fl1gKQ,code:200,signature:aa61f19da0eb5d99f13c145a40a7746b,msg:,timestamp:1648480034,salt:632648}解密后的响应体:{code:200,data:〔{id:1,name:boyka,registerTime:20220327T00:19:43。699,userType:COMMON}〕,msg:用户列表查询成功,salt:0}复制代码
  OK,客户端请求加密》发起请求》服务端解密》业务处理》服务端响应加密》客户端解密展示,看起来没啥问题,实际是头天下午花了2小时碰需求,差不多花1小时写好demo测试,然后对所有接口统一进行了处理,整体一下午赶脚应该行了吧,告诉H5和安卓端同学明儿上午联调(不小的大家到这个时候发现猫腻没有,当时确实疏忽了,翻了大车。。。。。。)
  次日,安卓端反馈,你这个加解密有问题,解密后的数据格式和之前不一样,仔细一看,擦,这个userType和registerTime是不对劲,开始思考:这个能是哪儿的问题呢?1s之后,初步定位,应该是响应体的JSON。toJSONString的问题:StringdataEncryptUtils。aesEncrypt(JSON。toJSONString(o)),复制代码
  Debug断点调试,果然,是JSON。toJSONString(o)这一步骤转换出了问题,那JSON转换时是不是有高级属性可以配置生成想要的序列化格式呢?FastJson在序列化时提供重载方法,找到其中一个SerializerFeature参数可以琢磨一下,这个参数是可以对序列化进行配置的,它提供了很多配置类型,其中感觉这几个比较沾边:WriteEnumUsingToString,WriteEnumUsingName,UseISO8601DateFormat复制代码
  对枚举类型来说,默认是使用的WriteEnumUsingName(枚举的Name),另一种WriteEnumUsingToString是重新toString方法,理论上可以转换成想要的样子,即这个样子:GetterJsonFormat(shapeJsonFormat。Shape。OBJECT)publicenumUserType{VIP(VIP用户),COMMON(普通用户);privateSprivateSUserType(Stringtype){this。codename();this。}OverridepublicStringtoString(){return{code:name(),type:type};}}复制代码
  结果转换出来的数据是字符串类型{code:COMMON,type:普通用户},这个方法好像行不通,还有什么好办法呢?思前想后,看文章开始定义的User和UserType类,标记数据序列化格式JsonFormat,再突然想起之前看到过的一些文章,SpringMVC底层默认是使用Jackson进行序列化的,那好了,就用Jacksong实施呗,将SecretResponseAdvice中的序列化方法替换一下:StringdataEncryptUtils。aesEncrypt(JSON。toJSONString(o),secretKey);换为:StringdataEncryptUtils。aesEncrypt(newObjectMapper()。writeValueAsString(o),secretKey);复制代码
  重新运行一波,走起:{code:200,data:〔{id:1,name:boyka,userType:{code:COMMON,type:普通用户},registerTime:{month:MARCH,year:2022,dayOfMonth:29,dayOfWeek:TUESDAY,dayOfYear:88,monthValue:3,hour:22,minute:30,nano:453000000,second:36,chronology:{id:ISO,calendarType:iso8601}}}〕,msg:用户列表查询成功}复制代码
  解密后的userType枚举类型和非加密版本一样了,舒服了,好像还不对,registerTime怎么变成这个样子了?原本是2022032423:58:39这种格式的,Jackson之LocalDateTime转换,无需改实体类这篇文章讲到了这个问题,并提出了一种解决方案,不过用在我们目前这个需求里面,就是有损改装了啊,不太可取,遂去Jackson官网上查找一下相关文档,当然Jackson也提供了ObjectMapper的序列化配置,重新再初始化配置ObjectMpper对象:StringDATETIMEFORMATTERyyyyMMddHH:mm:ObjectMapperobjectMappernewJackson2ObjectMapperBuilder()。findModulesViaServiceLoader(true)。serializerByType(LocalDateTime。class,newLocalDateTimeSerializer(DateTimeFormatter。ofPattern(DATETIMEFORMATTER)))。deserializerByType(LocalDateTime。class,newLocalDateTimeDeserializer(DateTimeFormatter。ofPattern(DATETIMEFORMATTER)))。build();复制代码
  转换结果:{code:200,data:〔{id:1,name:boyka,userType:{code:COMMON,type:普通用户},registerTime:2022032922:57:33}〕,msg:用户列表查询成功}复制代码
  OK,和非加密版的终于一致了,完了吗?感觉还是可能存在些什么问题,首先业务代码的时间序列化需求不一样,有yyyyMMddhh:mm:ss的,也有yyyyMMdd的,还可能其他配置思考不到位的,导致和之前非加密版返回数据不一致的问题,到时候联调测出来了也麻烦,有没有一劳永逸的办法呢?同事一句话点亮我,看一下spring框架自身是怎么序列化的,照着配置应该就行嘛,好像有点道理,不从0开始分析源码了,敢兴趣的朋友可以看看这篇文章源码分析SpringMVC源码(三)RequestBody和ResponseBody原理解析,感觉写可以。
  跟着执行链路,找到具体的响应序列化,重点就是RequestResponseBodyMethodProcessor,protectedTvoidwriteWithMessageConverters(NullableTvalue,MethodParameterreturnType,ServletServerHttpRequestinputMessage,ServletServerHttpResponseoutputMessage)throwsIOException,HttpMediaTypeNotAcceptableException,HttpMessageNotWritableException{获取响应的拦截器链并执行beforeBodyWrite方法,也就是执行了我们自定义的SecretResponseAdvice中的beforeBodyWrite啦bodythis。getAdvice()。beforeBodyWrite(body,returnType,selectedMediaType,converter。getClass(),inputMessage,outputMessage);if(body!null){执行响应体序列化工作if(genericConverter!null){genericConverter。write(body,(Type)targetType,selectedMediaType,outputMessage);}else{converter。write(body,selectedMediaType,outputMessage);}}复制代码
  进而通过实例化的AbstractJackson2HttpMessageConverter对象找到执行序列化的核心方法AbstractGenericHttpMessageConverter:publicfinalvoidwrite(Tt,NullableTypetype,NullableMediaTypecontentType,HttpOutputMessageoutputMessage)throwsIOException,HttpMessageNotWritableException{。。。this。writeInternal(t,type,outputMessage);outputMessage。getBody()。flush();}找到Jackson序列化AbstractJackson2HttpMessageConverter:从spring容器中获取并设置的ObjectMapper实例protectedObjectMapperobjectMprotectedvoidwriteInternal(Objectobject,NullableTypetype,HttpOutputMessageoutputMessage)throwsIOException,HttpMessageNotWritableException{MediaTypecontentTypeoutputMessage。getHeaders()。getContentType();JsonEncodingencodingthis。getJsonEncoding(contentType);JsonGeneratorgeneratorthis。objectMapper。getFactory()。createGenerator(outputMessage。getBody(),encoding);this。writePrefix(generator,object);OC?serializationVFilterPJavaTypejavaTif(objectinstanceofMappingJacksonValue){MappingJacksonValuecontainer(MappingJacksonValue)valuecontainer。getValue();serializationViewcontainer。getSerializationView();filterscontainer。getFilters();}if(type!nullTypeUtils。isAssignable(type,value。getClass())){javaTypethis。getJavaType(type,(Class)null);}ObjectWriterobjectWriterserializationView!null?this。objectMapper。writerWithView(serializationView):this。objectMapper。writer();if(filters!null){objectWriterobjectWriter。with(filters);}if(javaType!nulljavaType。isContainerType()){objectWriterobjectWriter。forType(javaType);}SerializationConfigconfigobjectWriter。getConfig();if(contentType!nullcontentType。isCompatibleWith(MediaType。TEXTEVENTSTREAM)config。isEnabled(SerializationFeature。INDENTOUTPUT)){objectWriterobjectWriter。with(this。ssePrettyPrinter);}重点进行序列化objectWriter。writeValue(generator,value);this。writeSuffix(generator,object);generator。flush();}复制代码
  那么,可以看出SpringMVC在进行响应序列化的时候是从容器中获取的ObjectMapper实例对象,并会根据不同的默认配置条件进行序列化,那处理方法就简单了,我也可以从Spring容器拿数据进行序列化啊。SecretResponseAdvice进行如下进一步改造:ControllerAdvicepublicclassSecretResponseAdviceimplementsResponseBodyAdvice{AutowiredprivateObjectMapperobjectMOverridepublicObjectbeforeBodyWrite(。。。。){。。。。。StringdataStrobjectMapper。writeValueAsString(o);StringdataEncryptUtils。aesEncrypt(dataStr,secretKey);。。。。。}}复制代码
  经测试,响应数据和非加密版万全一致啦,还有GET部分的请求加密,以及后面加解密惨遭跨域问题,后面有空再和大家聊聊。
  作者:宫三公子
  链接:https:juejin。cnpost7080568585021554718
投诉 评论 转载

茅台上的两条红飘带有什么用?管家今天来告诉你在所有的茅台酒中,不管是普通飞天也好,还是特供定制也罢了,茅台酒瓶颈上都有两条红飘带,都写着中国贵州茅台酒五个大字。但这仅仅是用来绣子的吗?如果你这样想,那就大错特错了!……过往不老提,余生不总谈人生,不过是由过去、现在和未来组成,本来,这三者相辅相成,不分你我,但,面对现实,总有人会顾此失彼,或沉浸于过去无法自拔,或总是设想着未来,耽搁了现在,或只顾着当下,既不懂得总……我们的爱在赎罪痛了,哭了,累了,倦了。一路的伤痛,一路的沧桑!孤枕难眠,今夜我无法入睡,忍不住的伤泪只会更加深悴!曾经写过凄美的情书,却写不出动人的爱情!……网红书店限定炸猪排金山这条大街还藏了多少小惊喜?金山一街一路系列找寻时间的记忆,聆听他们的诉说在金山区,张堰大街藏着不少人的回忆。不甚宽阔的街道,却自带宁静闲适的气质。骑着单车的人是慢的、……手持式工业平板电脑哪款好?工业pad怎么挑选手持式工业平板电脑哪款好?工业pad怎么挑选视频加载中。。。1、看厂家品牌影响力三防平板电脑作为工业平板电脑的细分领域,国内已经凝聚了大批诸如研维等影响力较大……7万爆改房车,玩遍新疆西藏,两人三猫环游中国也太爽了一辆13万的房车,被改造成原木色温馨小屋的模样,虽是路上的家,但烹饪、洗漱、睡觉的空间一应俱全。在路上,他们也曾遇到过深陷泥沼,等待救援的危险时刻,但更多的是体验日照金山……4年1。33亿稳了!场均20分防守全队第一,年薪仅2000万赛季至今西部15支球队排名变化最频繁,勇士、湖人、森林狼三支所谓的强队目前都排名在十名以外,爵士和灰熊表现让人惊艳,排名第四第五,开拓者还是和往年一样,常规赛期间基本都保持在前……做一个新时代成长型的妈妈,首先要照顾好自己头条创作挑战赛2014年,《人民日报》上《中国妈妈为什么这么累?》这篇文章的系列图片在各大网站上被疯狂的转发,引起了社会的共鸣。虽时隔多年,但仍有越来越多的妈妈挤入……老大一个接口加解密任务丢过来,我肝了3天,感觉可以收拾工位了这日,刚撸完2两代码,正准备掏出手机摸鱼放松放松,只见老大朝我走过来,并露出一个善意的微笑,兴伟呀,xx项目有于安全问题,需要对接口整体进行加密处理,你这方面比较有经验,就给你……神舟十三号载人飞船返回舱首次面向公众展出9月15日,在北京科学中心光年深处展厅,神舟十三号载人飞船返回舱首次面向公众展出,返回舱降落伞、长征三号甲运载火箭残骸等一批珍贵的航天器实物也同时亮相。吴凡摄图片来源:视觉中国……宝格丽梵克雅宝卡地亚蒂芙尼2022年新款珠宝合集综合文字、图片来源:宝格丽、梵克雅宝、卡地亚、蒂芙尼官网仅供交流分享,感谢原作者如果小编的选用让您觉得不妥请联系我们会第一时间删除处理,万分感谢2022年已过……人均年薪超过20万,医疗从业者想赚钱,得去这些城市抱团抗寒,是健康行业的一个风向标文赵天宇编王小图Pixabay进入九月,A股上市公司的半年报已完成披露,《财经》记者梳理发现,健康领域利润最高的50家企……
改变自己,需要经历的五个阶段我再也不想鸡娃了,我要躺平牛肉怎么腌制才嫩?12年经验分享,不干不柴不塞牙,和豆腐一样夜雨丨唐代贤初秋随想稻盛和夫在萧条中飞跃的5大对策欧冠0哥本哈根取开门红立秋后,吃菠菜芹菜不如吃它,全身是宝,才1元1斤,包饺子太香有多少钱能躺平呢?躺平中国最美的村,篁岭温哥华远郊的哈里森小镇,除了温泉还能玩啥?精彩数不胜数辽宁第2批注册球员公布,杨鸣5年长约留超新星,郭艾伦无缘大名女性机器人功能外形很逼真,却引发争议,网友又不能生娃
风景这边独好作文800字校园安全工作总结生娃后的第一年是否分床睡,已经决定了这个家庭未来的幸福度你将独自面对整个世界营业厅能查手机号定位吗(手机定位咋查一个人位置)为什么有些中年女人还会有少女感?热传聚热点网 微信群怎么设置管理微信群如何设置管理国庆去哪儿玩?保姆级别游玩攻略来啦大学贫困申请书华硕PadFone支持1080p播放吗孩子往往因为无娱乐而沉迷电脑,家长们应重视这一点!什么是孕妇护肤品

友情链接:中准网聚热点快百科快传网快生活快软网快好知文好找菏泽德阳山西湖州宝鸡上海茂名内江三亚信阳长春北海西安安徽黄石烟台沧州湛江肇庆鹤壁六安韶关成都钦州