본문 바로가기
IT/Spring boot

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

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

 

Rest API

Rest API 설계에서 자원을 식별하는 데 URI를 사용하는 방식은 매우 일반적이며,

자원이 명확히 식별될 수 있어야 한다는 REST의 개념에 부합한다.

따라서 URI리소스 식별자인 ID를 포함하는 것은 흔한 접근 방식이다.

예를 들어, Http PUT 요청을 통해 /api/post/1이라는 엔드포인트는

1번 포스트를 수정하는 API라는 것을 직관적으로 알 수 있다.

 

리소스 식별자와 PK( Primary Key ) 사용

리소스를 식별하기 위해 보통 고유한 값을 필요로 하는데,

중복되지 않는 고유 ID를 생성하는 방식 중 하나로 DB의 PK를 자주 활용한다.

PK는 이미 DB에서 유일성을 보장하고 있기 때문에

이를 REST API에서 식별자로 사용하는 것이 자연스럽고 URI를 표현하는데 적합하다.

특히 작은 규모의 프로젝트에서는 별도의 고유 ID 생성기를 구현하는 대신

PK를 사용하는 것이 비용 및 리소스를 절감할 수 있는 효율적인 방법이 될 수 있다.

Primary Key 노출의 문제점

그러나 REST API에서 DB의 PK를

무분별하게 외부로 노출하는 것은 몇 가지 부작용을 초래할 수 있다.

  1. 리소스 주소 노출로 인한 보안 취약성
    예를 들어, Http DELETE method를 사용하는 /api/post/1과 같은 요청은
    누구나 1번 포스트를 삭제할 수 있다는 것을 의미한다.
    인증 및 권한 관리가 제대로 이루어지지 않으면,
    악의적인 사용자PK 값을 이용해 자원을 무단으로 수정하거나 삭제할 수 있다.
    따라서 이를 막기 위해 API에 적절한 권한 관리와 인증 절차를 반드시 구현해야 한다.
  2. Auto Increment 패턴 노출
    많은 데이터베이스에서 PK는 Auto Increment 방식으로 관리된다.
    이 경우 PK가 일정한 순서로 증가하는데,
    이를 악용하면 공격자는 현재 DB에 저장된 데이터의 개수를 유추하거나,
    이후 생성될 리소스의 ID를 예측할 수 있다.
    이는 크롤링 도구로 악용될 수 있어 보안상의 문제가 될 수 있다.

따라서 URI에 Primary Key를 노출시킨다면 보안적인 취약성이 나타나고,

추가적인 권한 관리 및 인증 절차의 구현이 반드시 필요하다.

이러한 문제를 해결하기 위해 PK 값을 암호화하는 방법을 고려할 수 있다.

PK 자체를 외부에 그대로 노출하지 않고, 암호화된 값을 노출하는 방식이다.

이 방법은 적절한 암호화 알고리즘비밀 키를 사용해 PK를 보호함으로써 보안을 강화할 수 있다.

 

PK 암호화 구조 / Java AES 대칭키

PK의 암호화 구조부터 살펴보면 다음과 같다.

  1. 클라이언트에서 전체 데이터 조회 혹은 하나의 데이터 조회를 요청
  2. 서버는 DB에서 데이터를 조회한 후,
    응답 데이터를 생성하는 과정에서 PK 값을 암호화
  3. 암호화된 PK를 포함한 데이터를 클라이언트에게 응답으로 반환
  4. Update / Delete와 같은 리소스 수정 및 삭제에 대한 요청 또한
    암호화된 PK로 URI를 구성하도록 한다.

이 방식은 클라이언트가 PK 값 자체를 알 수 없도록 하여, 데이터의 안전성을 확보할 수 있다.

Java 환경에서는 javax.crypto 패키지를 사용해 AES 대칭키 암호화를 구현할 수 있다.

AES 암호화는 보안성이 높으면서도 성능에 크게 부담을 주지 않기 때문에

REST API의 PK 암호화에 적합하다. 이를 코드에 적용시켜서 살펴보자.

@Entity
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Post extends BaseEntity{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String content;
}

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

    private final PostService postService;

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

 

위는 기본적인 Post Entity와 컨트롤러 메서드이다.

PK는 Long Type으로 DB에 자동으로 1씩 증가하며 저장된다.

만약 아래 존재하는 URI를 통해 API를 요청한다면 /test/1과 같이 PK를 노출하는 형태가 될 것이다.

따라서 PK 노출을 막기 위해 이를 암호화할 필요가 있다.

public class EncryptionUtil {

    private static final String ALGORITHM = "AES";
    private static final byte[] SECRET_KEY = "NaHCSecretKey123".getBytes();

    public static String encrypt(Long value) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] valueBytes = ByteBuffer.allocate(Long.BYTES).putLong(value).array();
        byte[] encryptedData = cipher.doFinal(valueBytes);
        return Base64.getEncoder().encodeToString(encryptedData);
    }

    public static Long  decrypt(String encryptedData) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decodedData = Base64.getDecoder().decode(encryptedData);
        byte[] decryptedBytes = cipher.doFinal(decodedData);
        return ByteBuffer.wrap(decryptedBytes).getLong();
    }
}

 

해당 코드가 Java에서 AES 대칭키 암호화 방식을 사용하여

Long 타입의 값을 암호화하고 복호화하는 기능을 구현한 클래스이다.

AES 알고리즘을 사용하며, AES는 128비트, 192비트, 256비트의

고정 길이를 가지는 블록 암호화 알고리즘이다.

여기서는 128비트(16바이트) 길이의

비밀 키(SECRET_KEY)를 사용해 데이터를 암호화하고 복호화한다.

코드들을 자세히 살펴보면 다음과 같다.

private static final byte[] SECRET_KEY = "NaHCSecretKey123".getBytes();

 

SECRET_KEYAES 암호화를 위한 비밀 키로, 16바이트 길이로 정의되어 있다.

비밀 키는 암호화와 복호화에 동일하게 사용되는 대칭키이다.

"NaHCSecretKey123"는 16글자 문자열로 되어 있으며,

getBytes() 메서드를 통해 바이트 배열로 변환하여 암호화 알고리즘에 적용시킨다.

AES는 비밀 키의 길이가 정확히 16바이트여야 하므로,

키의 길이가 맞지 않으면 오류가 발생할 수 있다.

public static String encrypt(Long value) throws Exception {
    SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY, ALGORITHM);
    Cipher cipher = Cipher.getInstance(ALGORITHM);
    cipher.init(Cipher.ENCRYPT_MODE, secretKey);
    byte[] valueBytes = ByteBuffer.allocate(Long.BYTES).putLong(value).array();
    byte[] encryptedData = cipher.doFinal(valueBytes);
    return Base64.getEncoder().encodeToString(encryptedData);
}

 

encrypt() 메서드는 Long 타입의 값을 AES 알고리즘으로 암호화하고,

이를 Base64 문자열로 인코딩하여 반환한다.

먼저 AES 알고리즘에서 사용할 비밀 키를 생성해야 한다.

SECRET_KEY는 바이트 배열로 제공되며,

이를 SecretKeySpec 객체로 변환해 AES 알고리즘에 사용할 수 있도록 한다.


암호화 모드(ENCRYPT_MODE)Cipher 객체를 초기화하며,

암호화에 사용할 비밀 키(secretKey)를 전달해 준다.

암호화를 진행하기 위해서 Long 타입의 값을 바이트 배열로 변환한다.

그 후 valueBytes(변환된 Long 값)를 AES 알고리즘으로 암호화한다.

이에 대한 결과는 암호화된 바이트 배열이다.

마지막으로 암호화된 바이트 배열을 Base64 인코딩하여 문자열로 반환한다.

이를 통해 PK 값을 암호화하여 노출되는 것을 막을 수 있다.

public static Long  decrypt(String encryptedData) throws Exception {
    SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY, ALGORITHM);
    Cipher cipher = Cipher.getInstance(ALGORITHM);
    cipher.init(Cipher.DECRYPT_MODE, secretKey);
    byte[] decodedData = Base64.getDecoder().decode(encryptedData);
    byte[] decryptedBytes = cipher.doFinal(decodedData);
    return ByteBuffer.wrap(decryptedBytes).getLong();
}

 

decrypt() 메서드는 암호화된 Base64 문자열을 AES로 복호화하여

원래의 Long 값으로 변환해 반환하는 역할을 한다.

암호화 때와 마찬가지로, 복호화를 위해 비밀 키를 생성해 준 후,

Cipher 객체를 AES 알고리즘으로 초기화한다.

이번에는 복호화 모드(DECRYPT_MODE)Cipher 객체를 초기화하고,

암호화 때 사용한 동일한 비밀 키를 제공해 준다.

암호화된 Base64 문자열을 바이트 배열로 디코딩을 진행하는데.

이는 Base64로 인코딩 된 데이터를 다시 바이너리 데이터로 변환하는 과정이다.

마지막으로 AES 알고리즘을 사용해 디코딩된 데이터를 복호화한다.

결과는 복호화된 바이트 배열로, ByteBuffer로 감싸서 원래의 Long 값으로 변환해 준다.

 

위 코드까지가 AES 알고리즘을 사용해 문자열 데이터를 암호화하고 복호화하는 예시이다.

서버에 저장된 비밀 키를 이용해 PK 값을 암호화할 수 있으며,

클라이언트에게는 암호화된 값을 반환하여 보안을 유지할 수 있다.

포스트맨의 결과 확인을 보기 전,

컨트롤러 또한 알고리즘에 맞게 Id를 Long Type이 아닌 String으로 받도록 변경해 준다.

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

PostMan Image

 

원래라면 해당 URI는 test/1이라는 구성이 되고 URI에 Primary Key를 노출시키는 구조가 된다.

하지만 API 요청 및 응답에 모두 복호화된 PK를 제공함으로써,

보안성 유지 및 리소스에 대한 노출을 막을 수 있다.

 

REST API 설계에서 PK를 암호화하는 방식은

외부에 노출되는 정보를 최소화하고, 악의적인 공격을 방지하는 데 효과적이다.

Auto Increment 패턴이나 리소스 주소 노출에 따른 보안 취약성을 우려한다면,

적절한 암호화 기법을 사용해 이를 해결할 수 있다.

AES 알고리즘을 통한 PK 암호화는 통해 추가적인 리소스를 크게 요구하지 않으면서도,

서비스의 안전성을 높일 수 있는 구현이다.

 

 

참고자료

https://hogwart-scholars.tistory.com/entry/Spring-Boot-Auto-Increment-PK-Id%EB%A5%BC-%EB%85%B8%EC%B6%9C%ED%95%98%EC%A7%80-%EC%95%8A%EC%9C%BC%EB%A9%B4%EC%84%9C-API%EC%97%90-%ED%99%9C%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

반응형

loading