public class StringIsBlankExample {
public static void main(String[] args) {
String str1 = "";
String str2 = " ";
String str3 = " \t ";
System.out.println(str1.isBlank());
System.out.println(str2.isBlank());
System.out.println(str3.isBlank());
}
}
/** 출력
true
true
true
*/
lines() 메서드는 문자열을 줄바꿈으로 구분하여 스트림으로 반환.
import java.util.stream.Stream;
public class StringLinesExample {
public static void main(String[] args) {
String str = "Hello\nWorld\nJava";
Stream<String> lines = str.lines();
lines.forEach(System.out::println);
}
}
/** 출력
Hello
World
Java
*/
strip() 메서드는 문자열의 앞뒤 공백을 제거한 새로운 문자열을 반환.
public class StringStripExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.strip());
System.out.println(str2.strip());
}
}
/** 출력
abc
def
*/
stripLeading() 메서드는 문자열의 앞쪽 공백을 제거한 새로운 문자열을 반환.
public class StringStripLeadingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripLeading());
System.out.println(str2.stripLeading());
}
}
/**
abc
def
*/
stripTrailing() 메서드는 문자열의 뒤쪽 공백을 제거한 새로운 문자열을 반환.
public class StringStripTrailingExample {
public static void main(String[] args) {
String str1 = " abc ";
String str2 = "\t def \n";
System.out.println(str1.stripTrailing());
System.out.println(str2.stripTrailing());
}
}
/** 출력
abc
def
*/
formatted(Object... args) 메서드는 지정된 인자를 사용하여 문자열을 형식화하고 형식화된 문자열을 반환.
public class StringFormattedExample {
public static void main(String[] args) {
String str = "My name is %s, I'm %d years old.";
String formattedStr = str.formatted( "John", 25);
System.out.println(formattedStr);
}
}
translateEscapes() 메서드는 자바 이스케이프 시퀀스를 해당 문자로 변환하여 새로운 문자열을 반환
public class StringTranslateEscapesExample {
public static void main(String[] args) {
String str = "Hello\\nWorld\\tJava";
String translatedStr = str.translateEscapes();
System.out.println(translatedStr);
}
}
transform() 메서드는 문자열을 다른 인코딩 형식으로 변환.
public class StringTransformExample {
public static void main(String[] args) {
String str = "hello world";
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
String newStr = new String(bytes, StandardCharsets.ISO_8859_1);
System.out.println(newStr);
}
}
Spring Boot에서는 전역적으로 적용되는 날짜 및 시간 형식을 지정할 수 있습니다. 이를 통해 코드에서 중복되는 날짜 및 시간 형식 코드를 제거하고 코드의 가독성과 유지 보수성을 향상시킬 수 있습니다.
여러가지 방안이 있겠습니다만 저는 아래같은 방식으로 진행해왔으니 참고하시면 될것 같습니다.
@JsonFormat 어노테이션
@Data
public class MyDateTime {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone = "UTC")
private Date dateTime;
}
WebMvcConfigurer 인터페이스 구현
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.timeZone(TimeZone.getDefault());
return builder;
}
}
postman 으로 테스트 할 controller 를 하나 만들어보겠습니다.
@RestController
@RequestMapping("/")
public class dataController {
@GetMapping("/dateTime")
public MyDateTime getMyDateTime() {
MyDateTime myDateTime = new MyDateTime();
myDateTime.setDateTime(new Date());
return myDateTime;
}
}
@JsonComponent 어노테이션 사용하는 방법
CustomDateSerializer 를 작성합니다.
public class CustomDateSerializer extends JsonSerializer<Date> {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
@Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(dateFormat.format(value));
}
}
CustomDateDeserializer를 작성 합니다.
public class CustomDateDeserializer extends JsonDeserializer<Date> {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
@Override
public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
String dateString = node.textValue();
try {
return dateFormat.parse(dateString);
} catch (ParseException e) {
throw new IOException("Invalid date format: " + dateString, e);
}
}
}
새로운 응답형식으로 객체 하나 더 만들어 볼게요.
@Data
public class MyResponse {
private String message;
private Date timestamp;
}
마지막으로 postman 이 호출할 controller 를 하나 추가로 만들어 볼게요.
public class MyController {
@GetMapping("/myEndpoint")
public MyResponse getMyResponse() {
MyResponse response = new MyResponse();
response.setMessage("안녕! 난 블레이크야!");
response.setTimestamp(new Date());
return response;
}
}
RequestBodyAdvice의 사용법에 대해 묻는 사람이 있어 이에 대해 글을 쓰게 되었습니다.
암호화 및 복호화 자체는 어렵지 않지만, 언제 처리해야 할까요? 필터를 정의하여 요청과 응답을 각각 가로채 처리하는 것도 가능하지만 이 방법은 거칠지만 유연합니다. 왜냐하면, 첫 번째 손에 요청 매개 변수와 응답 데이터를 얻을 수 있기 때문입니다. 그러나 SpringMVC에서는 ResponseBodyAdvice와 RequestBodyAdvice를 제공하여 요청과 응답을 사전 처리할 수 있도록 지원합니다.
그래서 오늘 이 글은 두 가지 목적을 가지고 있습니다:
매개 변수/응답 암호화 및 복호화에 대한 아이디어 공유.
ResponseBodyAdvice와 RequestBodyAdvice의 사용법을 공유합니다.
그러면 이제 본론으로 들어가 보겠습니다.
1.개발 암호화 starter
우리가 개발하는 이 도구를 보다 일반화하기 위해, 그리고 스프링 부트 스타터를 직접 정의하기 위해 이 도구를 starter로 만들겠습니다. 이후에는 Spring Boot 프로젝트에서 바로 사용할 수 있습니다.
먼저 Spring Boot 프로젝트를 생성하고, spring-boot-starter-web 의존성을 추가합니다.
의존성을 추가한 후에는 미리 사용할 수 있도록 암호화 유틸리티 클래스를 정의합니다. 암호화 방법에는 대칭 키 암호화와 비대칭 키 암호화가 있으며, 대칭 키 암호화에서는 AES, DES, 3DES 등 다양한 알고리즘이 사용됩니다. 여기에서는 Java에서 제공하는 Cipher를 사용하여 대칭 키 암호화 방식 중 AES 알고리즘을 사용합니다.
이 Utils 클래스는 간단하므로 따로 설명은 필요하지 않을것 같습니다. 그러나 암호화된 데이터는 가독성이 없을 수 있으므로 일반적으로 암호화된 데이터를 Base64 알고리즘을 사용하여 인코딩하여 읽을 수 있는 문자열을 얻을 수 있습니다. 다시 말해서, 위의 AES 암호화 메소드의 반환 값은 Base64로 인코딩 된 문자열입니다. AES 복호화 메소드의 매개 변수도 Base64로 인코딩 된 문자열이며, 이 문자열을 먼저 디코딩 한 다음 해독해야합니다.
그 다음 우리는 예비로 응답 도구 클래스를 패키징 할 것입니다. 이것은 사람들이 종종 송건 비디오를보고 이미 잘 이해할 것입니다.
public class RespBean {
private Integer status;
private String msg;
private Object obj;
public static RespBean build() {
return new RespBean();
}
public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}
public static RespBean ok(String msg, Object obj) {
return new RespBean(200, msg, obj);
}
public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}
public static RespBean error(String msg, Object obj) {
return new RespBean(500, msg, obj);
}
private RespBean() {
}
private RespBean(Integer status, String msg, Object obj) {
this.status = status;
this.msg = msg;
this.obj = obj;
}
public Integer getStatus() {
return status;
}
public RespBean setStatus(Integer status) {
this.status = status;
return this;
}
public String getMsg() {
return msg;
}
public RespBean setMsg(String msg) {
this.msg = msg;
return this;
}
public Object getObj() {
return obj;
}
public RespBean setObj(Object obj) {
this.obj = obj;
return this;
}
}
@Decrypt 와 @Encrypt 를 만들어 볼게요.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.PARAMETER})
public @interface Decrypt {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Encrypt {
}
혹은 사용자가 직접 암호화 키를 구성할 수 있기 때문에 EncryptProperties 클래스를 정의하여 사용자가 구성한 키를 읽도록 합니다.
@ConfigurationProperties(prefix = "spring.encrypt")
public class EncryptProperties {
private final static String DEFAULT_KEY = "https://rainsister.tistory.com/";
private String key = DEFAULT_KEY;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}
이후 key는 application.properties (혹은 application.yml) 파일에 선언해 놓으면 됩니다.
spring.encrypt.key=xxx // 이런 식으로
암호화
@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class EncryptResponse implements ResponseBodyAdvice<RespBean> {
private ObjectMapper om = new ObjectMapper();
@Autowired
EncryptProperties encryptProperties;
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return returnType.hasMethodAnnotation(Encrypt.class);
}
@Override
public RespBean beforeBodyWrite(RespBean body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
byte[] keyBytes = encryptProperties.getKey().getBytes();
try {
if (body.getMsg()!=null) {
body.setMsg(AESUtils.encrypt(body.getMsg().getBytes(),keyBytes));
}
if (body.getObj() != null) {
body.setObj(AESUtils.encrypt(om.writeValueAsBytes(body.getObj()), keyBytes));
}
} catch (Exception e) {
e.printStackTrace();
}
return body;
}
}
supports: 이 메서드는 어떤 유형의 인터페이스가 암호화되어야 하는지를 판단하기 위해 사용됩니다. returnType 매개 변수는 반환 유형을 나타내며, 여기에서 판단 로직은 해당 인터페이스에 @Encrypt 주석이 포함되어 있는지 여부입니다. 있으면 해당 인터페이스는 암호화 처리가 필요하며, 없으면 해당 인터페이스는 암호화 처리가 필요하지 않습니다.
beforeBodyWrite: 이 메서드는 데이터 응답 전에 실행됩니다. 즉, 우리는 응답 데이터를 먼저 두 번째 처리하고 처리가 완료되면 json으로 반환합니다. 여기에서는 처리 방법이 매우 간단합니다. RespBean의 status는 상태 코드이므로 암호화하지 않아도됩니다. 두 개의 다른 필드를 다시 암호화하고 다시 값을 설정하면됩니다.
주의!사용자 정의 ResponseBodyAdvice는 @ControllerAdvice 주석을 사용하여 표시해야합니다.
복호화
@EnableConfigurationProperties(EncryptProperties.class)
@ControllerAdvice
public class DecryptRequest extends RequestBodyAdviceAdapter {
@Autowired
EncryptProperties encryptProperties;
@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Decrypt.class) || methodParameter.hasParameterAnnotation(Decrypt.class);
}
@Override
public HttpInputMessage beforeBodyRead(final HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
byte[] body = new byte[inputMessage.getBody().available()];
inputMessage.getBody().read(body);
try {
byte[] decrypt = AESUtils.decrypt(body, encryptProperties.getKey().getBytes());
final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return bais;
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
} catch (Exception e) {
e.printStackTrace();
}
return super.beforeBodyRead(inputMessage, parameter, targetType, converterType);
}
}
DecryptRequest 클래스에서는 RequestBodyAdvice 인터페이스를 직접 구현하는 대신 RequestBodyAdviceAdapter 클래스를 상속합니다. RequestBodyAdviceAdapter 클래스는 RequestBodyAdvice 인터페이스의 하위 클래스이며, 인터페이스의 일부 메서드를 구현하므로 RequestBodyAdviceAdapter를 상속하면 실제 요구 사항에 따라 일부 메서드만 구현하면됩니다.
supports: 이 메서드는 어떤 인터페이스가 인터페이스 복호화를 처리해야 하는지를 판단하기 위해 사용됩니다. 여기에서 판단 로직은 @Decrypt 주석이 메서드 또는 매개 변수에 포함되어 있는지 여부입니다. 복호화 문제를 처리합니다.
beforeBodyRead: 이 메서드는 매개 변수가 구체적인 객체로 변환되기 전에 실행됩니다. 먼저 데이터를 스트림에서 로드한 다음 데이터를 복호화하고 복호화가 완료되면 HttpInputMessage 객체를 다시 구성하여 반환합니다.
사용은 어떻게?
public class User {
private Long id;
private String username;
//생략 getter/setter
}
@RestController
public class HelloController {
@GetMapping("/user")
@Encrypt
public RespBean getUser() {
User user = new User();
user.setId((long) 99);
user.setUsername("javaboy");
return RespBean.ok("ok", user);
}
@PostMapping("/user")
public RespBean addUser(@RequestBody @Decrypt User user) {
System.out.println("user = " + user);
return RespBean.ok("ok", user);
}
}