老大一个接口加解密任务丢过来,我肝了3天,感觉可以收拾工位了
这日,刚撸完2两代码,正准备掏出手机摸鱼放松放松,只见老大朝我走过来,并露出一个善意的微笑,兴伟呀,xx项目有于安全问题,需要对接口整体进行加密处理,你这方面比较有经验,就给你安排上了哈,看这周内提测行不。。。,额,摸摸头上飘摇着而稀疏的长发,感觉我爱了。
和产品、前端同学对外需求后,梳理了相关技术方案,主要的需求点如下:尽量少改动,不影响之前的业务逻辑;考虑到时间紧迫性,可采用对称性加密方式,服务需要对接安卓、IOS、H5三端,另外考虑到H5端存储密钥安全性相对来说会低一些,故分针对H5和安卓、IOS分配两套密钥;要兼容低版本的接口,后面新开发的接口可不用兼容;接口有GET和POST两种接口,需要都要进行加解密;
需求解析:服务端、客户端和H5统一拦截加解密,网上有成熟方案,也可以按其他服务中实现的加解密流程来搞;使用AES放松加密,考虑到H5端存储密钥安全性相对来说会低一些,故分针对H5和安卓、IOS分配两套密钥;本次涉及客户端和服务端的整体改造,经讨论,新接口统一加secret前缀来区分
按本次需求来简单还原问题,定义两个对象,后面用得着,
用户类:DatapublicclassUser{privateIntegerid;privateStringname;privateUserTypeuserTypeUserType。COMMON;JsonFormat(patternyyyyMMddHH:mm:ss)privateLocalDateTimeregisterTime;}复制代码
用户类型枚举类:GetterJsonFormat(shapeJsonFormat。Shape。OBJECT)publicenumUserType{VIP(VIP用户),COMMON(普通用户);privateStringcode;privateStringtype;UserType(Stringtype){this。codename();this。typetype;}}复制代码
构造一个简单的用户列表查询示例: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(用户列表查询成功);returnresponse;}}复制代码
调用: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,Classlt;?extendsHttpMessageConverterlt;?aClass){returntrue;}OverridepublicHttpInputMessagebeforeBodyRead(HttpInputMessageinputMessage,MethodParameterparameter,TypetargetType,Classlt;?extendsHttpMessageConverterlt;?converterType)throwsIOException{如果支持加密消息,进行消息解密。StringhttpBody;if(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();StringnewSignature;if(!StringUtils。isEmpty(privateKey)){newSignatureMd5Utils。genSignature(timestampsaltdataprivateKey);}if(!newSignature。equals(signature)){验签失败thrownewResultException(SECRETAPIERROR,验签失败,请确认加密方式是否正确);}try{StringdecryptEncryptUtils。aesDecrypt(data,privateKey);if(StringUtils。isEmpty(decrypt)){decrypt{};}returndecrypt;}catch(Exceptione){log。error(error:,e);}thrownewResultException(SECRETAPIERROR,解密失败);}}复制代码
SecretResponseAdvice响应加密:ControllerAdvicepublicclassSecretResponseAdviceimplementsResponseBodyAdvice{privateLoggerloggerLoggerFactory。getLogger(SecretResponseAdvice。class);Overridepublicbooleansupports(MethodParametermethodParameter,ClassaClass){returntrue;}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();StringdataNewtimestampsaltdatasecretKey;StringnewSignatureMd5Utils。genSignature(dataNew);returnSecretResponseBasic。success(data,timestamp,salt,newSignature);}catch(Exceptione){logger。error(beforeBodyWriteerror:,e);returnSecretResponseBasic。fail(SECRETAPIERROR,,服务端处理结果数据异常);}}}returno;}}复制代码
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(普通用户);privateStringcode;privateStringtype;UserType(Stringtype){this。codename();this。typetype;}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:ss;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实例protectedObjectMapperobjectMapper;protectedvoidwriteInternal(Objectobject,NullableTypetype,HttpOutputMessageoutputMessage)throwsIOException,HttpMessageNotWritableException{MediaTypecontentTypeoutputMessage。getHeaders()。getContentType();JsonEncodingencodingthis。getJsonEncoding(contentType);JsonGeneratorgeneratorthis。objectMapper。getFactory()。createGenerator(outputMessage。getBody(),encoding);this。writePrefix(generator,object);Objectvalueobject;Classlt;?serializationViewnull;FilterProviderfiltersnull;JavaTypejavaTypenull;if(objectinstanceofMappingJacksonValue){MappingJacksonValuecontainer(MappingJacksonValue)object;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{AutowiredprivateObjectMapperobjectMapper;OverridepublicObjectbeforeBodyWrite(。。。。){。。。。。StringdataStrobjectMapper。writeValueAsString(o);StringdataEncryptUtils。aesEncrypt(dataStr,secretKey);。。。。。}}复制代码
经测试,响应数据和非加密版万全一致啦,还有GET部分的请求加密,以及后面加解密惨遭跨域问题,后面有空再和大家聊聊。
作者:宫三公子
链接:https:juejin。cnpost7080568585021554718
全球性萧条期为什么一定会到来辩证世界观系列001工业时代的天时推算之法一。工业时代的天时生产力发展的春夏秋冬如果说宏观经济周期理论最简单最朴实的表述,便是一升一降一荣一枯,周而复始在曲折中前进。经济如同下表所
裙子下面尽量少穿小白鞋,建议搭配这5双低跟鞋,适合小个子春天来了,各种各样的裙子也要搭配起来了!想要把裙子穿的精致时髦又显高,除了要选对适合自己的款式,在鞋子的搭配方面也要多加斟酌。今年不流行用小白鞋搭配裙子了,建议试试这5双低跟鞋来搭
最近火了一种穿法,叫奶奶鞋收腰裙,优雅显高,适合小个子时间过得飞快,转眼间,春天的进度条也已经过了大半,很多姐妹都开始准备起了春夏季节的单品。其中裙子也算是入手率最高的款式,而今年就火了一种穿法,用奶奶鞋和收腰裙搭配,温柔优雅还很显高
百亩枫林藏山中,快去打卡黄金谷来源重庆日报网枫叶不只是秋天的专属,也有春天的绚烂!3月17日,来自北碚区文化旅游委的消息,北碚黄金谷首届彩色枫叶节将于3月18日正式迎客,山谷中超百亩的枫叶林色彩斑斓绚烂多姿,盛
联想拯救者K7机械键盘2023款亮相原标题联想拯救者K7机械键盘2023款亮相98配列佳达隆G黄Pro轴体三模连接来源IT之家3月18日消息,联想今日公布了拯救者K7机械键盘2023款,预计将在不久后推出。据介绍,该
CBA110比83大胜青岛队北控豪取三连胜3月18日晚,北京控股男篮在2022至2023赛季CBA常规赛第36轮中继续坐镇主场迎战青岛国信队。北控将帅携此前两轮连胜江苏队山西队的余勇,本场再度打出高效攻防,最终以110比8
李亚鹏王菲离婚十年另结新欢,有人苦苦追忆,有人大踏步前行最近王菲和谢霆锋的热度挺高的,两人在公共场合牵手的画面频频登上热搜。其实,王菲和谢霆锋每次同框必会出现牵手的画面,而且两人都是大大方的,完全无视面前的闪光灯。时间回到2000年6月
王曼昱排名世界第二,陈梦排名世界第四,钱天一排名世界第五头条创作挑战赛WTT新加坡大满贯赛事告一段落,最大的赢家就是孙颖莎和钱天一。因为这两个选手分获冠亚军,要说谁的积分涨得最快,那肯定是钱天一,直接上涨了1300多分,之前排名第15位
屠龙者终成恶龙,冥冥中历史将走完一个轮回!当今大不列颠王国的历史,是接连不断的伤天害理和强取豪夺的历史1776年,美丽国最重要的立国文书之一独立宣言对英国作如是强烈谴责,老实说,这个谴责并没有冤枉大英帝国。但讽刺的是,屠龙
该不该信命?古人早有答案人是不是应该信命呢?从李卫亲身经历的一件事中,就可以找到答案!李卫是雍正时期的名臣,在他还未显贵的时候,有一次乘船渡江,同船的还有一个道士。有一个人因为船费的事儿和船夫争吵了起来。
五一假期快来了,江苏5A级景区抓紧收藏江苏5A级景区南京(苏A)钟山风景名胜区中山陵园风景区(中山陵免费)夫子庙秦淮风光带(免费)无锡(苏B)惠山古镇(免费)鼋头渚灵山景区无锡影视基地三国水浒景区徐州(苏C)云龙湖景区
关于我们这十年爱甘南短视频创作大赛的启事视频加载中关于我们这十年爱甘南短视频创作大赛的启事党的十八大以来,在习近平新时代中国特色社会主义思想指引下,甘南各族人民团结奋斗勇毅前行,贯彻新发展理念构建新发展格局,努力打造五无
防守4大天王!新赛季失球榜巴萨第1,大巴黎第4,黑马太意外新赛季防守最好的4大俱乐部,到底有哪些?以下是详细的排名!202223赛季的巴黎圣日耳曼,在新帅加尔蒂埃的带领下,看起来像是一支肩负使命的球队。新教练带来了他自己的一套新战术和新阵
洗一次头发隔多久?大多数人搞错了,怨不得护发素用不当头发掉一直以来,有很多人困惑,头发为什么有时候掉得特别多?有时不排除用脑过度,心理压力大,使用不当,做法错误等等,我们平时尽力所能地做一些调整,所能避免的就要改变方式,要正确的根据发质确
20岁靠三级片爆红和多位男星传绯闻,她疯成这样怎么没人骂?还有谁没看过美女这两张照片,我都会哭的!黑色修身礼服搭配绿宝石项链,烈焰红唇明眸皓齿舒淇,从来不会让人失望。她只是站在那,就无形间勾着众人的目光。动图的杀伤力更猛!抬眼颔首间,简直
一叶知秋,一念情长,爱过一次,思念一生有一场相遇,唯美了时光,有一场离别,伤感了念想,再也看不到你深情的双眸,只能一个人在深秋里抒写想念。一叶知秋,一念情长,爱过一次,再也不忘,问世间情为何物,只叫人生死相许,悠悠岁月
人生路,不回头俗话说的好,掌握思考过程,也就掌握了人生路,不回头。孟子讲过一句值得人反复寻思的话,尽信书,则不如无书。他会这么说是有理由的。那么,我们不得不相信,人生路,不回头的出现,重写了人生
2022。09。26早安心语,清晨正能量最棒语录说说激励人心的图片早上好,今天是2022年09月26日,星期一,农历九月初一,壬寅年虎年己酉月壬午日。人生几个秋,弹指一挥间,岁月总无情,生命多悔恨,丝染无复白,鬓白无重黑,愿君多谨记,努力爱青春,
桂花秋思他乡客桂子月中落,天香云外飘。若非空气中浸润着甜甜的桂香,我似已不知时维九月,节近中秋。唉,又是月圆,金风送爽,撵走了炎夏烦听寒蝉,黄叶舞秋,断送了流年。找一个清静之处,仰面向天,微闭双
躺平的年轻人十年后会是什么样?会很爽,会活得更久,会发现新大陆,会后悔自己怎么没早点躺平。因为躺平的本质就是我知道我成不了人上人,所以我选择我喜欢的简单快乐生活方式不信的话你试着幻想一下躺平后的生活1,以前学不
在声色场所演出就是人尽可夫的坏女孩?因为看到一些事,所以想说一个很实际的问题,不知道大家对于在酒吧演出是怎么想的,但是,我却是真真切切的感受到那种把你当玩物一样的感觉,哪怕你洁身自好,在别人眼里都一样。很多人觉得,世
如果可以安乐死如果可以安乐死,我现在会活得很踏实,因为我知道我挺不住了会有办法,安乐死就像定心丸,像解药,很好。当生则生,当死则死,每个人承受压力不同,如果实在撑不住了,选择安乐死离开这个世界,