본문 바로가기
IT/Spring boot

[Spring boot] 암호화된 PK의 복호화 구현

by kyu-nahc 2024. 10. 4.
반응형

 

PK 암호화

저번 포스트에서 URI에서 PK가 노출되는 것을 막기 위해

PK를 암호화하여 URI를 구성하도록 구현하였다.

 

[Spring boot] PK Id 노출을 막는 Rest API

Rest APIRest API 설계에서 자원을 식별하는 데 URI를 사용하는 방식은 매우 일반적이며, 자원이 명확히 식별될 수 있어야 한다는 REST의 개념에 부합한다. 따라서 URI에 리소스 식별자인 ID를 포함하는

kyu-nahc.tistory.com

따라서 Rest API를 요청할 때 URI 형식은 /test/aAPzwpKf+0mFtlfbU1wCWA== 다음과 같고,

이런 식으로 암호화된 값을 통해 URI를 구성하여 API 요청을 하게 된다.

그렇다면 API 요청을 받고 이를 복호화하는 로직도 반드시 존재해야 한다.

 

컨트롤러에서의 값 변환

위의 경우처럼 애플리케이션을 개발할 때

종종 리소스에 대한 고유한 식별자(Identifier)를 전달받아 요청을 처리해야 하는 경우가 있다.

이때 리소스 식별자는 시스템 내부의 중요한 정보이므로,

만약 식별자가 단순히 1씩 증가하는 값이라면 보안 강화를 위해 암호화하여 사용해야 한다.

따라서 위의 경우처럼 URI 패턴에 암호화된 PK를 사용하는 것이 적합하다.

아래 컨트롤러 메서드처럼 특정 사용자의 정보를 가져오는 API가 있다고 하자.

이때 @PathVariable로 전달받는 Id는 암호화된 상태이다.

따라서 이를 처리하는 컨트롤러나 하위 계층에서 복호화 로직이 필요하다.

하지만, 이 복호화 로직을 모든 컨트롤러에 적용하는 것은 비효율적이고 중복된 작업이 된다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/test")
@Slf4j
public class TestController {

    private final PostService postService;

    @GetMapping("/{id}")
    public ResponseEntity<PostResponse> getPost(@PathVariable(name="id") String id) throws Exception {
        // 암호화된 Id를 복호화
        Long decodedValue = EncryptionUtil.decrypt(id);
        return ResponseEntity.ok(postService.getPostByIdProcess(decodedValue));
    }
}

 

복호화는 위의 코드처럼 각 컨트롤러나 서비스에서 반복적으로 적용해야 할 수 있는데,

이는 코드 중복과 유지 보수의 어려움을 초래한다.

따라서 반복적인 복호화 작업을 간소화하고, 코드의 일관성과 재사용성을 확보하기 위해,

스프링이 제공하는 타입 변환 기능을 활용할 수 있다.

 

이 문제를 해결하기 위해 개발자가 ArgumentResolver를 떠올릴 수 있지만,

ArgumentResolver는 이 경우 적합하지 않다.

그 이유는 ArgumentResolver는 파라미터의 값을 생성하는 역할만 하기 때문이다.

즉, 이미 생성된 @PathVariable이나 @RequestParam 값을 변환하는 기능은 없다.

우리가 필요로 하는 것은 이미 생성된 값을 변환하는 로직이므로, 다른 방법을 선택해야 한다.

 

컨버터( Converter )를 이용하는 값 변환

스프링 프레임워크는 String 타입을 Long 타입으로 자동 변환하는 기능을 제공하는데,

이는 스프링의 타입 컨버터(Converter)를 통해 수행된다.

따라서 원하는 변환 로직을 추가하려면 커스텀 컨버터를 구현할 수 있다.

@PathVariable로 받은 암호화된 String 타입의 Id 값을 복호화하여

Long 타입으로 변환하는 컨버터를 작성하면 다음과 같다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptId { }

 

먼저, 복호화가 필요한 값을 식별하기 위해 커스텀 어노테이션을 생성한다.

@Target(ElementType.PARAMETER)를 통해 해당 변환이 필요한 메서드 파라미터에만 적용시킨다.

@Retention(RetentionPolicy.RUNTIME)는 어노테이션이 런타임에 참조될 수 있도록 해준다.

나중에 설명할 컨버터에서 이 어노테이션이 달린

파라미터에 대해서만 복호화 작업을 수행할 수 있게 도와준다.

public class DecryptConverter implements ConditionalGenericConverter {

    @Override
    public boolean matches(
            @NonNull
            final TypeDescriptor sourceType,
            final TypeDescriptor targetType) {
        return targetType.hasAnnotation(DecryptId.class);
    }

    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        return Set.of(
                new ConvertiblePair(String.class, Long.class)
        );
    }

    @Override
    public Object convert(
            final Object source,
            @NonNull  final TypeDescriptor sourceType,
            @NonNull final TypeDescriptor targetType) {
        try {
            return EncryptionUtil.decrypt((String)source);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

 

복호화 컨버터는 ConditionalGenericConverter 인터페이스를 구현하여 만들 수 있다.

이 컨버터는 String 타입을 Long 타입으로 변환하는 역할을 하며,

위의 @DecryptId 어노테이션이 있는 경우에만 실행된다. 

메서드에 대해 살펴보면 다음과 같다.

  • getConvertibleTypes()
    변환할 수 있는 타입 쌍을 반환한다.
    여기서는 String에서 Long으로 변환하는 경우에만 작동하도록 설정
  • matches()
    이 메서드는 특정 조건을 만족하는지 확인한다.
    여기서는 파라미터에 @DecryptId 어노테이션이 있는지 확인하고,
    어노테이션이 있을 경우에만 변환을 수행하도록 한다.
  • convert()
    실제 변환이 이루어지는 메서드
    source 파라미터는 암호화된 String 값이고,
    이를 복호화하여 Long 타입으로 변환한 후 반환하도록 구성
  • decrypt()
    복호화 작업을 수행

decrypt() 메서드에 대한 자세한 정보는 아래 포스팅을 참고하면 된다.

 

[Spring boot] PK Id 노출을 막는 Rest API

Rest APIRest API 설계에서 자원을 식별하는 데 URI를 사용하는 방식은 매우 일반적이며, 자원이 명확히 식별될 수 있어야 한다는 REST의 개념에 부합한다. 따라서 URI에 리소스 식별자인 ID를 포함하는

kyu-nahc.tistory.com

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DecryptConverter());
    }
}

 

이제 구현된 컨버터를 스프링의 웹 설정에 등록해야 한다.

WebMvcConfigurer를 구현하여 커스텀 컨버터를 추가해 준다. 

addFormatters() 메서드를 통해 커스텀 컨버터를 스프링의 타입 변환 체계에 추가한다.

이 설정을 추가하면, 스프링은 @PathVariable이나 @RequestParam으로

전달된 값을 자동으로 복호화하여 Long 타입으로 변환해 준다.

@GetMapping("/{id}")
public ResponseEntity<PostResponse> getPost(
	@PathVariable(name="id") @DecryptId Long id) throws Exception {
    return ResponseEntity.ok(postService.getPostByIdProcess(id));
}

 

이제 컨트롤러에서는 암호화된 Id가 자동으로 복호화된 상태로 전달된다.

이제 /test/{id}와 같은 요청이 들어올 때,

Id는 자동으로 복호화된 상태인 Long 타입으로 전달된다.

PostMan Image

 

위의 포스트맨의 실행화면을 보면 암호화된 값을 URI를 구성하여 API 요청을 보낸다.

하지만 컨트롤러에서 @DecryptId 어노테이션을 통해

복호화된 값을 Long Type으로 바로 전달받는 것을 볼 수 있다.

 

지금까지는 String 타입을 복호화하여 Long 타입으로 변환하는 과정을 살펴보았다.

그러나 실제로는 이 Id로 데이터베이스에서 데이터를 조회한 후,

해당 Post 객체를 파라미터로 받는 방식으로 확장할 수도 있다.

이를 위해 컨버터의 변환 결과를 Long 대신 Post 객체로 반환하도록 수정하면 다음과 같다.

@Component
@RequiredArgsConstructor
public class DecryptConverter implements ConditionalGenericConverter {

    private final PostRepository postRepository;

    @Override
    public boolean matches(
            @NonNull
            final TypeDescriptor sourceType,
            final TypeDescriptor targetType) {
        return targetType.hasAnnotation(DecryptId.class);
    }

    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        return Set.of(
                new ConvertiblePair(String.class, Post.class)
        );
    }

    @Override
    public Object convert(
            final Object source,
            @NonNull  final TypeDescriptor sourceType,
            @NonNull final TypeDescriptor targetType) {
        try {
            Long id =  EncryptionUtil.decrypt((String)source);
            return postRepository.findById(id).orElse(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

 

이 방식으로 확장하면 컨트롤러는 @PathVariable로 복호화된 Id를 받는 대신,

직접적으로 Post 객체를 전달받을 수 있어 더 간결하고 효율적인 코드를 작성할 수 있다.

스프링의 Converter 기능을 활용하여 반복적인 복호화 로직을 간단하게 처리할 수 있다.

커스텀 컨버터를 통해 @PathVariable로 받은 값을 변환함으로써,

컨트롤러 코드의 중복을 제거하고, 가독성을 높일 수 있다.

이 방식은 단순히 Id 복호화뿐만 아니라,

더 복잡한 변환 작업에도 적용할 수 있어 확장성이 뛰어나다는 장점이 존재한다.

 

참고자료

https://mangkyu.tistory.com/310

 

반응형

loading