안녕하세요? 2차 인증(Two-Factor Authentication)을 위해 OTP에 대해 공부하던 중 알게된 사실들에 대해 정리해보려고 합니다.
OTP(One-Time Password, 일회용 비밀번호)의 동작 원리는 사용자가 인증을 시도할 때마다 한 번만 사용할 수 있는 비밀번호를 생성하고 검증하는 것을 의미 합니다. OTP는 보안성을 높이기 위해 고안된 방식으로, 주요하게 두 가지 방식이 있습니다: 시간 기반 OTP(Time-based OTP, TOTP)와 카운터 기반 OTP(HMAC-based OTP, HOTP). 여기서 TOTP가 더 일반적으로 사용됩니다. 오늘 알아볼 내용도 TOTP에 대해 알아봅니다 :)
제가 알아본 방법은 commons-codec 방식과 warrenstrange/googleauth 라이브러리 방식이 있는데요. 이 둘중에 무엇을 활용할지는 프로젝트 요구사항과 선호하는 개발 스타일에 따라 달라질 수 있습니다. 두 가지 접근 방식의 장단점은 아래와 같습니다.
Commons-Codec 방식
장점:
- 경량성: commons-codec는 일반적인 인코딩/디코딩 작업을 위한 라이브러리로, OTP 생성에 필요한 HMAC-SHA 알고리즘을 구현하는 데 사용할 수 있습니다. 이 라이브러리는 상대적으로 가볍습니다.
- 유연성: 라이브러리를 사용하여 직접 OTP 알고리즘을 구현하면, 특정 요구사항에 맞게 커스터마이징할 수 있습니다. HMAC-SHA1, SHA256, SHA512 등의 다양한 해시 알고리즘을 사용할 수 있습니다.
- 통제: OTP 생성 로직을 직접 구현하기 때문에 세부 사항을 커스텀 할 수 있습니다.
단점:
- 개발 복잡성: OTP 알고리즘을 직접 구현하려면 추가적인 코딩 작업이 필요하며, 표준 준수 및 보안성을 확보하기 위해 신중한 구현이 필요합니다.
- 버그 발생 가능성: 직접 구현할 경우 실수로 인한 버그가 발생할 가능성이 있습니다.
Warrenstrange/Googleauth
장점:
- 간편함: warrenstrange/googleauth는 구글 OTP 알고리즘(TOTP) 구현을 위한 라이브러리로, 설정과 사용이 간편합니다. 복잡한 구현 없이 쉽게 OTP 기능을 추가할 수 있습니다.
- 신뢰성: 이미 검증된 라이브러리를 사용함으로써 보안성과 신뢰성을 확보할 수 있습니다. 널리 사용되는 라이브러리이기 때문에 커뮤니티와 문서를 쉽게 찾을 수 있다는 장점이 있어요.
- 통합: 구글 OTP 표준을 준수하므로, 구글 인증 앱 등과 쉽게 통합할 수 있습니다.
단점:
- 제한된 유연성: 라이브러리의 기능과 설정에 제약이 따릅니다. 즉 특정 요구사항에 맞게 커스터마이징하기 어려울 수 있습니다.
- 외부 의존성: 외부 라이브러리에 의존하게 되므로, 라이브러리 업데이트 및 유지보수에 대한 추가적인 고려가 필요합니다.
결론
- 단순하고 빠른 구현이 필요한 경우: warrenstrange/googleauth 라이브러리
- 커스터마이징과 세부 제어가 필요한 경우: commons-codec
TOTP (Time-based OTP)의 동작원리
TOTP는 시간에 기반하여 OTP를 생성합니다. 사용자는 일정한 시간 간격마다 변경되는 비밀번호를 사용하여 인증을 받아야 하는 방식이죠. 일반적으로 우리는 사용자 측면에서만 TOTP를 이용해왔습니다. 우선 1차 로그인을 하면, 구글 OTP와 같은 앱을 켜서 정해진 시간마다 변동돼는 6자리 숫자를 모바일로 확인해서 웹화면에 입력하죠. 그렇다면 이러한 일련의 과정은 전체적으로 어떤 과정이 일어나기에 가능한걸까요? 이번에는 TOTP의 그 동작 원리에 대해 확인해 봅시다.
- 공유 비밀 키(Secret Key): 서버와 클라이언트(사용자)는 미리 공유된 비밀 키를 가지고 있어야 합니다.
- 시간 동기화: 서버와 클라이언트는 시간을 동기화해야 합니다. 일반적으로 Unix 타임스탬프(1970년 1월 1일 00:00:00 UTC부터의 초)를 사용합니다.
- 시간 슬롯 계산: 현재 시간을 일정한 간격(예: 30초)으로 나누어 슬롯을 계산합니다. 이 간격을 시간 틱(Time Tick)이라고 합니다.
- HMAC 계산: 현재 시간 슬롯과 비밀 키를 사용하여 HMAC-SHA1 알고리즘을 적용합니다. 여기서 HMAC은 키를 사용한 해시 기반 메시지 인증 코드입니다.
- OTP 생성: HMAC의 결과로부터 특정 길이(예: 6자리)의 숫자를 추출하여 OTP를 생성합니다. 일반적으로 OTP는 HMAC 결과의 일부를 사용하여 6자리 또는 8자리 숫자로 변환됩니다.
- 검증: 사용자가 입력한 OTP를 서버에서 동일한 방식으로 생성된 OTP와 비교하여 검증합니다. 시간 슬롯이 짧기 때문에 OTP는 매우 짧은 시간 동안만 유효합니다.
Warrenstrange/Googleauth를 이용한 키 제너레이트 및 검증
pom.xml
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.5.0</version>
</dependency>
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
public class GoogleAuthExample {
public static void main(String[] args) {
// Create an instance of GoogleAuthenticator
GoogleAuthenticator gAuth = new GoogleAuthenticator();
// Generate a new key
GoogleAuthenticatorKey key = gAuth.createCredentials();
String secret = key.getKey();
System.out.println("Secret key: " + secret);
// Generate a QR code URL to be scanned by Google Authenticator app
String userName = "user@example.com";
String issuer = "ExampleIssuer";
String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(issuer, userName, key);
System.out.println("QR Code URL: " + qrCodeUrl);
// Simulate the OTP that the user would provide from their Google Authenticator app
int otp = gAuth.getTotpPassword(secret);
System.out.println("Generated OTP: " + otp);
// Verify the provided OTP
boolean isCodeValid = gAuth.authorize(secret, otp);
System.out.println("Is the OTP valid? " + isCodeValid);
}
}
getOtpAuthURL로부터 알아낸 URL을 실제로 접속하면 QR코드로서 확인해 볼 수 있습니다. 그런데 이렇게 하면 api.qrserver.com에 귀속된 URL을 알려주게 되는데, api.qrserver.com은 구글 서버가 아닙니다. api.qrserver.com은 goqr.me라는 웹사이트에서 제공하는 QR 코드 생성 서비스의 API 라고하네요. goqr.me는 QR 코드를 생성하고 관리하는 데 특화된 서비스로, 다양한 형태의 QR 코드를 쉽게 만들 수 있도록 도와줍니다. 하지만, 이런 의존적인 방식이 마음에 안들경우 직접 QR을 생성할 필요성이 있는데, 이 부분에 대해서는 ZXing 라이브러리를 사용하여 자체적으로 QR을 생성하는 방식으로 해결하였습니다!
QR 코드 자체생성하기
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.4.1</version>
</dependency>
</dependencies>
QR을 만들어내는 서비스 로직을 아래와 같이 만들어봅시다.
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class QRCodeService {
public byte[] generateQRCode(String text, int width, int height) throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hints);
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
return pngOutputStream.toByteArray();
}
}
이제, 컨트롤러 입니다.
import com.google.zxing.WriterException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RestController
public class QRCodeController {
@Autowired
private QRCodeService qrCodeService;
@GetMapping("/generateQRCode")
public ResponseEntity<byte[]> generateQRCode(@RequestParam String text) {
try {
byte[] qrCodeImage = qrCodeService.generateQRCode(text, 350, 350);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
return ResponseEntity.ok().headers(headers).body(qrCodeImage);
} catch (WriterException | IOException e) {
return ResponseEntity.status(500).body(null);
}
}
}
이제 http://localhost:8080/generateQRCode?text={인코딩하고자 하는 데이터} 를 요청하면, 자체적으로 생성된 QR PNG를 확인해 볼 수 있습니다 :)
어때요? 근사하죠? 읽어주셔서 감사합니다.
'Develop Issue > Open Source, API' 카테고리의 다른 글
git merge 취소하기 (2) | 2020.06.01 |
---|---|
네이버 Map, 카카오 Map, Google Map API 비교 (1) | 2019.09.23 |
날짜별 지역별 날씨 API 제공 사이트 총정리 (2) | 2016.01.19 |