안녕하세요~ 퇴근하고 돌아온 jju_developer입니다.
기존에 구글 otp를 사용하는 이유에 대해서 설명을 드렸습니다.
[java] GOOGLE OTP란? 구글 인증 서비스 도입 방법 (One-Time Password)
안녕하세요 jju_developer 입니다~ 오늘도 어김없이 퇴근하고 돌아왔습니다. 오늘은 행운의 메일이 딱~ 날라오면서,,,! 정보 보안은 OTP 로 적용이 가능할 것. 이라는 조건이 날라오게 되었습니다.
jju240.tistory.com
추가 보안을 해야한다는 프로젝트가 있어서
해당 프로젝트를 위해 공부를 해본 소스코드를 공유드리겠습니다.
1. LoginController.java
LoginController 클래스는 OTP 생성을 위한 요청을 처리하고, 사용자로부터 입력받은 OTP를 검증하는 역할을 합니다.
public class LoginController {
/**
* OTP 키를 생성하고 QR 코드 URL을 생성하여 JSP에 전달하는 메서드.
* @author OGJ
* @param request HttpServletRequest 객체
* @param model ModelMap 객체
* @return 로그인 페이지의 경로
*/
@RequestMapping(value = "/generateOtp.do")
public String generateOtp(HttpServletRequest request, ModelMap model) {
try {
// OtpServlet의 로직을 사용하여 OTP 키를 생성하고 URL을 얻어옵니다.
String encodedKey = OtpServlet.generateOtpAndGetKey();
String user = "gj";
String host = "jju_blog";
String qrCodeUrl = OtpServlet.getQRBarcodeURL(user, host, encodedKey);
// 생성된 encodedKey와 qrCodeUrl을 JSP에 전달합니다.
model.addAttribute("encodedKey", encodedKey);
model.addAttribute("qrCodeUrl", qrCodeUrl);
model.addAttribute("qr", OtpServlet.getQrCodeUrl("jju_blog", encodedKey));
// 생성된 정보를 세션에 저장하거나 필요에 따라 ModelMap에 추가합니다.
request.getSession().setAttribute("encodedKey", encodedKey);
request.getSession().setAttribute("url", qrCodeUrl);
return "/main/login"; // OTP 입력 폼이 있는 JSP 페이지로 이동
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "OTP 생성 중 오류가 발생했습니다.");
return "/errorPage"; // 오류 페이지로 이동
}
}
/**
* 사용자로부터 입력받은 OTP를 검증하는 메서드.
* @param userCode 사용자가 입력한 OTP 코드
* @param encodedKey 서버에서 생성된 OTP 키
* @param request HttpServletRequest 객체
* @param model ModelMap 객체
* @return 검증 결과에 따라 이동할 페이지 경로
*/
@RequestMapping(value = "/otpVerification.do")
public String adminOtpVerification(@RequestParam("user_code") String userCode,
@RequestParam("encodedKey") String encodedKey,
HttpServletRequest request,
ModelMap model) {
if (encodedKey == null || encodedKey.isEmpty()) {
model.addAttribute("error", "OTP 키가 유효하지 않습니다. 다시 시도해 주세요.");
return "/otpVerificationErrorPage"; // 오류 페이지로 이동
}
boolean otpVerificationResult = false;
try {
otpVerificationResult = OtpResultServlet.check_code(encodedKey, Long.parseLong(userCode), new Date().getTime() / 30000);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
if (otpVerificationResult) {
// OTP 검증 성공 시
return "redirect:/admin/dashboard"; // 대시보드로 리다이렉트 예시
} else {
// OTP 검증 실패 시
model.addAttribute("error", "OTP 검증에 실패했습니다. 다시 시도해 주세요.");
return "/otpVerificationErrorPage"; // 오류 페이지 예시
}
}
}
- generateOtp 메서드는 OtpServlet을 사용하여 OTP 키와 QR 코드 URL을 생성하고 이를 모델과 세션에 저장합니다.
- adminOtpVerification 메서드는 사용자가 입력한 OTP를 검증하고 결과에 따라 적절한 페이지로 이동합니다.
2. OtpResultServlet.java
OtpResultServlet 클래스는 OTP 검증 로직을 구현합니다.
사용자가 입력한 코드와 서버에서 생성된 OTP 키를 비교하여 유효성을 검사합니다.
public class OtpResultServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
// 클라이언트로부터 'user_code' 파라미터를 가져와 문자열로 저장
String user_codeStr = req.getParameter("user_code");
// 'user_code' 문자열을 정수로 변환하여 저장
long user_code = Integer.parseInt(user_codeStr);
// 클라이언트로부터 'encodedKey' 파라미터를 가져와 저장
String encodedKey = req.getParameter("encodedKey");
// 현재 시간을 밀리초로 가져옴
long l = new Date().getTime();
// 현재 시간을 30초 단위로 나눔
long ll = l / 30000;
// OTP 검증 결과를 저장할 변수 초기화
boolean check_code = false;
try {
// 'encodedKey', 'user_code', 'll' 값을 사용하여 OTP를 검증
check_code = check_code(encodedKey, user_code, ll);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
// 예외 발생 시 스택 트레이스를 출력
e.printStackTrace();
}
// 검증 결과를 콘솔에 출력
System.out.println("check_code : " + check_code);
}
/**
* 주어진 비밀키와 시간을 사용하여 OTP를 검증합니다.
* @param secret 비밀키 (Base32로 디코딩된 바이트 배열)
* @param code 사용자가 입력한 OTP 코드
* @param t 현재 시간대를 나타내는 값 (시간 슬라이딩 윈도우에 사용됨)
* @return 일치하는 OTP 코드가 발견되면 true, 그렇지 않으면 false 반환
* @throws NoSuchAlgorithmException 암호화 알고리즘이 지원되지 않을 때 발생
* @throws InvalidKeyException 주어진 키가 유효하지 않을 때 발생
*/
protected static boolean check_code(String secret, long code, long t) throws NoSuchAlgorithmException, InvalidKeyException {
// Base32 인코딩 객체 생성
Base32 codec = new Base32();
// 비밀키를 Base32로 디코딩
byte[] decodedKey = codec.decode(secret);
// 시간 창 크기를 3으로 설정
int window = 3;
// -3부터 +3까지의 시간 창을 확인
for (int i = -window; i <= window; ++i) {
// 현재 시간 창에 대한 OTP 코드 생성
long hash = verify_code(decodedKey, t + i);
// 생성된 OTP 코드와 사용자가 입력한 코드가 일치하는지 확인
if (hash == code) {
// 일치하면 true 반환
return true;
}
}
// 일치하는 코드가 없으면 false 반환
return false;
}
/**
* 주어진 키와 시간 값을 사용하여 OTP 코드를 생성합니다.
* @param key 비밀키
* @param t 시간 값
* @return 생성된 OTP 코드
* @throws NoSuchAlgorithmException 암호화 알고리즘이 지원되지 않을 때 발생
* @throws InvalidKeyException 주어진 키가 유효하지 않을 때 발생
*/
private static int verify_code(byte[] key, long t) throws NoSuchAlgorithmException, InvalidKeyException {
// 8바이트 배열 생성
byte[] data = new byte[8];
// 주어진 시간 값을 바이트 배열로 변환
long value = t;
for (int i = 8; i-- > 0; value >>>= 8) {
data[i] = (byte) value;
}
// 비밀키를 사용하여 HMAC-SHA1 서명 객체 생성
SecretKeySpec signKey = new SecretKeySpec(key, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
// HMAC-SHA1 서명 객체 초기화
mac.init(signKey);
// 데이터에 대한 HMAC-SHA1 해시 생성
byte[] hash = mac.doFinal(data);
// 해시의 마지막 바이트에서 오프셋을 결정
int offset = hash[20 - 1] & 0xF;
// 해시의 일부분을 사용하여 OTP 코드 생성
long truncatedHash = 0;
for (int i = 0; i < 4; ++i) {
truncatedHash <<= 8;
truncatedHash |= (hash[offset + i] & 0xFF);
}
// OTP 코드를 6자리로 변환
truncatedHash &= 0x7FFFFFFF;
truncatedHash %= 1000000;
// 생성된 OTP 코드를 정수로 반환
return (int) truncatedHash;
}
}
- service 메서드는 요청에서 사용자 코드와 OTP 키를 받아와 검증합니다.
- check_code 메서드는 주어진 비밀키와 시간을 사용하여 OTP를 검증합니다.
- verify_code 메서드는 HMAC-SHA1 알고리즘을 사용하여 OTP를 생성합니다.
여기서 잠깐!!!
HmacSHA1 이 무엇일까요?
궁금증을 풀기위해 잠깐 지식타임 갖겠습니다!!
HmacSHA1는 HMAC (Hash-based Message Authentication Code) 방식 중 하나로,
메시지 인증을 위해 SHA-1 해시 함수를 사용하는 암호화 알고리즘입니다.
HMAC은 메시지 무결성과 인증을 검증하는 데 사용되며,
키와 메시지를 조합하여 해시 값을 생성합니다.
SHA-1(Secure Hash Algorithm 1)은 160비트 해시 값을 생성하는 해시 함수입니다.
HmacSHA1의 용도는?
HmacSHA1 알고리즘은 보통 비밀키를 사용하여 메시지의 무결성을 확인하고,
메시지가 변경되지 않았음을 보장하는 데 사용됩니다.
우리의 OTP(One-Time Password) 시스템에서는 사용자가 입력한 OTP가 유효한지 검증하기 위해
HmacSHA1과 같은 알고리즘을 사용해 OTP 코드를 생성하였습니다.
이름을 바꿀 수 있는지 여부?
HmacSHA1의 "HmacSHA1"은 알고리즘의 이름을 지정하는 표준 명칭이며, 이를 임의로 바꿀 수 없습니다.
이 이름은 특정한 알고리즘을 지정하며, JCE (Java Cryptography Extension)와 같은 암호화 라이브러리에서 지원하는 알고리즘을 지칭합니다.
가장 궁금했던 대체 가능한 알고리즘은?
SHA-1은 암호화 강도가 다소 약한 것으로 간주되기 때문에,
보안이 더 중요한 경우 SHA-256이나 SHA-512와 같은 더 강력한 해시 함수를 사용하는 것이 좋습니다.
예를 들어, HMAC-SHA256을 사용하려면 알고리즘 이름을 "HmacSHA256"으로 변경해야 합니다.
저는 실제로 회사에서 반영을 할때에는 좀더 알아보고 sha256으로 반영할 예정입니다.
참고 부탁드립니다~! ^.^
3. OtpServlet.java
OtpServlet 클래스는 OTP 키를 생성하고 QR 코드 URL을 생성하는 로직을 포함합니다.
/**
* OtpServlet 클래스는 OTP(One-Time Password)를 생성하고,
* 이를 QR 코드 형태로 클라이언트에게 제공하는 기능을 담당합니다.
* 이 클래스는 HttpServlet을 상속하여 HTTP 요청을 처리합니다.
*/
public class OtpServlet extends HttpServlet {
/**
* HTTP 요청을 처리하는 메서드입니다.
* @param req HttpServletRequest 객체
* @param res HttpServletResponse 객체
* @throws ServletException 서블릿 관련 예외
* @throws IOException 입출력 관련 예외
*/
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
// OTP 생성 및 키 반환
String encodedKey = generateOtpAndGetKey();
// 생성된 Key 출력
System.out.println("generated encodedKey : " + encodedKey);
// 생성된 바코드 주소
String url = getQRBarcodeURL("gj", "jju_blog", encodedKey);
System.out.println("URL : " + url);
// 뷰 경로 설정
String view = "/WEB-INF/view/otpTest.jsp";
// 요청에 속성 설정
req.setAttribute("encodedKey", encodedKey);
req.setAttribute("url", url);
// 뷰로 포워딩
req.getRequestDispatcher(view).forward(req, res);
}
/**
* OTP 키를 생성하고 Base32로 인코딩된 문자열을 반환합니다.
* @return 생성된 OTP 키
*/
public static String generateOtpAndGetKey() {
// 80비트 랜덤 숫자 배열
byte[] buffer = new byte[10];
// 랜덤한 바이트 배열 생성
new SecureRandom().nextBytes(buffer);
// Base32 인코딩
Base32 codec = new Base32();
// 40비트 키 생성
byte[] secretKey = Arrays.copyOf(buffer, 5);
// 생성된 키를 반환
String generatedKey = new String(codec.encode(secretKey));
System.out.println("Generated Key : " + generatedKey);
return generatedKey;
}
/**
* 주어진 사용자, 호스트, 비밀키를 사용하여 QR 바코드 URL을 생성합니다.
* @param user 사용자명
* @param host 호스트명
* @param secret 비밀키
* @return 생성된 QR 바코드 URL
*/
public static String getQRBarcodeURL(String user, String host, String secret) {
// QR 바코드 URL 포맷
String format = "https://chart.googleapis.com/chart?cht=qr&chs=300x300&chl=otpauth://totp/%s@%s%%3Fsecret%%3D%s&chld=H|0";
// 포맷을 사용하여 URL 생성
return String.format(format, user, host, secret);
}
/**
* 주어진 이름과 비밀키를 사용하여 QR 코드 URL을 생성합니다.
* @param displayName 표시할 이름
* @param secret 비밀키
* @return 생성된 QR 코드 URL
* @throws Exception 예외 발생 시
*/
public static String getQrCodeUrl(String displayName, String secret)
throws Exception {
// QR 코드 텍스트 포맷
String format = "otpauth://totp/" + URLEncoder.encode(displayName, String.valueOf(StandardCharsets.UTF_8))
.replace("+", "%20")
+ "?secret=" + secret;
// QR 코드 이미지 생성
return generateQRCodeImage(format);
}
/**
* 주어진 바코드 텍스트를 사용하여 QR 코드 이미지를 생성합니다.
* @param barcodeText 바코드 텍스트
* @return 생성된 QR 코드 이미지 (Base64 인코딩)
* @throws Exception 예외 발생 시
*/
public static String generateQRCodeImage(String barcodeText) throws Exception {
// QR 코드 작성기 생성
QRCodeWriter qrCodeWriter = new QRCodeWriter();
// 바코드 텍스트를 QR 코드로 인코딩
BitMatrix bitMatrix = qrCodeWriter.encode(barcodeText, BarcodeFormat.QR_CODE, 200, 200);
// PNG 출력 스트림 생성
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
// 비트 매트릭스를 PNG 포맷으로 스트림에 작성
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
// Base64로 인코딩하여 반환
return Base64.getEncoder().encodeToString(pngOutputStream.toByteArray());
}
}
view 변수: 이 변수는 JSP 페이지의 경로를 설정합니다. /WEB-INF/view/otpTest.jsp는 뷰 페이지의 위치를 나타냅니다.
이부분은 각자 만드실 QR이 보일 뷰 페이지가 있는 곳으로 경로를 변경해주어야 합니다!
4. JSP 에서 OR 생성하기
위의 백엔드 로직을 사용하려면, OTP 생성 및 QR 코드 표시와 OTP 검증을 지원하는 JSP 페이지가 필요합니다. 주어진 두 JSP 페이지를 비교할 때, 백엔드 로직을 제대로 활용하려면 다음 사항을 고려해야 합니다:
- QR 코드 표시: 백엔드에서 생성된 QR 코드를 표시해야 합니다.
- OTP 코드 입력: 사용자로부터 OTP 코드를 입력받아야 합니다.
- encodedKey 전달: 백엔드에서 생성된 encodedKey를 숨겨진 필드로 전달해야 합니다.
<!-- LoginController의 adminOtpVerification 메서드로 요청을 보내도록 합니다. -->
<form action="${pageContext.request.contextPath}/admin/otpVerification.do" method="post">
<!-- 사용자에게 OTP 코드를 입력받습니다. -->
code : <input name="user_code" type="text"><br><br>
<!-- 숨겨진 필드로 encodedKey를 전달합니다. -->
<input name="encodedKey" type="hidden" value="${encodedKey}"><br><br>
<!-- 전송 버튼을 제공합니다. -->
<input type="submit" value="전송!">
</form>
<!-- QR 코드를 표시합니다. -->
<div>
<h4>Google Authenticator 앱에서 QR code 를 스캔해주세요.</h4>
<!-- QR 코드 이미지를 표시합니다. -->
<img src="data:image/jpeg;base64, ${qr}" alt="QR Code">
</div>
위 코드는 테스트 코드이며 각자 코드에 맞게 변형해서 사용하시기 바랍니다!
자, 이렇게 구글 OTP 를 활용한 기초 틀의 코드를 보여드렸습니다.
실제 코드는 이보다 더 로직이 길고 많지만,
각자의 회사의 요구사항에 맞게 변경해서 사용하시면 됩니다.
이번에 구글 otp를 사용해보면서 보안이 이중 삼중으로 될 수 있고,
또한 어떤 api를 사용해야지 사용자가 더욱 편리하게 사용할 수 있구나를 느끼게 되었습니다.
08/01 무더위가 시작되고 있습니다. 건강에 유의하시기 바랍니다.
그럼 오늘도 수고하셨습니다~!

'백엔드 관련' 카테고리의 다른 글
[VSCode] 깃에 http clone 주소가 있을 때 코드를 한번 열어보자 초급반 (0) | 2024.09.10 |
---|---|
[java 오류 찾기] Logger 선언 & Actuator (1) | 2024.08.16 |
[jar] 자르 파일 인터넷에서 다운받고 dependency 추가 할 때 (128) | 2024.07.17 |
[java] GOOGLE OTP란? 구글 인증 서비스 도입 방법 (One-Time Password) (66) | 2024.07.16 |
JSONObject["data"] is not a string 스프링부트 자바 오류 (2) | 2024.06.14 |