728x90

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

이상으로 어노테이션 하나로 전역시간에 대한 처리하는 방법을 대략적으로 알아봤습니다.


내저장소 바로가기 luxury515

728x90

RequestBodyAdvice의 사용법에 대해 묻는 사람이 있어 이에 대해 글을 쓰게 되었습니다.

암호화 및 복호화 자체는 어렵지 않지만, 언제 처리해야 할까요? 필터를 정의하여 요청과 응답을 각각 가로채 처리하는 것도 가능하지만 이 방법은 거칠지만 유연합니다. 왜냐하면, 첫 번째 손에 요청 매개 변수와 응답 데이터를 얻을 수 있기 때문입니다. 그러나 SpringMVC에서는 ResponseBodyAdvice와 RequestBodyAdvice를 제공하여 요청과 응답을 사전 처리할 수 있도록 지원합니다.

그래서 오늘 이 글은 두 가지 목적을 가지고 있습니다:

매개 변수/응답 암호화 및 복호화에 대한 아이디어 공유. ResponseBodyAdvice와 RequestBodyAdvice의 사용법을 공유합니다. 그러면 이제 본론으로 들어가 보겠습니다.

1.개발 암호화 starter 우리가 개발하는 이 도구를 보다 일반화하기 위해, 그리고 스프링 부트 스타터를 직접 정의하기 위해 이 도구를 starter로 만들겠습니다. 이후에는 Spring Boot 프로젝트에서 바로 사용할 수 있습니다.

먼저 Spring Boot 프로젝트를 생성하고, spring-boot-starter-web 의존성을 추가합니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided</scope>
    <version>2.4.3</version>
</dependency>

의존성을 추가한 후에는 미리 사용할 수 있도록 암호화 유틸리티 클래스를 정의합니다. 암호화 방법에는 대칭 키 암호화와 비대칭 키 암호화가 있으며, 대칭 키 암호화에서는 AES, DES, 3DES 등 다양한 알고리즘이 사용됩니다. 여기에서는 Java에서 제공하는 Cipher를 사용하여 대칭 키 암호화 방식 중 AES 알고리즘을 사용합니다.

public class AESUtils {

    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";

    // cipher 얻기
    private static Cipher getCipher(byte[] key, int model) throws Exception {
        SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
        cipher.init(model, secretKeySpec);
        return cipher;
    }

    // AES 암호화
    public static String encrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
        return Base64.getEncoder().encodeToString(cipher.doFinal(data));
    }

    // AES 복호화
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
        return cipher.doFinal(Base64.getDecoder().decode(data));
    }
}

이 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);
    }
}
spring.encrypt.key=1234567890123456


내저장소 바로가기 luxury515

728x90

환경변수 PATH: JDK버전전환에 사용될 스크립트 경로 지정

C:\java\scripts. [메인폴더 , 여기에 버전별 변경로 변경스크립트를 넣는다]

C:\j. va\ 에 (실제 java 설치된 폴더 경로) 에 scripts 폴더 생성

배치이름 지정 java{JDK version}.bat

java11.bat

스크립트:

@echo off
set JAVA_HOME=C:\java\jdk-11.0.15+10 # 실제 JDK 경로
set Path=%JAVA_HOME%\bin;%Path%
echo Java 11 activated.
java -version

위 같은 방식으로 버전별로 배치파일을 만들고

버전교체 명령

java11 ,java17 이런 식으로 한면 됨.


내저장소 바로가기 luxury515

728x90
zsh: command not found: ls zsh: command not found: vi

위와 같은 에러가 나타나면서 명령어들 안먹힘. 조금전 환경변수를 설정하고 source ~/.zshrc 를 하니 그때부터 안됨.

에러: prompt_status:5: command not found: wc

일단 급하니…

export PATH=%PATH:/bin:/usr/local/bin:/usr/bin

이후 ~/.zshrc 를 편집하여 잘못설정한 환경변수들을 제거 후

source ~/.zshrc


내저장소 바로가기 luxury515

+ Recent posts