網(wǎng)站設(shè)計(jì)多少錢一個(gè)優(yōu)化設(shè)計(jì)一年級(jí)下冊(cè)數(shù)學(xué)答案
? ? 今天聊下接口安全問(wèn)題,涉及到接口的加密和解密
經(jīng)常和外部單位接口調(diào)用梳理了相關(guān)技術(shù)方案,主要的需求點(diǎn)如下:
1,盡量少改動(dòng),不影響之前的業(yè)務(wù)邏輯
2,考慮到時(shí)間緊迫性,可采用對(duì)稱性加密方式,服務(wù)需要對(duì)接安卓,IOS,H5三端,另外考慮到H5端存儲(chǔ)密鑰安全性相對(duì)來(lái)說(shuō)比較低一些,故針對(duì)H5和安卓,IOS分配兩套密鑰
3,要兼容低版本的接口,后面新開發(fā)的接口可不兼容
4,接口有GET和POST兩種接口,需要都進(jìn)行加密。
需求分析:
1,服務(wù)端,客戶端和H5統(tǒng)一攔截加密,網(wǎng)上有成熟的方案,也可以按其他服務(wù)中實(shí)現(xiàn)的加密流程來(lái)實(shí)現(xiàn)
2,使用AES放松加密,考慮到H5端存儲(chǔ)密鑰安全性相對(duì)來(lái)水比較低一些。故針對(duì)H5和安卓,IOS分配兩套密鑰
3,本次涉及客戶端和服務(wù)器端的整體改造,經(jīng)討論,新接口統(tǒng)一加/secret/ 前綴來(lái)區(qū)分
用戶類:
@Data
public?class?User?{private?Integer?id;private?String?name;private?UserType?userType?=?UserType.COMMON;@JsonFormat(pattern?=?"yyyy-MM-dd?HH:mm:ss")private?LocalDateTime?registerTime;
}
用戶類型枚舉類
@Getter
@JsonFormat(shape?=?JsonFormat.Shape.OBJECT)
public?enum?UserType?{VIP("VIP用戶"),COMMON("普通用戶");private?String?code;private?String?type;UserType(String?type)?{this.code?=?name();this.type?=?type;}
}
簡(jiǎn)單的用戶列表查詢示例:
@RestController
@RequestMapping(value?=?{"/user",?"/secret/user"})
public?class?UserController?{@RequestMapping("/list")ResponseEntity<List<User>>?listUser()?{List<User>?users?=?new?ArrayList<>();User?u?=?new?User();u.setId(1);u.setName("boyka");u.setRegisterTime(LocalDateTime.now());u.setUserType(UserType.COMMON);users.add(u);ResponseEntity<List<User>>?response?=?new?ResponseEntity<>();response.setCode(200);response.setData(users);response.setMsg("用戶列表查詢成功");return?response;}
}
調(diào)用:localhost:8080/user/list
運(yùn)行如下
{"code":?200,"data":?[{"id":?1,"name":?"boyka","userType":?{"code":?"COMMON","type":?"普通用戶"},"registerTime":?"2022-03-24?23:58:39"}],"msg":?"用戶列表查詢成功"
}
目前主要是利用ControllerAdvice來(lái)對(duì)請(qǐng)求和響應(yīng)體進(jìn)行攔截,主要定義SecretRequestAdvice對(duì)請(qǐng)求進(jìn)行加密和SecretResponseAdvice對(duì)響應(yīng)進(jìn)行加密(實(shí)際情況會(huì)稍微復(fù)雜一點(diǎn),項(xiàng)目中又GET類型請(qǐng)求,自定義了一個(gè)Filter進(jìn)行不同的請(qǐng)求解密處理)。
好了,網(wǎng)上的ControllerAdvice使用示例非常多,我這把兩個(gè)核心方法給大家展示看看,相信大佬們一看就曉得了
SecretRequestAdvice請(qǐng)求解密
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public?class?SecretRequestAdvice?extends?RequestBodyAdviceAdapter?{@Overridepublic?boolean?supports(MethodParameter?methodParameter,?Type?type,?Class<??extends?HttpMessageConverter<?>>?aClass)?{return?true;}@Overridepublic?HttpInputMessage?beforeBodyRead(HttpInputMessage?inputMessage,?MethodParameter?parameter,?Type?targetType,?Class<??extends?HttpMessageConverter<?>>?converterType)?throws?IOException?{//如果支持加密消息,進(jìn)行消息解密。String?httpBody;if?(Boolean.TRUE.equals(SecretFilter.secretThreadLocal.get()))?{httpBody?=?decryptBody(inputMessage);}?else?{httpBody?=?StreamUtils.copyToString(inputMessage.getBody(),?Charset.defaultCharset());}//返回處理后的消息體給messageConvertreturn?new?SecretHttpMessage(new?ByteArrayInputStream(httpBody.getBytes()),?inputMessage.getHeaders());}/***?解密消息體**?@param?inputMessage?消息體*?@return?明文*/private?String?decryptBody(HttpInputMessage?inputMessage)?throws?IOException?{InputStream?encryptStream?=?inputMessage.getBody();String?requestBody?=?StreamUtils.copyToString(encryptStream,?Charset.defaultCharset());//?驗(yàn)簽過(guò)程HttpHeaders?headers?=?inputMessage.getHeaders();if?(CollectionUtils.isEmpty(headers.get("clientType"))||?CollectionUtils.isEmpty(headers.get("timestamp"))||?CollectionUtils.isEmpty(headers.get("salt"))||?CollectionUtils.isEmpty(headers.get("signature")))?{throw?new?ResultException(SECRET_API_ERROR,?"請(qǐng)求解密參數(shù)錯(cuò)誤,clientType、timestamp、salt、signature等參數(shù)傳遞是否正確傳遞");}String?timestamp?=?String.valueOf(Objects.requireNonNull(headers.get("timestamp")).get(0));String?salt?=?String.valueOf(Objects.requireNonNull(headers.get("salt")).get(0));String?signature?=?String.valueOf(Objects.requireNonNull(headers.get("signature")).get(0));String?privateKey?=?SecretFilter.clientPrivateKeyThreadLocal.get();ReqSecret?reqSecret?=?JSON.parseObject(requestBody,?ReqSecret.class);String?data?=?reqSecret.getData();String?newSignature?=?"";if?(!StringUtils.isEmpty(privateKey))?{newSignature?=?Md5Utils.genSignature(timestamp?+?salt?+?data?+?privateKey);}if?(!newSignature.equals(signature))?{//?驗(yàn)簽失敗throw?new?ResultException(SECRET_API_ERROR,?"驗(yàn)簽失敗,請(qǐng)確認(rèn)加密方式是否正確");}try?{String?decrypt?=?EncryptUtils.aesDecrypt(data,?privateKey);if?(StringUtils.isEmpty(decrypt))?{decrypt?=?"{}";}return?decrypt;}?catch?(Exception?e)?{log.error("error:?",?e);}throw?new?ResultException(SECRET_API_ERROR,?"解密失敗");}
}
SecretResponseAdvice響應(yīng)加密
@ControllerAdvice
public?class?SecretResponseAdvice?implements?ResponseBodyAdvice?{private?Logger?logger?=?LoggerFactory.getLogger(SecretResponseAdvice.class);@Overridepublic?boolean?supports(MethodParameter?methodParameter,?Class?aClass)?{return?true;}@Overridepublic?Object?beforeBodyWrite(Object?o,?MethodParameter?methodParameter,?MediaType?mediaType,?Class?aClass,?ServerHttpRequest?serverHttpRequest,?ServerHttpResponse?serverHttpResponse)?{//?判斷是否需要加密Boolean?respSecret?=?SecretFilter.secretThreadLocal.get();String?secretKey?=?SecretFilter.clientPrivateKeyThreadLocal.get();//?清理本地緩存SecretFilter.secretThreadLocal.remove();SecretFilter.clientPrivateKeyThreadLocal.remove();if?(null?!=?respSecret?&&?respSecret)?{if?(o?instanceof?ResponseBasic)?{//?外層加密級(jí)異常if?(SECRET_API_ERROR?==?((ResponseBasic)?o).getCode())?{return?SecretResponseBasic.fail(((ResponseBasic)?o).getCode(),?((ResponseBasic)?o).getData(),?((ResponseBasic)?o).getMsg());}//?業(yè)務(wù)邏輯try?{String?data?=?EncryptUtils.aesEncrypt(JSON.toJSONString(o),?secretKey);//?增加簽名long?timestamp?=?System.currentTimeMillis()?/?1000;int?salt?=?EncryptUtils.genSalt();String?dataNew?=?timestamp?+?""?+?salt?+?""?+?data?+?secretKey;String?newSignature?=?Md5Utils.genSignature(dataNew);return?SecretResponseBasic.success(data,?timestamp,?salt,?newSignature);}?catch?(Exception?e)?{logger.error("beforeBodyWrite?error:",?e);return?SecretResponseBasic.fail(SECRET_API_ERROR,?"",?"服務(wù)端處理結(jié)果數(shù)據(jù)異常");}}}return?o;}
}
運(yùn)行如下:
請(qǐng)求方法:
localhost:8080/secret/user/listheader:
Content-Type:application/json
signature:55efb04a83ca083dd1e6003cde127c45
timestamp:1648308048
salt:123456
clientType:ANDORIDbody體:
//?原始請(qǐng)求體
{"page":?1,"size":?10
}
//?加密后的請(qǐng)求體
{"data":?"1ZBecdnDuMocxAiW9UtBrJzlvVbueP9K0MsIxQccmU3OPG92oRinVm0GxBwdlXXJ"
}//?加密響應(yīng)體:
{"data":?"fxHYvnIE54eAXDbErdrDryEsIYNvsOOkyEKYB1iBcre/QU1wMowHE2BNX/je6OP3NlsCtAeDqcp7J1N332el8q2FokixLvdxAPyW5Un9JiT0LQ3MB8p+nN23pTSIvh9VS92lCA8KULWg2nViSFL5X1VwKrF0K/dcVVZnpw5h227UywP6ezSHjHdA+Q0eKZFGTEv3IzNXWqq/otx5fl1gKQ==","code":?200,"signature":?"aa61f19da0eb5d99f13c145a40a7746b","msg":?"","timestamp":?1648480034,"salt":?632648
}//?解密后的響應(yīng)體:
{"code":?200,"data":?[{"id":?1,"name":?"boyka","registerTime":?"2022-03-27T00:19:43.699","userType":?"COMMON"}],"msg":?"用戶列表查詢成功","salt":?0
}
OK,客戶端請(qǐng)求加密-》發(fā)起請(qǐng)求-》服務(wù)端解密-》業(yè)務(wù)處理-》服務(wù)端響應(yīng)加密-》客戶端解密展示,看起來(lái)沒(méi)啥問(wèn)題,實(shí)際是頭天下午花了2小時(shí)碰需求,差不多花1小時(shí)寫好demo測(cè)試,然后對(duì)所有接口統(tǒng)一進(jìn)行了處理,整體一下午趕腳應(yīng)該行了吧,告訴H5和安卓端同學(xué)明兒上午聯(lián)調(diào)(不小的大家到這個(gè)時(shí)候發(fā)現(xiàn)貓膩沒(méi)有,當(dāng)時(shí)確實(shí)疏忽了,翻了大車......)
次日,安卓端反饋,你這個(gè)加解密有問(wèn)題,解密后的數(shù)據(jù)格式和之前不一樣,仔細(xì)一看,擦,這個(gè)userType和registerTime是不對(duì)勁,開始思考:這個(gè)能是哪兒的問(wèn)題呢?1s之后,初步定位,應(yīng)該是響應(yīng)體的JSON.toJSONString的問(wèn)題:
String?data?=?EncryptUtils.aesEncrypt(JSON.toJSONString(o)),
Debug斷點(diǎn)調(diào)試,果然,是JSON.toJSONString(o)這一步驟轉(zhuǎn)換出了問(wèn)題,那JSON轉(zhuǎn)換時(shí)是不是有高級(jí)屬性可以配置生成想要的序列化格式呢?FastJson在序列化時(shí)提供重載方法,找到其中一個(gè)"SerializerFeature"參數(shù)可以琢磨一下,這個(gè)參數(shù)是可以對(duì)序列化進(jìn)行配置的,它提供了很多配置類型,其中感覺(jué)這幾個(gè)比較沾邊
WriteEnumUsingToString,
WriteEnumUsingName,
UseISO8601DateFormat
對(duì)枚舉類型來(lái)說(shuō),默認(rèn)是使用的WriteEnumUsingName(枚舉的Name), 另一種WriteEnumUsingToString是重新toString方法,理論上可以轉(zhuǎn)換成想要的樣子,即這個(gè)樣子
@Getter
@JsonFormat(shape?=?JsonFormat.Shape.OBJECT)
public?enum?UserType?{VIP("VIP用戶"),COMMON("普通用戶");private?String?code;private?String?type;UserType(String?type)?{this.code?=?name();this.type?=?type;}@Overridepublic?String?toString()?{return?"{"?+"\"code\":\""?+?name()?+?'\"'?+",?\"type\":\""?+?type?+?'\"'?+'}';}
}
結(jié)果轉(zhuǎn)換出來(lái)的數(shù)據(jù)是字符串類型"{"code":"COMMON", "type":"普通用戶"}",這個(gè)方法好像行不通,還有什么好辦法呢?思前想后,看文章開始定義的User和UserType類,標(biāo)記數(shù)據(jù)序列化格式@JsonFormat,再突然想起之前看到過(guò)的一些文章,SpringMVC底層默認(rèn)是使用Jackson進(jìn)行序列化的,那好了,就用Jacksong實(shí)施唄,將SecretResponseAdvice中的序列化方法替換一下
String?data?=?EncryptUtils.aesEncrypt(JSON.toJSONString(o),?secretKey);換為:
String?data?=EncryptUtils.aesEncrypt(new?ObjectMapper().writeValueAsString(o),?secretKey);
運(yùn)行結(jié)果如下:
{"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枚舉類型和非加密版本一樣了,舒服了,== 好像還不對(duì),registerTime怎么變成這個(gè)樣子了?原本是"2022-03-24 23:58:39"這種格式的,網(wǎng)上有很多解決方案,不過(guò)用在我們目前這個(gè)需求里面,就是有損改裝了啊,不太可取,遂去Jackson官網(wǎng)上查找一下相關(guān)文檔,當(dāng)然Jackson也提供了ObjectMapper的序列化配置,重新再初始化配置ObjectMpper對(duì)象
String?DATE_TIME_FORMATTER?=?"yyyy-MM-dd?HH:mm:ss";
ObjectMapper?objectMapper?=?new?Jackson2ObjectMapperBuilder().findModulesViaServiceLoader(true).serializerByType(LocalDateTime.class,?new?LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER))).deserializerByType(LocalDateTime.class,?new?LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMATTER))).build();
轉(zhuǎn)化結(jié)果:
{"code":?200,"data":?[{"id":?1,"name":?"boyka","userType":?{"code":?"COMMON","type":?"普通用戶"},"registerTime":?"2022-03-29?22:57:33"}],"msg":?"用戶列表查詢成功"
}
OK,和非加密版的終于一致了,完了嗎?感覺(jué)還是可能存在些什么問(wèn)題,首先業(yè)務(wù)代碼的時(shí)間序列化需求不一樣,有"yyyy-MM-dd hh:mm:ss"的,也有"yyyy-MM-dd"的,還可能其他配置思考不到位的,導(dǎo)致和之前非加密版返回?cái)?shù)據(jù)不一致的問(wèn)題,到時(shí)候聯(lián)調(diào)測(cè)出來(lái)了也麻煩,有沒(méi)有一勞永逸的辦法呢?哎,這個(gè)時(shí)候如果你看過(guò) Spring 源碼的話,就應(yīng)該知道spring框架自身是怎么序列化的,照著配置應(yīng)該就行嘛,好像有點(diǎn)道理,我這里不從0開始分析源碼了
跟著執(zhí)行鏈路,找到具體的響應(yīng)序列化,重點(diǎn)就是RequestResponseBodyMethodProcessor
protected?<T>?void?writeWithMessageConverters(@Nullable?T?value,?MethodParameter?returnType,?ServletServerHttpRequest?inputMessage,?ServletServerHttpResponse?outputMessage)?throws?IOException,?HttpMediaTypeNotAcceptableException,?HttpMessageNotWritableException?{//?獲取響應(yīng)的攔截器鏈并執(zhí)行beforeBodyWrite方法,也就是執(zhí)行了我們自定義的SecretResponseAdvice中的beforeBodyWrite啦body?=?this.getAdvice().beforeBodyWrite(body,?returnType,?selectedMediaType,?converter.getClass(),?inputMessage,?outputMessage);if?(body?!=?null)?{//?執(zhí)行響應(yīng)體序列化工作if?(genericConverter?!=?null)?{genericConverter.write(body,?(Type)targetType,?selectedMediaType,?outputMessage);}?else?{converter.write(body,?selectedMediaType,?outputMessage);}}
進(jìn)而通過(guò)實(shí)例化的AbstractJackson2HttpMessageConverter對(duì)象找到執(zhí)行序列化的核心方法
->?AbstractGenericHttpMessageConverter:public?final?void?write(T?t,?@Nullable?Type?type,?@Nullable?MediaType?contentType,?HttpOutputMessage?outputMessage)?throws?IOException,?HttpMessageNotWritableException?{...this.writeInternal(t,?type,?outputMessage);outputMessage.getBody().flush();}->?找到Jackson序列化?AbstractJackson2HttpMessageConverter://?從spring容器中獲取并設(shè)置的ObjectMapper實(shí)例protected?ObjectMapper?objectMapper;protected?void?writeInternal(Object?object,?@Nullable?Type?type,?HttpOutputMessage?outputMessage)?throws?IOException,?HttpMessageNotWritableException?{MediaType?contentType?=?outputMessage.getHeaders().getContentType();JsonEncoding?encoding?=?this.getJsonEncoding(contentType);JsonGenerator?generator?=?this.objectMapper.getFactory().createGenerator(outputMessage.getBody(),?encoding);this.writePrefix(generator,?object);Object?value?=?object;Class<?>?serializationView?=?null;FilterProvider?filters?=?null;JavaType?javaType?=?null;if?(object?instanceof?MappingJacksonValue)?{MappingJacksonValue?container?=?(MappingJacksonValue)object;value?=?container.getValue();serializationView?=?container.getSerializationView();filters?=?container.getFilters();}if?(type?!=?null?&&?TypeUtils.isAssignable(type,?value.getClass()))?{javaType?=?this.getJavaType(type,?(Class)null);}ObjectWriter?objectWriter?=?serializationView?!=?null???this.objectMapper.writerWithView(serializationView)?:?this.objectMapper.writer();if?(filters?!=?null)?{objectWriter?=?objectWriter.with(filters);}if?(javaType?!=?null?&&?javaType.isContainerType())?{objectWriter?=?objectWriter.forType(javaType);}SerializationConfig?config?=?objectWriter.getConfig();if?(contentType?!=?null?&&?contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM)?&&?config.isEnabled(SerializationFeature.INDENT_OUTPUT))?{objectWriter?=?objectWriter.with(this.ssePrettyPrinter);}//?重點(diǎn)進(jìn)行序列化objectWriter.writeValue(generator,?value);this.writeSuffix(generator,?object);generator.flush();}
看出SpringMVC在進(jìn)行響應(yīng)序列化的時(shí)候是從容器中獲取的ObjectMapper實(shí)例對(duì)象,并會(huì)根據(jù)不同的默認(rèn)配置條件進(jìn)行序列化,那處理方法就簡(jiǎn)單了,我也可以從Spring容器拿數(shù)據(jù)進(jìn)行序列化啊。SecretResponseAdvice進(jìn)行如下進(jìn)一步改造
@ControllerAdvice
public?class?SecretResponseAdvice?implements?ResponseBodyAdvice?{@Autowiredprivate?ObjectMapper?objectMapper;@Overridepublic?Object?beforeBodyWrite(....)?{.....String?dataStr?=objectMapper.writeValueAsString(o);String?data?=?EncryptUtils.aesEncrypt(dataStr,?secretKey);.....}}