L'article d'aujourd'hui parle des problèmes de sécurité des interfaces, impliquant le cryptage et le décryptage des interfaces.
Après avoir trié les besoins externes des étudiants sur les produits et des étudiants front-end, nous avons trié les solutions techniques pertinentes. Les principaux points de demande sont les suivants. suit :
@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; }
Classe d'énumération de type d'utilisateur :
@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; } }
@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; } }
{ "code": 200, "data": [{ "id": 1, "name": "boyka", "userType": { "code": "COMMON", "type": "普通用户" }, "registerTime": "2022-03-24 23:58:39" }], "msg": "用户列表查询成功" }
@ControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) @Slf4j public class SecretRequestAdvice extends RequestBodyAdviceAdapter { @Override public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass){ return true; } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { //如果支持加密消息,进行消息解密。 String httpBody; if (Boolean.TRUE.equals(SecretFilter.secretThreadLocal.get())) { httpBody = decryptBody(inputMessage); } else { httpBody = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset()); } //返回处理后的消息体给messageConvert return 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()); // 验签过程 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, "请求解密参数错误,clientType、timestamp、salt、signature等参数传递是否正确传递"); } 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)) { // 验签失败 throw new ResultException(SECRET_API_ERROR, "验签失败,请确认加密方式是否正确"); } 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, "解密失败"); } }
@ControllerAdvice public class SecretResponseAdvice implements ResponseBodyAdvice { private Logger logger = LoggerFactory.getLogger(SecretResponseAdvice.class); @Override public boolean supports(MethodParameter methodParameter, Class aClass){ return true; } @Override public 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) { // 外层加密级异常 if (SECRET_API_ERROR == ((ResponseBasic) o).getCode()) { return SecretResponseBasic.fail(((ResponseBasic) o).getCode(), ((ResponseBasic) o).getData(), ((ResponseBasic) o).getMsg()); } // 业务逻辑 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, "", "服务端处理结果数据异常"); } } } return o; } }
请求方法: localhost:8080/secret/user/list header: Content-Type:application/json signature:55efb04a83ca083dd1e6003cde127c45 timestamp:1648308048 salt:123456 clientType:ANDORID body体: // 原始请求体 { "page": 1, "size": 10 } // 加密后的请求体 { "data": "1ZBecdnDuMocxAiW9UtBrJzlvVbueP9K0MsIxQccmU3OPG92oRinVm0GxBwdlXXJ" } // 加密响应体: { "data": "fxHYvnIE54eAXDbErdrDryEsIYNvsOOkyEKYB1iBcre/QU1wMowHE2BNX/je6OP3NlsCtAeDqcp7J1N332el8q2FokixLvdxAPyW5Un9JiT0LQ3MB8p+nN23pTSIvh9VS92lCA8KULWg2nViSFL5X1VwKrF0K/dcVVZnpw5h227UywP6ezSHjHdA+Q0eKZFGTEv3IzNXWqq/otx5fl1gKQ==", "code": 200, "signature": "aa61f19da0eb5d99f13c145a40a7746b", "msg": "", "timestamp": 1648480034, "salt": 632648 } // 解密后的响应体: { "code": 200, "data": [{ "id": 1, "name": "boyka", "registerTime": "2022-03-27T00:19:43.699", "userType": "COMMON" }], "msg": "用户列表查询成功", "salt": 0 }
Le lendemain, le côté Android a signalé qu'il y avait un problème avec votre cryptage et votre décryptage. Après le décryptage, le format des données est différent d'avant et j'ai réalisé que quelque chose ne va pas avec userType et. registerTime. J’ai commencé à réfléchir : quel pourrait être le problème ? Après 1 seconde, le positionnement initial était qu'il devrait s'agir d'un problème avec JSON.toJSONString dans le corps de la réponse :
String data = EncryptUtils.aesEncrypt(JSON.toJSONString(o)),
Debug le débogage du point d'arrêt Effectivement, c'est l'étape JSON.toJSONString(o) qui a provoqué. le problème. Ensuite JSON Existe-t-il des propriétés avancées qui peuvent être configurées pour générer le format de sérialisation souhaité lors de la conversion ? FastJson fournit des méthodes de surcharge lors de la sérialisation. Si vous trouvez l'un des paramètres "SerializerFeature", vous pouvez y penser. Ce paramètre peut être configuré pour la sérialisation, parmi lesquels je pense que ceux-ci sont relativement pertinents : #🎜. 🎜 #
WriteEnumUsingToString, WriteEnumUsingName, UseISO8601DateFormat
@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; } @Override public String toString(){ return "{" + ""code":"" + name() + '"' + ", "type":"" + type + '"' + '}'; } }
Les données converties sont de type chaîne "{"code": "COMMON", "type": "Utilisateur normal"}". y a-t-il un autre bon moyen ? Après y avoir réfléchi, j'ai regardé les classes User et UserType définies au début de l'article et j'ai marqué le format de sérialisation des données @JsonFormat. Puis je me suis soudainement souvenu de certains articles que j'avais vus auparavant. La couche inférieure de SpringMVC utilise Jackson pour la sérialisation. par défaut. Eh bien, utilisez simplement Jackson pour l'implémenter. Eh bien, remplacez la méthode de sérialisation dans SecretResponseAdvice :
String data = EncryptUtils.aesEncrypt(JSON.toJSONString(o), secretKey); 换为: String data =EncryptUtils.aesEncrypt(new ObjectMapper().writeValueAsString(o), secretKey);
Réexécutez et démarrez :
{ "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": "用户列表查询成功" }
Le type d'énumération userType déchiffré est. la même chose que la version non cryptée. Maintenant, je me sens à l'aise, == Cela ne semble pas encore correct. Pourquoi registerTime est-il devenu comme ça ? Il était à l'origine au format "2022-03-24 23:58:39". Il existe de nombreuses solutions sur Internet, mais lorsqu'elles sont utilisées dans nos besoins actuels, il s'agit d'une modification avec perte, ce qui n'est pas conseillé, nous y sommes donc allés. vers le site officiel de Jackson Rechercher des documents associés Bien sûr, Jackson fournit également la configuration de la sérialisation d'ObjectMapper Réinitialisez et configurez l'objet ObjectMpper :
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();
Résultat de la conversion :
.{ "code": 200, "data": [{ "id": 1, "name": "boyka", "userType": { "code": "COMMON", "type": "普通用户" }, "registerTime": "2022-03-29 22:57:33" }], "msg": "用户列表查询成功" }
OK,和非加密版的终于一致了,完了吗?感觉还是可能存在些什么问题,首先业务代码的时间序列化需求不一样,有"yyyy-MM-dd hh:mm:ss"的,也有"yyyy-MM-dd"的,还可能其他配置思考不到位的,导致和之前非加密版返回数据不一致的问题,到时候联调测出来了也麻烦,有没有一劳永逸的办法呢?哎,这个时候如果你看过 Spring 源码的话,就应该知道spring框架自身是怎么序列化的,照着配置应该就行嘛,好像有点道理,我这里不从0开始分析源码了。
跟着执行链路,找到具体的响应序列化,重点就是RequestResponseBodyMethodProcessor,
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { // 获取响应的拦截器链并执行beforeBodyWrite方法,也就是执行了我们自定义的SecretResponseAdvice中的beforeBodyWrite啦 body = this.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: 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容器中获取并设置的ObjectMapper实例 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); } // 重点进行序列化 objectWriter.writeValue(generator, value); this.writeSuffix(generator, object); generator.flush(); }
那么,可以看出SpringMVC在进行响应序列化的时候是从容器中获取的ObjectMapper实例对象,并会根据不同的默认配置条件进行序列化,那处理方法就简单了,我也可以从Spring容器拿数据进行序列化啊。SecretResponseAdvice进行如下进一步改造:
@ControllerAdvice public class SecretResponseAdvice implements ResponseBodyAdvice { @Autowired private ObjectMapper objectMapper; @Override public Object beforeBodyWrite(....){ ..... String dataStr =objectMapper.writeValueAsString(o); String data = EncryptUtils.aesEncrypt(dataStr, secretKey); ..... } }
经测试,响应数据和非加密版万全一致啦,还有GET部分的请求加密,以及后面加解密惨遭跨域问题,后面有空再和大家聊聊。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!