java

[JAVA] Google OTP (TOTP) 구현하기

moonsiri 2025. 1. 13. 18:32
728x90
반응형

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 GoogleauthApache 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 알고리즘을 사용하는 모든 인증 서비스에서 사용 가능합니다.
 

728x90
반응형