■ OTP 2FA (One-Time Password Two-Factor Authentication)
사용자 인증 방식을 강화하기 위해 사용하는 기술로 1회용 비밀번호(OTP)를 활용하여 다중 인증(MFA)을 구현하는 기술이다.
■ Google OTP를 활용한 2차 인증 구현 방법에 대해서 아래에 기술하고자 한다.
Google OTP를 활용한 2차 인증의 경우, 다른 서버와 통신을 하지 않기 때문에 폐쇄망에서 사용 가능하다.
*주의사항 : 서버와 통신하여 인증하는 절차가 없기 때문에 사용자의 시간과 서버의 시간을 일치시켜줘야 한다.
■ 인증 로직
1. 사용자의 애플리케이션에서 서버에서 발행한 계정에 해당하는 6자리 숫자를 입력 받는다.
2. 서버에서 사용자의 비밀키(32자리)를 이용하여 메서드 호출 시간에 따른 6자리 숫자를 생성하여 비교한다.
cf) Google OTP 사용을 위해 최초 1회 Google Authenticator(Google OTP) 애플리케이션에 계정 생성 필요
- 서버에서 생성한 QR 코드를 사용자가 스캔하여 계정 생성 가능
(사용자가 직접 계정 이름과 비밀키를 입력하는 방법도 있지만 보안과 UX 측면에서 QR 코드를 이용한 방법 권장)
■ 사용자 Google Authenticator(Google OTP) 애플리케이션 계정 생성 가이드
1. iOS 운영체제 모바일 기기 사용자
(1) "App Store"에서 "Google Authenticator" 앱 다운로드
(2) 포털 로그인 페이지에서 계정 정보 입력 후, GOTP 계정 생성 버튼 클릭
(3) 다운로드한 "Google Authenticator" 앱 실행 > 우측 하단 플러스 버튼 클릭 > QR 코드 스캔
2. Android 운영체제 모바일 기기 사용자
(1) "Google Play 스토어"에서 "Google OTP" 앱 다운로드
(2) 포털 로그인 페이지에서 계정 정보 입력 후, GOTP 계정 생성 버튼 클릭
(3) 다운로드한 "Google OTP" 앱 실행 > 우측 하단 플러스 버튼 클릭 > QR 코드 스캔
■ 구현 절차
1. '비밀키', '사용자 아이디(사용자명)', '발행자명'을 이용하여 Google OTP 인증용 링크 생성
- 사용자명과 발행자명은 애플리케이션에서 계정을 구분 짓기 위한 용도이고, 실제 OTP 생성에 관여하는 것은 비밀키 뿐이다.
2. 1번에서 생성한 인증용 링크를 이용하여 QR 코드 생성
3. 2번의 QR 코드를 사용자에게 표출하여 계정 등록 유도
4. 계정 등록을 완료한 1차 인증을 거친 사용자로부터 입력받은 OTP와 해당 사용자의 비밀키를 이용하여 생성한 OTP 비교
■ 필요 라이브러리
1. commons-codec-1.15.jar
https://mvnrepository.com/artifact/commons-codec/commons-codec/1.15
2. core-3.4.1.jar
https://mvnrepository.com/artifact/com.google.zxing/core/3.4.1
3. javase-3.4.1.jar
https://mvnrepository.com/artifact/com.google.zxing/javase/3.4.1
4. totp-1.0.jar
https://mvnrepository.com/artifact/de.taimos/totp/1.0
■ 예시 코드
아래 코드는 백엔드 부분만 간단히 완성한 것으로 QR 이미지를 예시와 같이 파일로 떨어트리지 않고 UI에 표출시키도록 하는 등 상황에 맞게 프론트와 접목시키면 됩니다.
● GoogleOTPService
import com.google.zxing.WriterException;
import java.io.IOException;
public interface GoogleOTPService {
// 개인키 생성 메서드 > UUID로 대체 가능
public String getGoogleOTPKey();
// Google OTP 인증용 링크를 생성하는 메서드
public String getGoogleOTPAuthURL(String GoogleOTPKey, String userId, String issuer);
// QR 코드 생성 메서드
public void getQRImage(String GoogleOTPAuthURL, String filePath, int height, int width) throws WriterException, IOException;
// 최초 어플 등록용 QR 이미지 생성
public void createQRImage(String userId, String issuer);
// Google OTP 생성 메서드
public String getTOTPCode(String secretKey);
}
● GoogleOTPServiceImpl
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import de.taimos.totp.TOTP;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.Hex;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.SecureRandom;
public class GoogleOTPServiceImpl implements GoogleOTPService {
// 개인키 생성 메서드 > UUID로 대체 가능
public String getGoogleOTPKey() {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[20];
random.nextBytes(bytes);
Base32 base32 = new Base32();
return base32.encodeToString(bytes);
}
// Google OTP 인증용 링크를 생성하는 메서드
public String getGoogleOTPAuthURL(String GoogleOTPKey, String userId, String issuer) {
try {
return "otpauth://totp/"
+ URLEncoder.encode(issuer + ":" + userId, "UTF-8").replace("+", "%20")
+ "?secret=" + URLEncoder.encode(GoogleOTPKey, "UTF-8").replace("+", "%20")
+ "&issuer=" + URLEncoder.encode(issuer, "UTF-8").replace("+", "%20");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
// QR 코드 생성 메서드
public void getQRImage(String GoogleOTPAuthURL, String filePath, int height, int width) throws WriterException, IOException {
BitMatrix matrix = new MultiFormatWriter().encode(GoogleOTPAuthURL, BarcodeFormat.QR_CODE, width, height);
try (FileOutputStream out = new FileOutputStream(filePath)) {
MatrixToImageWriter.writeToStream(matrix, "png", out);
}
}
// 최초 어플 등록용 QR 이미지 생성
public void createQRImage(String userId, String issuer) {
try {
String GoogleOTPKey = getGoogleOTPKey();
String GoogleOTPAuthURL = getGoogleOTPAuthURL(GoogleOTPKey, userId, issuer);
getQRImage(GoogleOTPAuthURL, {FILE_PATH} + {FILE_NAME} + ".png", {HEIGHT}, {WIDTH});
} catch (NullPointerException e) {
throw new RuntimeException(e);
} catch (WriterException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// Google OTP 생성 메서드
public String getTOTPCode(String secretKey) {
Base32 base32 = new Base32();
byte[] bytes = base32.decode(secretKey);
String hexKey = Hex.encodeHexString(bytes);
return TOTP.getOTP(hexKey);
}
}
댓글