[JAVA] Google OTP (TOTP) 구현하기
Google OTP는 2단계 인증을 통해 보안을 강화하는 중요한 요소입니다. 이번 포스트에서는 Java로 Google OTP를 구현하는 방법을 소개합니다. Google OTP 구현 시 사용하는 두 가지 방법인 Warrenstrange Googleauth 라이브러리와 Apache Commons Codec을 비교하고, 사용하는 방법을 설명하겠습니다.
1. OTP란?
OTP(One Time Password)는 특정 시간 동안만 유효한 일회성 비밀번호입니다. Google Authenticator와 같은 앱에서 생성되는 OTP는 Time-Based One-Time Password (TOTP) 알고리즘을 기반으로 작동하며, 서버와 클라이언트가 동일한 Secret Key와 시간 기준을 공유하여 생성됩니다.
2. Google OTP 구현을 위한 라이브러리 비교
기능 | Warrenstrange Googleauth | Apache Commons Codec |
추상화 수준 | 고수준 | 저수준 |
설정 및 사용 편의성 | 간단함 | 복잡함 |
시간 동기화 처리 | 내장 | 직접 구현 필요 |
적합한 사용 사례 | Google OTP와 빠른 연동 | 커스터마이징이 필요한 경우 |
Warrenstrange Googleauth
- Google OTP를 손쉽게 구현할 수 있는 고수준 라이브러리.
- Secret Key 생성, OTP 검증 등 대부분의 작업을 간단히 처리.
- 시간 동기화 및 OTP 검증 기능이 내장되어 있어 별도의 추가 구현이 필요 없음.
Apache Commons Codec
- Base32 인코딩/디코딩 및 HMAC 알고리즘을 제공하는 범용 라이브러리.
- TOTP를 직접 구현해야 하며, 시간 동기화 및 검증 로직을 직접 작성해야 함.
- 높은 커스터마이징이 가능하지만, 구현 복잡도가 높음.
3. Warrenstrange Googleauth 라이브러리로 Google OTP 구현
3.1. Maven 의존성 추가
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.5.0</version>
</dependency>
3.2. Secret key 생성
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
public class GoogleOtpService {
public String generateSecretKey() {
GoogleAuthenticator gAuth = new GoogleAuthenticator();
GoogleAuthenticatorKey key = gAuth.createCredentials();
return key.getKey(); // 사용자에게 제공할 Secret Key
}
public GoogleAuthenticatorKey generateKey() {
GoogleAuthenticator gAuth = new GoogleAuthenticator();
return gAuth.createCredentials();
}
}
3.3. QR 코드 URL 생성
Google Authenticator 앱에 등록할 수 있도록 QR 코드 URL을 생성합니다.
public static String getOtpAuthUrl(String issuer, String accountName, GoogleAuthenticatorKey key) {
return GoogleAuthenticatorQRGenerator.getOtpAuthURL(issuer, accountName, key);
}
3.4. OTP 검증
import com.warrenstrange.googleauth.GoogleAuthenticator;
public class GoogleOtpService {
private final GoogleAuthenticator gAuth = new GoogleAuthenticator();
public boolean verifyCode(String secretKey, int code) {
return gAuth.authorize(secretKey, code); // Secret Key와 OTP를 검증
}
}
3.5. 통합 코드 및 테스트
import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
public class GoogleOtpService {
private static final GoogleAuthenticator gAuth = new GoogleAuthenticator();
// Key 생성
public static GoogleAuthenticatorKey generateKey() {
return gAuth.createCredentials();
}
// OTP URL 생성
public static String getOtpAuthUrl(String issuer, String accountName, GoogleAuthenticatorKey key) {
return GoogleAuthenticatorQRGenerator.getOtpAuthURL(issuer, accountName, key);
}
// OTP 코드 조회
public static int getCode(String secretKey) {
return gAuth.getTotpPassword(secretKey);
}
// OTP 검증
public static boolean verifyCode(String secretKey, int code) {
return gAuth.authorize(secretKey, code);
}
}
class GoogleOtpServiceTest {
@Test
void googleOtpTest() {
// Secret Key 생성
GoogleAuthenticatorKey key = GoogleOtpService.generateKey();
String secretKey = key.getKey();
System.out.println("Secret Key: " + secretKey);
// OTP URL 생성
String otpUrl = GoogleOtpService.getOtpAuthUrl("FLOOR", "user@example.com", key);
System.out.println("OTP URL: " + otpUrl);
// OTP 검증 (테스트용)
// Google Authenticator 앱에서 생성된 코드를 사용
int code = GoogleOtpService.getCode(secretKey); // 테스트 코드
boolean isVerified = GoogleOtpService.verifyCode(secretKey, code);
Assertions.assertTrue(isVerified);
}
}
4. Apache Commons Codec 라이브러리로 Google OTP 구현
4.1. Maven 의존성 추가
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
4.2. Secret key 생성
public String generateKey() SecureRandom();
int len = 32;
// SecureRandom을 사용하여 랜덤 바이트 배열 생성
SecureRandom random = new SecureRandom();
byte[] secretKeyBytes = new byte[len / 8 * 5];
random.nextBytes(secretKeyBytes);
// Base32로 인코딩
Base32 base32 = new Base32();
return base32.encodeToString(secretKeyBytes).replace("=", ""); // byte size가 5의 배수이면 패딩(=) 생성 안함.
}
4.3. OTP 코드 발급
private static final int DEFAULT_INTERVAL = 30 * 1000; // 30초
private static final int DEFAULT_CODE_LENGTH = 6;
private static final String DEFAULT_ALGORITHM = "HmacSHA1"; // Google OTP 기본 알고리즘
/**
* 현재 시간 기준 OTP 코드 발급
*
* @param secretKey Secret Key
* @return OTP 코드
*/
public String getCurrentCode(String secretKey) {
return getCurrentCode(secretKey, DEFAULT_INTERVAL);
}
public String getCurrentCode(String secretKey, int interval) {
return generateTotp(secretKey, getStep(Instant.now().toEpochMilli(), interval));
}
/**
* 다음 시간에 발급될 코드
*
* @param secretKey Secret Key
* @return OTP 코드
*/
public String getNextCode(String secretKey) {
return getNextCode(secretKey, DEFAULT_INTERVAL);
}
public String getNextCode(String secretKey, int interval) {
return generateTotp(secretKey, getStep(Instant.now().toEpochMilli() + interval, interval));
}
/**
* 이전 시간에 발생한 코드
*
* @param secretKey Secret Key
* @return OTP 코드
*/
public String getBeforeCode(String secretKey) {
return getBeforeCode(secretKey, DEFAULT_INTERVAL);
}
public String getBeforeCode(String secretKey, int interval) {
return generateTotp(secretKey, getStep(Instant.now().toEpochMilli() - interval, interval));
}
/**
* TOTP 생성
*
* @param secretKey Secret Key (Base32 인코딩)
* @param step 시간 스텝
* @return OTP 코드
*/
private String generateTotp(String secretKey, long step) {
try {
Base32 base32 = new Base32();
byte[] decodedKey = base32.decode(secretKey);
// 스텝 값을 8바이트 배열로 변환
byte[] stepBytes = new byte[8];
for (int i = 7; i >= 0; i--) {
stepBytes[i] = (byte) (step & 0xFF);
step >>= 8;
}
// HMAC 생성
Mac mac = Mac.getInstance(DEFAULT_ALGORITHM);
mac.init(new SecretKeySpec(decodedKey, DEFAULT_ALGORITHM));
byte[] hmac = mac.doFinal(stepBytes);
// OTP 계산 (HMAC의 특정 바이트 추출)
int offset = hmac[hmac.length - 1] & 0x0F;
int otp = ((hmac[offset] & 0x7F) << 24)
| ((hmac[offset + 1] & 0xFF) << 16)
| ((hmac[offset + 2] & 0xFF) << 8)
| (hmac[offset + 3] & 0xFF);
// OTP 코드 6자리로 변환
otp %= (int) Math.pow(10, DEFAULT_CODE_LENGTH);
return String.format("%06d", otp);
} catch (GeneralSecurityException e) {
throw new RuntimeException("OTP 생성 실패", e);
}
}
/**
* 시간 스텝 계산
*
* @param baseMilliseconds 기준 시간 (밀리초)
* @param interval 시간 간격 (밀리초)
* @return 스텝 값
*/
private long getStep(long baseMilliseconds, int interval) {
return baseMilliseconds / interval;
}
4.4. OTP 검증
public boolean isAuthenticated(String secretKey, String code) {
// 현재, 이전, 다음 OTP 생성
Set<String> possibleCodes = Set.of(
getBeforeCode(secretKey),
getCurrentCode(secretKey),
getNextCode(secretKey)
);
// 입력 코드가 생성된 코드 리스트에 있는지 확인
return possibleCodes.contains(code);
}
Google OTP를 Java로 구현할 때, Googleauth 라이브러리는 간단하고 직관적인 방식으로 OTP 기능을 제공하며, Secret Key 생성과 검증을 모두 쉽게 처리할 수 있습니다.
반면, Apache Commons Codec을 사용하면 더 많은 유연성을 제공하지만, TOTP 알고리즘과 시간 동기화 같은 추가적인 구현이 필요합니다. 일반적인 Google Authenticator 연동에서는 Googleauth 라이브러리를 사용하는 것이 훨씬 생산적입니다.
위 로직은 구글 OTP 외에도 MS OTP처럼 TOTP 알고리즘을 사용하는 모든 인증 서비스에서 사용 가능합니다.