회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
외부에 제공하는 REST API를 개발해야 할 때, 인증은 어떻게 해야 할까요? 외부와 계약을 맺은 특정 업체에만 제공해야 하는 API인데, 외부에서 접근하는 만큼 인증이 중요했습니다. 로그인이 아닌 서버 to 서버로 제공하는 API는 어떻게 인증하는 걸까? 고민하던 차에 오픈 API가 생각이 났습니다. 오픈 API도 이와 비슷한 상황이지 않을까 하는 생각을 바탕으로 조사한 내용을 정리한 글입니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
외부에 제공하는 REST API를 개발해야 할 때, 인증은 어떻게 해야 할까요? 외부와 계약을 맺은 특정 업체에만 제공해야 하는 API인데, 외부에서 접근하는 만큼 인증이 중요했습니다. 로그인이 아닌 서버 to 서버로 제공하는 API는 어떻게 인증하는 걸까? 고민하던 차에 오픈 API가 생각이 났습니다. 오픈 API도 이와 비슷한 상황이지 않을까 하는 생각을 바탕으로 조사한 내용을 정리한 글입니다.
오픈 API는 외부 개발자가 애플리케이션이나 서비스와 상호작용할 수 있도록 공개한 API를 말합니다. 현재는 대부분 REST API로 구성하며, 오픈 API를 통해 데이터를 요청하거나 특정 작업을 수행할 수 있도록 제공합니다.
서비스를 운영하는 많은 회사에서 오픈 API를 제공하고 있습니다.
한때는 API 이코노미, API-First라는 용어라고 불리며, 오픈 API를 많이 만들던 시기도 있었다고 합니다. 초반에는 자신들의 서비스를 알리기 좋은 수단이기도 하고 했지만, 시간이 지나면서 외부 트래픽의 증가는 미미하고 유지보수에 대한 문제점도 발생하면서 제공을 줄이는 추세라고 합니다. 특히나 회사가 커질수록 MSA와 같은 아키텍처나 내부에서 여러 서비스를 개발하면서 오픈 API를 내부에서 많이 사용하게 되다 보니 외부 제공보다 내부에 집중하는 경향도 있다고 합니다. (API 이코노미에 관한 내용은 링크를 읽어보면 좋을 것 같습니다.)
그럼에도 여전히 수많은 오픈 API가 존재하며, 또한 생겨나고 있습니다. ChatGPT와 같은 LLM 서비스가 나오면서 이를 사용하는 오픈 API의 사용량은 실로 어마어마할 것입니다.
여러 서비스에서 제공하는 오픈 API를 사용하기 위해서는 대부분 비슷한 시나리오를 따르고 있습니다.
위처럼 발급받은 키는 HTTP API 요청 시 헤더 또는 요청 파라미터와 같은 방식으로 서버에 전달하여 인증 과정을 거쳐야 정상적으로 동작하게 됩니다. 사실 이는 로그인 인증 과정과 크게 다르지 않습니다.
위의 로그인 과정은 앞서 살펴봤던 오픈 API의 사용 과정과 대칭을 이룹니다.
접근 키를 생성할 수 있는 방식은 다양합니다. API Key, JWT, OAuth 2.0 등의 방식을 사용할 수 있습니다.
API Key는 식별할 수 있는 단순 문자열입니다. 이를 만드는 방법은 해시, UUID, 난수, 규칙을 갖는 문자열 등 만들기 나름입니다.
예시
JWT 는 서버와 클라이언트 사이에서 안전한 데이터 전송을 위해 사용하는 토큰 기반 인증 방식입니다. JWT는 서명된 JSON 포맷 토큰으로, 인증과 토큰의 무결성을 보장합니다.
JWT 구조는 3개의 데이터로 이루어져 있습니다.
헤더는 토큰의 타입과 서명에 사용된 알고리즘 정보를 담고 있습니다.
{
"alg": "HS256", // 서명 알고리즘, 여기서는 HMAC SHA-256
"typ": "JWT"
}
페이로드는 사용자 정보(클레임, Claim)을 담고 있습니다. 일반적으로 고유 식별자와 토큰 만료 시간과 같은 토큰의 메타 데이터를 담고 있습니다. 필요에 맞게 구성할 수 있습니다.
{
"sub": "1234567890", // (Subject): 사용자 ID와 같은 고유 식별자
"exp": 1717594100 // (Expiration): 토큰 만료 시간
"name": "John Doe", // 사용자 이름
"admin": true, // 어드민 권한 여부
}
서명은 헤더와 페이로드를 비밀키로 서명하여 생성합니다. 이 서명은 토큰의 무결성을 보장하며, 서버에서 토큰이 변조되지 않았는지 확인할 수 있습니다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
OAuth는 사용자가 비밀번호(접근 정보)를 공유하지 않고도 애플리케이션에 접근하기 위해 제3의 애플리케이션에 이미 등록된 자신의 정보로 인증 및 권한 부여를 하는 프로토콜입니다.
현재는 OAuth 라고 하면 대부분 2.0 버전을 사용하여 OAugh 2.0으로 불립니다. OAuth는 주로 카카오톡 로그인, 구글 로그인과 같은 SNS 간편 로그인으로 사용하고 있습니다. 예를 들어, 파커 쇼핑 앱을 만들었는데 접근성을 위해서 회원가입을 따로 하지 않고 카카오톡 로그인으로도 회원 등록이 되도록 개발하고 싶습니다.
구현마다 상세 사항은 달라질 수 있지만, 일반적인 흐름은 다음과 같습니다.
일반적인 로그인 팝업이 발생하는 상황이 아닌 API to API 상황에서도 OAuth를 통해서 인증이 가능할 수 있습니다. Authorization Server에서 로그인 팝업 대신 API로도 로그인이 가능하게 제공해 준다거나, 구현하는 Client에서 추가적인 작업이 더 필요할 수 있을 것입니다. (OAuth 2.0에 대한 자세한 내용은 블로그에 잘 정리되어 있으니, 궁금하신 분은 읽어보면 좋을 것 같습니다.)
지금까지 살펴본 인증 방식을 토대로 각각의 장단점을 살펴보겠습니다.
API Key
장점
단점
JWT
장점
단점
OAuth 2.0
장점
단점
오픈 API의 인증도 로그인과 같은 인증 과정을 거치지만, 상황은 확실히 다릅니다. 로그인 기능은 하나의 서비스에서 이루어지기 때문에 구현 및 관리가 용이합니다. 큰 서비스라고 해도 팀이 나뉘어있을지언정 같은 회사에서 이루어지는 인증 방식을 구현할 수 있습니다.
반면에 오픈 API는 외부에서 인증 방식을 구현해야 합니다. 필요에 따라서 복잡해질 수 있지만, 외부에서 이를 사용하기 어려울 수 있습니다. 사용 방법에 대해 변경 사항이 생겼을 때도 관리하기가 내부보다는 훨씬 어렵습니다. 이러한 점을 고려했을 때, API Key 방식이 적합하다고 생각했습니다. 인증 방식 구현이 간단하고, 관리 역시 직접 가능합니다.
API Key 방식은 사용하기 간단한 만큼 문제점도 있습니다. 대표적으로 위에서 언급했던 3가지의 단점입니다.
위 단점을 해결하는 데는 여러 방법이 존재합니다. 하지만 이를 적용하는 만큼 장점이었던 간단함이 점점 퇴색될 수 있습니다. 그렇기 때문에 상황에 따라 필요한 만큼만 적용하는 것이 좋을 것입니다. 여기서는 단점마다 한가지씩 적용해 볼만한 대표적인 방법들을 소개하겠습니다.
키 로테이션 또는 롤링은 인증에 사용되는 API Key를 주기적으로 변경해 주는 방법을 말합니다. 이를 통해서 키 유출에 대한 위험을 줄일 수 있습니다.
API Key를 생성하는 방식도 중요합니다. 안전한 API Key의 특성은 다음과 같습니다.
안전한 키 생성 방식 몇 가지를 알아보겠습니다.
UUID는 전 세계적으로 유일한 식별자를 생성하는 표준 방식입니다.
// UUID 생성
String uuid = UUID.randomUUID().toString();
System.out.println("Generated UUID: " + uuid);
// 생성 키: 33cf534f-894e-4047-8b70-69c9f960681d
2. SecureRandom(암호화 난수 생성)
SecureRandom secureRandom = new SecureRandom(); // 암호화 난수 생성
byte[] keyBytes = new byte[32]; // 256비트(32바이트) 키 생성
secureRandom.nextBytes(keyBytes);
String key = Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes);
System.out.println("Generated SecureRandom Key: " + key);
// 생성 키: gdCqlByiucDVFlt0BN0dN28mDIT9bogaqW5-pMfL5W0
3. SHA256 해시
// 입력값(고유 식별자 + 비밀 키) 결합 필요
String input = "unique_user_identifier" + "your_secret_key";
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
String key = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
System.out.println("Generated SHA-256 Hash: " + key);
// 생성 키: r396NURYomPq8UYAJ1AT-x4bk4dVwdhA3hphPWLXcNk
4. UUID + SHA 256 해시 조합
// UUID 생성
String uuid = UUID.randomUUID().toString();
// SHA-256 해시 생성
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(uuid.getBytes(StandardCharsets.UTF_8));
// Base64로 인코딩 (URL-safe 포맷)
String key = Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
System.out.println("Generated UUID and SHA-256 Hash: " + key);
// 생성 키: WMf8eEMAgSTZHVqLFZjwzPXNE0RPwV9x1ms84AxnTfs
이와 같이 예측 불가능한 키값을 바탕으로 로테이션을 돌려야 더욱 안전한 키 관리가 가능합니다. 키 로테이션은 일반적으로 다음과 같은 과정으로 진행됩니다.
위 과정을 자동화하는 것이 관리하기 더욱 쉬워질 것입니다. 키 갱신 API를 제공하여 로테이션을 자동으로 해줄 수도 있을 것입니다. 또한, 모니터링을 통해 키가 정상적으로 사용되고 있는지, 로테이션이 정상적으로 이루어졌는지 등을 빠르게 파악하는 것도 중요합니다.
HMAC은 일반적으로 해시 함수(예:SHA-256, SHA-1) 기반으로, 비밀 키와 메시지를 암호화하여 서버와 클라이언트 사이 메시지 무결성과 인증을 보장하는 방식입니다.
HMAC 인증은 두 가지 입력이 필요합니다.
비밀키는 서버와 클라이언트가 공유하는 비밀키로, 메시지를 해시 할 때 사용됩니다. 이 비밀 키를 알고 있는 사람만이 HMAC을 계산할 수 있으므로, 인증의 역할도 수행할 수 있습니다.
만약 HMAC 방식을 사용한다고 하면, API Key를 이 비밀키로 대체할 수 있습니다. 하지만 보안을 위해 좀 더 까다롭게 해주려면 둘 다 사용하는 것이 좋을 수 있습니다.
만약 공격자가 메시지와 서명을 가로챈 뒤, 메시지만 자신이 원하는 메시지로 변경하여 서버에게 요청했다고 가정해 봅시다. 그러면 서버에서는 변조된 메시지와 비밀키로 서명을 만드는데, 이때 만들어진 서명은 원본 메시지와 다르므로 요청한 서명과는 전혀 다른 서명 값이 만들어질 것입니다.
HMAC 인증은 이를 통해 메시지의 무결성을 지켜줍니다. 여기서 좀 더 안정성을 지키는 대표적인 방법은 타임스탬프나 Nonce(Number used once, 한 번만 사용되는 값) 값을 사용하는 것입니다. 해시 인코딩에 메시지와 함께 이러한 값을 포함해서 보내게 되면, 같은 메시지라도 다른 서명이 만들어집니다. 따라서 서명만 보고는 더욱 어떤 값인지는 더욱 예측하기 어려워집니다. 그리고 똑같은 메시지를 재사용할 수 없게 막아줄 수 있습니다.
이에 더해 타임스탬프를 활용하면 특정 시간에만 보낸 메시지만 허용한다는 규칙을 더 추가해 줄 수도 있습니다. 가령 5분 이내에 요청만 유효하게 처리할 것이라는 규칙을 만들 수 있습니다.
참고로 HMAC을 사용하게 되면 클라이언트 역시 구현이 필요합니다. HMAC은 널리 사용되는 인증 방식이기 때문에 자바 기준으로 기본 패키지에 포함되어 있어 편리하게 구현할 수 있습니다.
public class HMACTest {
private static final String SECRET_KEY = "supersecretkey"; // 서버에서 관리하는 비밀 키
@Test
void generate_and_verify() {
try {
// generateHMAC 메서드를 사용해 HMAC 서명 생성
String message = "This is the API request data";
String hmacSignature = generateHMAC(message, SECRET_KEY);
System.out.println("Generated HMAC Signature: " + hmacSignature);
// verifyHMAC 메서드를 사용해 HMAC 서명 검증
Assertions.assertThat(verifyHMAC(message, hmacSignature)).isTrue();
} catch (Exception e) {
System.out.println("Error occurred while generating or verifying HMAC signature: " + e.getMessage());
}
}
// HMAC 서명 생성 함수
private String generateHMAC(String data, String secretKey) throws Exception {
// HMAC 알고리즘 지정 (SHA-256)
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
// SecretKeySpec을 사용해 비밀 키 설정
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256_HMAC.init(secretKeySpec);
// 입력 데이터에 대해 HMAC 서명 생성
byte[] hmacBytes = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
// Base64로 인코딩하여 반환
return Base64.getEncoder().encodeToString(hmacBytes);
}
// HMAC 검증 메서드
private boolean verifyHMAC(String data, String receivedHMAC) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256_HMAC.init(secretKeySpec);
byte[] hmacBytes = sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
String calculatedHMAC = Base64.getEncoder().encodeToString(hmacBytes);
return calculatedHMAC.equals(receivedHMAC); // 받은 HMAC과 계산된 HMAC 비교
}
}
API Key 방식의 단점 중 하나는 DB와 같은 접근하는 데 비용이 많이 드는 것입니다. 이 비용을 줄여줄 수 있는 대표적인 방식이 캐시를 사용하는 것입니다. Redis와 같은 메모리 캐시를 사용하게 되면 키를 확인하는데 시간이 훨씬 줄어들 것입니다. (물론 키의 변화를 캐시에 계속 반영해 주어야하므로 관리 비용이 증가합니다. 따라서 무조건 캐시를 활용하기보다는 상황에 맞게 적용하는 것이 중요합니다.)
인증 방식 하나로는 당연히 보안적으로 모든 것을 해결할 수 없습니다. 보안은 백번, 천 번을 강조해도 과함이 없습니다. 인증 방식을 제외하고 추가할 수 있는 보안들을 살펴보겠습니다.
보안적으로 중요한 요청은 반드시 HTTPS를 사용해야 합니다. HTTPS 요청은 요청한 HTTP 프로토콜 내용을 암호화하여, 공격자에게 탈취당하더라도 내용을 볼 수 없게 막아주는 역할을 합니다.
사실 API Key나 비밀키 발급 경우에도 REST API를 사용해서 주고받는 경우가 많은데, 이런 경우는 반드시 HTTPS를 사용해야합니다. 클라이언트에서 HTTP로 요청보낼 시, HTTPS로 리다이렉트 설정을 해줄 수 있습니다. 이는 NGINX와 같은 웹서버나 애플리케이션에서 설정해 줄 수 있습니다.
서버에서 HTTPS로 리다이렉션 후, HSTS(HTTP Strict Transport Security) 헤더를 설정해 주면 설정한 시간 동안은 브라우저에서 HTTPS로 호출을 강제할 수도 있습니다.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000
: 브라우저가 앞으로 1년간(31,536,000초) 이 사이트를 HTTPS로만 접속하도록 합니다.includeSubDomains
: 하위 도메인도 HTTPS로만 접속하도록 지시합니다.preload
: 브라우저의 HSTS 프리로드 리스트에 추가해달라는 요청으로, 이를 통해 브라우저에 미리 HTTPS로 강제 호출하라는 정보를 내장할 수 있습니다.
IP 화이트 리스트는 허용할 IP만 서버로 접근 가능하도록 설정하는 것입니다. 이 설정은 NGINX와 같은 웹서버, 애플리케이션 코드, 클라우드 서비스 등에서 설정할 수 있습니다. IP 리스트를 설정할 때, 주의할 점은 요청이 프록시 서버나 로드벨런서를 거쳐 갈 경우 IP가 변경된다는 것입니다.
원본 소스 IP(네트워크 계층)는 Remote Address라는 곳에 저장됩니다. 만약 프록시 서버나 로드벨런서를 거친다면 HTTP 요청 헤더를 살펴봐야 합니다.
X-Forwarded-For
헤더X-Real-IP
헤더
일반적으로 프록시 서버나 로드 밸런서를 사용하는 경우 클라이언트의 실제 IP는 X-Forwarded-For
헤더에 포함됩니다. 여러 개의 프록시 서버를 거칠 경우 X-Forwarded-For
헤더에는 클라이언트의 실제 IP와 각 프록시 서버의 IP가 순서대로 추가됩니다.
X-Forwarded-For: 클라이언트IP, 중간프록시IP, 최종프록시IP
일부 서버나 프록시 환경에서는 X-Real-IP
헤더에 클라이언트의 실제 IP를 추가합니다. 예를 들어, Nginx는 기본 설정에서 X-Real-IP
헤더를 통해 클라이언트의 IP를 전달할 수 있습니다. 대부분 설정에서는 위 헤더에 설정된 값도 가져올 수 있습니다.
양방향 인증은 서버와 클라이언트 간의 쌍방향 인증을 수행하여 양쪽이 서로의 신원을 확인하고 보안을 강화하는 SSL/TLS 인증 방식입니다. 일반적으로 HTTPS를 사용할 때는 서버만 클라이언트에게 인증서를 제시해 클라이언트가 서버를 신뢰할 수 있도록 하는 반면, Mutual SSL에서는 클라이언트도 자신의 인증서를 서버에 제시하여 서버가 클라이언트를 신뢰할 수 있도록 합니다.
양방향 인증은 금융 서비스, 정부 기관, 기업 네트워크 등 민감한 데이터가 오가는 높은 보안이 필요한 상황에서 많이 사용됩니다. 클라이언트 역시 비밀번호나 키가 아닌 인증서로 인증을 진행하므로 유출에 대한 위험이 적습니다.
단, 양방향으로 인증하는 만큼 설정과 구현이 복잡하고 인증서의 관리가 필요합니다. 또한, 클라이언트의 SSL/TLS 핸드셰이크 과정이 추가되는 등의 성능적으로도 문제가 될 수 있는 단점을 가지고 있습니다.
지금까지 오픈 API는 인증을 어떻게 할까?라는 의문에서 시작하여, 개인적으로는 API key 방식이 적합하다고 판단했습니다. 그리고 API Key 방식의 단점을 보완하는 방법과 보안을 더 높일 수 있는 방법에 대해서도 같이 살펴보았습니다. 요즘은 대부분 HTTPS를 당연히 쓰고 있는 환경에 있습니다. 이는 HTTP 패킷이 노출되더라도 내부 데이터의 유출까지 이어지기에는 어렵다고 볼 수 있습니다.
이러한 상황에서 보안이 매우 중요한 상황이 아니라면 API Key 방식으로도 충분하지 않을까 생각합니다. 여기서 보안이 중요하다는 것은 오픈 API로 제공되는 기능으로 내부의 시스템이 망가질 수 있느냐를 생각해 보아야 할 것입니다. 사실 알림 또는 이메일 전송, 개인 리소스 할당과 같은 기능이 유출되어 악용되면, 내부 시스템이 아닌 API Key를 갖고 있는 클라이언트에게 피해가 큽니다. 그렇게 때문에 클라이언트가 직접 API Key를 노출시키지 않기 위해 노력을 많이 해야 합니다. 그리고 우리가 사용하는 많은 오픈 API 기능은 여기에 속합니다.
내부 시스템을 충분히 어지럽히거나 망가뜨릴 수 있는 오픈 API 기능은 당연히 여기서 보안을 더욱 높여야 합니다. 이를 위해서 앞서 살펴본 키 로테이션, HMAC, IP 화이트 리스트, Mutual SSL 등을 필요에 맞게 적용해야 할 것입니다.
<참고 자료>
<원문>
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.