Maison  >  Article  >  Périphériques technologiques  >  Le cryptage et le décryptage des données de l'interface Spring Boot doivent être conçus comme ceci ~

Le cryptage et le décryptage des données de l'interface Spring Boot doivent être conçus comme ceci ~

王林
王林avant
2023-05-13 10:58:05926parcourir

L'article d'aujourd'hui parle des problèmes de sécurité des interfaces, impliquant le cryptage et le décryptage des interfaces.

Spring Boot 接口数据加解密就该这样设计~

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 :

    # 🎜🎜# Effectuez le moins de modifications possible sans affecter la logique métier précédente
  • Compte tenu de l'urgence du temps, un cryptage symétrique peut être utilisé. pour être connecté à Android, IOS et H5 De plus, H5 doit être pris en compte. La sécurité des clés de stockage final est relativement faible, donc deux jeux de clés sont alloués pour H5 et Android et IOS
  • #🎜🎜 ; # doit être compatible avec les interfaces des versions inférieures, et les interfaces nouvellement développées n'ont pas besoin d'être compatibles
  • L'interface a deux interfaces, GET et POST, qui doivent être cryptées et déchiffrées ; #
  • Analyse des exigences :
#🎜 🎜#Le serveur, le client et le H5 interceptent uniformément le cryptage et le décryptage. Il existe des solutions matures sur Internet, ou vous pouvez suivre le cryptage. et processus de décryptage implémentés dans d'autres services

Utilisez AES pour assouplir le cryptage, en tenant compte du côté H5 La sécurité des clés de stockage sera relativement faible, donc deux jeux de clés sont alloués pour H5 et Android et IOS ;
  • Cela implique la transformation globale du client et du serveur Après discussion, la nouvelle interface Ajoutez le préfixe /secret/ pour distinguer
  • Pour restaurer simplement. le problème selon cette exigence, définissez deux objets, qui seront utilisés plus tard,
  • Classe d'utilisateur :
@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;
}
}
#🎜🎜 #Construisez un exemple de requête de liste d'utilisateurs simple :

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

Appel : localhost:8080/user/ list

Les résultats de la requête sont les suivants, pas de problème :

{
 "code": 200,
 "data": [{
"id": 1,
"name": "boyka",
"userType": {
 "code": "COMMON",
 "type": "普通用户"
},
"registerTime": "2022-03-24 23:58:39"
 }],
 "msg": "用户列表查询成功"
}

Actuellement, ControllerAdvice est principalement utilisé pour intercepter les requêtes et les corps de réponse. Il définit principalement SecretRequestAdvice pour chiffrer les requêtes et SecretResponseAdvice pour répondre (la situation réelle est un peu plus compliquée. Il y a une requête de type GET dans le projet, et un filtre est personnalisé pour traiter différentes requêtes de décryptage).

D'accord, il existe de nombreux exemples d'utilisation de ControllerAdvice sur Internet. Je vais vous montrer les deux méthodes de base. Je pense que les grands le sauront en un coup d'œil, pas besoin d'en dire plus. Code ci-dessus :

SecretRequestAdvice déchiffrement de la demande :

@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, "解密失败");
}
}

SecretResponseAdvice chiffrement de la réponse :

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

OK, la démo du code est prête, essayons-la :

请求方法:
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
}

OK, le client demande le cryptage-"Initier la demande-"Déchiffrement côté serveur-"Traitement métier-"Chiffrement de réponse côté serveur-"Affichage du décryptage côté client. Il semble qu'il n'y ait pas problème, mais c'était en fait la veille. J'ai passé 2 heures dans l'après-midi à revoir les exigences, et près d'une heure à écrire le test de démonstration, puis j'ai traité toutes les interfaces de manière unifiée. Cela devrait suffire pour rattraper mon retard. tout l'après-midi, et dites aux étudiants H5 et Android de faire un débogage commun demain matin (ce n'est pas une petite tâche) A ce moment-là, tout le monde a découvert qu'il n'y avait rien de mal. Ils ont effectivement été négligents à ce moment-là et ont renversé...) #🎜. 🎜#

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

Pour les types d'énumération, la valeur par défaut est d'utiliser WriteEnumUsingName (le nom de l'énumération). L'autre type de WriteEnumUsingToString est la méthode re-toString, qui peut théoriquement être convertie en ce que vous voulez, c'est-à-dire. comme ceci : #🎜 🎜#
@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!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer