본문 바로가기
java

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

by moonsiri 2025. 1. 13.
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
반응형

댓글