Heim  >  Artikel  >  Technologie-Peripheriegeräte  >  Die Datenverschlüsselung und -entschlüsselung der Spring Boot-Schnittstelle sollte wie folgt gestaltet sein ~

Die Datenverschlüsselung und -entschlüsselung der Spring Boot-Schnittstelle sollte wie folgt gestaltet sein ~

王林
王林nach vorne
2023-05-13 10:58:05855Durchsuche

Der heutige Artikel befasst sich mit Fragen der Schnittstellensicherheit, einschließlich der Schnittstellenverschlüsselung und -entschlüsselung.

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

Nachdem wir die externen Anforderungen der Produkt- und Front-End-Studenten erfüllt haben, haben wir die relevanten technischen Lösungen ermittelt:

  • Änderungen so gering wie möglich halten, ohne die bisherige Geschäftslogik zu beeinträchtigen.
  • Angesichts der Dringlichkeit können Sie die symmetrische Verschlüsselungsmethode anwenden. Der Dienst muss eine Verbindung zu Android, IOS und H5 herstellen. Da die Sicherheit des H5-Speicherschlüssels relativ gering ist, werden außerdem zwei Schlüsselsätze zugewiesen für H5 und Android und IOS;
  • Erforderlich Kompatibel mit Schnittstellen niedrigerer Versionen, neue Schnittstellen müssen nicht kompatibel sein
  • Die Schnittstelle verfügt über zwei Schnittstellen, GET und POST, die verschlüsselt und entschlüsselt werden müssen; Anforderungsanalyse:

Vereinheitlichung von Server, Client und H5. Um Verschlüsselung und Entschlüsselung abzufangen, gibt es ausgereifte Lösungen im Internet, oder Sie können den in anderen Diensten implementierten Verschlüsselungs- und Entschlüsselungsprozessen folgen.

    Verwenden Sie AES, um die Verschlüsselung zu lockern. Da die Sicherheit des H5-Endspeicherschlüssels relativ gering ist, werden für H5 zwei Schlüsselsätze mit Android und IOS verteilt.
  • Dieses Mal geht es um die allgemeine Transformation des Clients und des Servers wird mit dem Präfix /secret/ vereinheitlicht, um es zu unterscheiden.
  • Um das Problem einfach gemäß dieser Anforderung zu beheben, definieren Sie zwei Objekte, die später verwendet werden:
Benutzerklasse:

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

Benutzertyp-Aufzählungsklasse:

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

Erstellen Sie ein einfaches Beispiel für eine Benutzerlistenabfrage:

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

Call: localhost:8080/user/list

Abfrageergebnisse Wie folgt, kein Problem:

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

Derzeit wird ControllerAdvice hauptsächlich zum Abfangen von Anfragen und Antworttexten verwendet zum Verschlüsseln von Anforderungen und SecretResponseAdvice zum Verschlüsseln von Antworten (die tatsächliche Situation wird etwas komplizierter sein, und es gibt Anforderungen vom Typ GET im Projekt. Ein Filter wird für die Entschlüsselungsverarbeitung verschiedener Anforderungen angepasst).

Okay, es gibt viele Anwendungsbeispiele für ControllerAdvice. Ich glaube, die Großen werden es auf einen Blick verstehen. Obiger Code:

SecretRequestAdvice-Anfrageentschlüsselung:

@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-Antwortverschlüsselung:

@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, die Code-Demo ist fertig, probieren wir es aus:

请求方法:
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, Client-Anfrageverschlüsselung -> Anfrage initiieren -> Server-Entschlüsselung -》 Geschäftsverarbeitung – „Serverseitige Antwortverschlüsselung“ – Clientseitige Entschlüsselungsanzeige Es scheint, dass es am Nachmittag zuvor 2 Stunden gedauert hat, um die Anforderungen zu erfüllen, und fast 1 Stunde, um den Demotest zu schreiben und dann alle Schnittstellen vereinheitlicht haben. Ich werde meinen H5- und Android-Klassenkameraden sagen, dass sie morgen früh gemeinsam debuggen sollen (jeder hat inzwischen herausgefunden, dass nichts faul ist. Ich war tatsächlich nachlässig

Am nächsten Tag meldete die Android-Seite, dass es ein Problem mit Ihrer Ver- und Entschlüsselung gibt. Bei genauerem Hinsehen stimmt etwas nicht userType und registerTime. Ich begann zu denken: Was könnte das Problem sein? Nach 1s war die anfängliche Positionierung, dass es sich um ein Problem mit JSON.toJSONString im Antworttext handeln sollte:

String data = EncryptUtils.aesEncrypt(JSON.toJSONString(o)),

Debug-Breakpoint-Debugging. Gibt es eines? Erweiterte JSON-Konvertierung während der JSON-Konvertierung? Können Attribute konfiguriert werden, um das gewünschte Serialisierungsformat zu generieren? FastJson bietet Überladungsmethoden während der Serialisierung. Dieser Parameter kann für die Serialisierung konfiguriert werden. Er bietet viele Konfigurationstypen, von denen ich denke, dass diese relativ relevant sind:

WriteEnumUsingToString,
WriteEnumUsingName,
UseISO8601DateFormat

Für Aufzählungstypen , die Standardeinstellung ist die Verwendung von WriteEnumUsingName (der Name der Aufzählung). Theoretisch kann sie in das umgewandelt werden, was Sie möchten, also wie folgt:

@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 + '"' +
'}';
}
}

Das Ergebnis wird konvertiert data Es handelt sich um einen String vom Typ „{“code“:“COMMON“, „type“: „normaler Benutzer“}. Diese Methode scheint nicht zu funktionieren. Nachdem ich darüber nachgedacht hatte, schaute ich mir die am Anfang des Artikels definierten Klassen User und UserType an und markierte das Datenserialisierungsformat mit @JsonFormat. Dann fielen mir plötzlich einige Artikel ein, die ich zuvor gesehen hatte. Die unterste Ebene von SpringMVC verwendet Jackson für die Serialisierung Standard. Nun, verwenden Sie einfach Jackson, um es zu implementieren. Ersetzen Sie die Serialisierungsmethode in SecretResponseAdvice:

String data = EncryptUtils.aesEncrypt(JSON.toJSONString(o), secretKey);
 换为:
String data =EncryptUtils.aesEncrypt(new ObjectMapper().writeValueAsString(o), secretKey);

Führen Sie eine Welle erneut aus und starten Sie:

{
 "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": "用户列表查询成功"
}

Der entschlüsselte UserType-Aufzählungstyp ist derselbe wie die unverschlüsselte Version, I fühle dich wohl, == Es scheint nicht richtig, registerTime Wie ist es so geworden? Es hatte ursprünglich das Format „2022-03-24 23:58:39“. Es gibt viele Lösungen im Internet, aber bei der Verwendung in unseren aktuellen Anforderungen handelt es sich um eine verlustbehaftete Modifikation, die nicht ratsam ist, also haben wir uns entschieden Suchen Sie auf der offiziellen Website von Jackson nach relevanten Dokumenten. Natürlich bietet Jackson auch die Serialisierungskonfiguration des ObjectMpper-Objekts an:

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();

Konvertierungsergebnis:

{
 "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部分的请求加密,以及后面加解密惨遭跨域问题,后面有空再和大家聊聊。

Das obige ist der detaillierte Inhalt vonDie Datenverschlüsselung und -entschlüsselung der Spring Boot-Schnittstelle sollte wie folgt gestaltet sein ~. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:51cto.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen