This day, just rolled out two code, is preparing to take out a cellular phone touch fish to relax, I saw the boss came up to me, and show a “bona fide” smile, xing wei, xx project on security issues, need to encrypt the interface as a whole, the more you experience, will give you arrange the ha, don’t think this week to test line… Well, I feel my hair waving and thin, and I love it.

After meeting the external requirements of products and front-end students, I sorted out relevant technical solutions, and the main demand points are as follows:

  1. Change as little as possible, do not affect the previous business logic;
  2. Considering the urgency of time, symmetric encryption can be adopted, and the service needs to interconnect with Android, IOS, and H5 terminals. In addition, considering that the security of storing keys on H5 terminal is relatively low, two sets of keys are allocated for H5, Android, and IOS.
  3. To be compatible with the interface of the lower version, the interface of the later development can not be compatible;
  4. Interfaces include GET and POST interfaces, which need to be decrypted.

Requirements analysis:

  1. Server, client and H5 unified interception encryption and decryption, there are mature programs online, can also be achieved by other services in the encryption and decryption process to do;
  2. If AES is used to relax encryption, H5 storage keys are relatively less secure. Therefore, two sets of keys are allocated for H5, Android, and IOS.
  3. This time involves the overall transformation of client and server. After discussion, the new interface is distinguished by /securityApi/ prefix

According to this demand to simply restore the problem, define two objects, which will be needed later,

The user types:

@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;
}
Copy the code

User type enumeration class:

@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum UserType {
    VIP("VIP"),
    COMMON("Ordinary user");
    private String code;
    private String type;

    UserType(String type) {
        this.code = name();
        this.type = type; }}Copy the code

Example of constructing a simple user list query:

@RestController
@RequestMapping(value = {"/user", "/securityApi/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("User list query succeeded");
        returnresponse; }}Copy the code

Call: localhost: 8080 / user/list

The query result is as follows:

{" code ": 200," data ": [{" id" : 1, "name" : "boyka", "userType" : {" code ":" COMMON ", "type" : "average user"}, "registerTime" : "2022-03-24 23:58:39"}], "MSG ":" user query succeeded "}Copy the code

At present, ControllerAdvice is mainly used to intercept the request and response body. It mainly defines SecretRequestAdvice to encrypt the request and SecretResponseAdvice to encrypt the response. Custom Filter for different request decryption processing).

Ok, there are plenty of examples of ControllerAdvice on the web, so I’m going to show you two of the core methods. The code:

SecretRequestAdvice Request decryption:

/ * * *@description:
 * @author: boykaff
 * @dateThe 2022-03-25-0025 * /
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class SecretRequestAdvice extends RequestBodyAdviceAdapter {
    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class
       > aClass) {
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class
       > converterType) throws IOException {
        // Decrypt messages if encrypted messages are supported.
        String httpBody;
        if (Boolean.TRUE.equals(SecretFilter.secretThreadLocal.get())) {
            httpBody = decryptBody(inputMessage);
        } else {
            httpBody = StreamUtils.copyToString(inputMessage.getBody(), Charset.defaultCharset());
        }
        // Return the processed message body to messageConvert
        return new SecretHttpMessage(new ByteArrayInputStream(httpBody.getBytes()), inputMessage.getHeaders());
    }

    /** * Decrypts the message body **@paramInputMessage Message body *@returnClear * /
    private String decryptBody(HttpInputMessage inputMessage) throws IOException {
        InputStream encryptStream = inputMessage.getBody();
        String requestBody = StreamUtils.copyToString(encryptStream, Charset.defaultCharset());
        // The verification process
        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, "Request decryption parameter error, clientType, timestamp, salt, signature and other parameters transfer is correct");
        }

        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)) {// Failed to check the visa
            throw new ResultException(SECRET_API_ERROR, "Failed to check, please confirm whether the encryption method is correct.");
        }

        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, "Decryption failed"); }}Copy the code

SecretResponseAdvice Response encryption

@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) {
        // Determine whether encryption is required
        Boolean respSecret = SecretFilter.secretThreadLocal.get();
        String secretKey = SecretFilter.clientPrivateKeyThreadLocal.get();
        // Clear the local cache
        SecretFilter.secretThreadLocal.remove();
        SecretFilter.clientPrivateKeyThreadLocal.remove();
        if (null! = respSecret && respSecret) {if (o instanceof ResponseBasic) {
                // The outer encryption level is abnormal
                if (SECRET_API_ERROR == ((ResponseBasic) o).getCode()) {
                    return SecretResponseBasic.fail(((ResponseBasic) o).getCode(), ((ResponseBasic) o).getData(), ((ResponseBasic) o).getMsg());
                }
                // Business logic
                try {
                    String data = EncryptUtils.aesEncrypt(JSON.toJSONString(o), secretKey);
                    // Add a signature
                    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, ""."Abnormal server processing result data"); }}}returno; }}Copy the code

OK, code Demo is ready, trial run a wave:

Request method: localhost: 8080 / secret/user/list header: Content-Type:application/json signature:55efb04a83ca083dd1e6003cde127c45 timestamp:1648308048 salt:123456 ClientType :ANDORID Body: // Original request body {"page": 1, "size": 10} // Encrypted request body {"data": "1 zbecdndumocxaiw9utbrjzlvvbuep9k0msixqccmu3opg92orinvm0gxbwdlxxj"} / / encryption response body: {" data ": "fxHYvnIE54eAXDbErdrDryEsIYNvsOOkyEKYB1iBcre/QU1wMowHE2BNX/je6OP3NlsCtAeDqcp7J1N332el8q2FokixLvdxAPyW5Un9JiT0LQ3MB8p+nN2 3pTSIvh9VS92lCA8KULWg2nViSFL5X1VwKrF0K/dcVVZnpw5h227UywP6ezSHjHdA+Q0eKZFGTEv3IzNXWqq/otx5fl1gKQ==", "code": 200, "signature": "aa61f19da0eb5d99f13c145a40a7746b", "msg": "", "timestamp": 1648480034, "salt": 632648} // Decrypted response body: {"code": 200, "data": [{"id": 1, "name": "boyka", "registerTime": "The 2022-03-27 T00: parts. 699", "userType" : "COMMON"}], "MSG" : "user list query success", "salt" : 0}Copy the code

OK, the client request encryption, decryption – “a request -” the service side – “business process -” the server response encryption, decryption – “the client show, seems to do not have what problem, is actually spent two hours in the afternoon before touch demand, about 1 hour to write good demo test, and then has carried on the processing, to all interfaces and unified whole afternoon should be at the line, Tell H5 and Andzhuoduan students tomorrow morning joint tune (no small everyone to this time found no foul play, at that time did negligence, turned over the cart……)

The next day, Android replied that there was something wrong with your encryption and decryption. The data format after decryption was different from that before. After a closer look, it was found that there was something wrong with userType and registerTime. After 1s, the initial location should be the response body json.tojsonString problem:

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

ToJSONString (o) : toJSONString(o) : toJSONString(O) : toJSONString(O) FastJson provides overloaded methods for serialization. If you look for one of the “SerializerFeature” parameters, you can configure serialization. There are a number of configuration types available for this parameter.

WriteEnumUsingToString,
WriteEnumUsingName,
UseISO8601DateFormat
Copy the code

For enumeration types, the default is to use WriteEnumUsingName(the Name of the enumeration). The other WriteEnumUsingToString method is the re-toString method, which can theoretically be converted to the desired state, namely this:

@getter @jsonFormat (shape = jsonformat.shap.object) Public enum UserType {VIP("VIP user "), COMMON(" COMMON user "); private String code; private String type; UserType(String type) { this.code = name(); this.type = type; } @Override public String toString() { return "{" + "\"code\":\"" + name() + '\"' + ", \"type\":\"" + type + '\"' + '}';  }}Copy the code

{“code”:”COMMON”, “type”:” COMMON user “}” The User and UserType classes are defined at the beginning of the article, and the data serialization format @jsonFormat is set to @jsonFormat. Replace the serialization method in SecretResponseAdvice with:

String data = EncryptUtils.aesEncrypt(JSON.toJSONString(o), secretKey); String data = encryptutils.aesencrypt (new ObjectMapper().writeValueAsString(o), secretKey);

Copy the code

Rerun a wave, go up:

{
	"code": 200."data": [{
		"id": 1."name": "boyka"."userType": {
			"code": "COMMON"."type": "Ordinary user"
		},
		"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": "User list query succeeded"
}
Copy the code

The decrypted userType enumeration is now the same as the unencrypted version. == registerTime 2022-03-24 23:58:39 Jackson LocalDateTime conversion, no need to change the entity class This article discusses the problem, and proposes a solution, but in our current requirements, is the loss of the conversion, it is not desirable. ObjectMapper = ObjectMapper; ObjectMpper = ObjectMapper; ObjectMapper = ObjectMapper; ObjectMapper = ObjectMapper;

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();Copy the code

Conversion result:

{" code ": 200," data ": [{" id" : 1, "name" : "boyka", "userType" : {" code ":" COMMON ", "type" : "average user"}, "registerTime" : "2022-03-29 22:57:33"}], "MSG ":" user query succeeded "}Copy the code

OK, it’s finally consistent with the unencrypted version. Are we done? First of all, the time serialization requirements of the business code are different, including “YYYY-MM-DD HH: MM: SS” and “YYYY-MM-DD”. Other configurations may not be properly thought out, resulting in inconsistent data returned by the previous unencrypted version. Is there a permanent solution? The spring framework is serialized according to the configuration. It seems to be reasonable. The source code is not analyzed from 0. Dare interested friends can look at this article source analysis Spring MVC source code (three) —– @requestBody and @responseBody principle parsing, feel can write.

Follow execution link, find specific response serialization, the key is RequestResponseBodyMethodProcessor,

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
        // Get the chain of interceptors for the response and execute beforeBodyWrite, which implements beforeBodyWrite in our custom SecretResponseAdvice
		body = this.getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, converter.getClass(), inputMessage, outputMessage);
		if(body ! =null) {
		    // Perform the response body serialization
			if(genericConverter ! =null) {
				genericConverter.write(body, (Type)targetType, selectedMediaType, outputMessage);
			} else{ converter.write(body, selectedMediaType, outputMessage); }}Copy the code

Then by instantiating AbstractJackson2HttpMessageConverter object to find the core of the serialization methods

-> 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(); } - > find Jackson serialization AbstractJackson2HttpMessageConverter:// ObjectMapper instance obtained and set from spring container
	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);
		}
        // Focus on serialization
		objectWriter.writeValue(generator, value);
		this.writeSuffix(generator, object);
		generator.flush();
    }
Copy the code

SpringMVC serializes the response by retrieving the ObjectMapper instance object from the container and using a different default configuration. SecretResponseAdvice is further modified as follows:


@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); . }}Copy the code

After testing, the response data and the non-encrypted version is completely consistent, as well as GET part of the request encryption, and the encryption and decryption behind the cross domain problem, and later have a chat with you.