Enterprise Java (or any other) applications ofter requires logging of request and response bodies to investigate any upcoming issue. Logging these payloads are easy-peasy, but what should we do with any sensitive data inside those payloads? In this short article I'd like to share a solution with Spring Boot that is used in most large enterprise companies to log their incoming and outgoing messages in a secure and regulated way.
Let's start our journey with
where we generate our initial application:
Let's press on "Generate" to download the starter version of the app. Once the download has completed, unzip the archive and open it in your preferred IDE (I prefer IntelliJ).
Step 1: Create the request class
@Getter
@Setter
public class ServiceRequest {
private String name;
}
Step 2: Create the response class
Create a the response class called ServiceResponse that will be provided by our controller:
@Getter
@Setter
public class ServiceResponse {
private String message;
private String requestId;
}
Step 3: Create the REST controller
@Getter
@Setter
public class ServiceResponse {
private String message;
private String requestId;
}
@RestController
public class ServiceController {
@PostMapping("/service")
public @ResponseBody ServiceResponse service(@RequestBody ServiceRequest request) {
ServiceResponse response = new ServiceResponse();
response.setMessage("Welcome to the sensitive logging demo, " + request.getName() + "!");
response.setRequestId(UUID.randomUUID().toString());
return response;
}
}
Step 4: Implement the request logger service
@Slf4j
@ControllerAdvice
public class RequestLogger implements RequestBodyAdvice {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(
@NonNull MethodParameter methodParameter,
@NonNull Type targetType,
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public @NonNull HttpInputMessage beforeBodyRead(
@NonNull HttpInputMessage inputMessage,
@NonNull MethodParameter parameter,
@NonNull Type targetType,
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return inputMessage;
}
@Override
public @NonNull Object afterBodyRead(
@NonNull Object body,
@NonNull HttpInputMessage inputMessage,
@NonNull MethodParameter parameter,
@NonNull Type targetType,
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
logRequest(body);
return body;
}
@Override
public Object handleEmptyBody(
Object body,
@NonNull HttpInputMessage inputMessage,
@NonNull MethodParameter parameter,
@NonNull Type targetType,
@NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
private void logRequest(Object body) {
try {
log.info("Request logged: {}", objectMapper.writeValueAsString(body));
} catch (JsonProcessingException e) {
log.error("Error while logging request", e);
}
}
}
Step 5: Implement the response logger service
@Slf4j
@ControllerAdvice
public class ResponseLogger implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
@NonNull MethodParameter returnType,
@NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
@NonNull ServerHttpRequest request,
@NonNull ServerHttpResponse response) {
logResponse(body);
return body;
}
private void logResponse(Object body) {
try {
log.info("Response: {}", objectMapper.writeValueAsString(body));
} catch (JsonProcessingException e) {
log.error("Error while logging response", e);
}
}
}
Step 6: Add the masking annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Sensitive {
}
Step 7: Enhance the ObjectMapper Part I: Serializer
public class MaskSensitiveDataSerializer extends StdSerializer<String> {
@Serial
private static final long serialVersionUID = 1L;
public MaskSensitiveDataSerializer() {
super(String.class);
}
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString("****");
}
}
Step 7: Enhance the ObjectMapper Part II: Annotation introspect
public class CustomJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findSerializer(Annotated am) {
Sensitive annotation = am.getAnnotation(Sensitive.class);
return (annotation != null) ? MaskSensitiveDataSerializer.class : super.findSerializer(am);
}
}
Step 8: Enhance the ObjectMapper Part III: Configuration
@Bean
@Primary
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.registerModule(new JsonComponentModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
}
@Bean
@Qualifier("loggingObjectMapper")
public ObjectMapper loggingObjectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.registerModule(new JsonComponentModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.enable(SerializationFeature.INDENT_OUTPUT)
.setAnnotationIntrospector(new CustomJacksonAnnotationIntrospector());
}
Step 9: Enhance Loggers
@Autowired
@Qualifier("loggingObjectMapper")
private ObjectMapper objectMapper;
Step 10: Add the annotation to the request and response
@Getter
@Setter
public class ServiceRequest {
@Sensitive
private String name;
}
@Getter
@Setter
public class ServiceResponse {
private String message;
@Sensitive
private String requestId;
}
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.