관리자 모드로 로그인 할 때 2차 인증을 하는 기능 추가가 필요했다
여러가지 방법이 있고,
그 중
Google Authenticator을 사용하기로함
후딱 끝날거라고 생각했는데 ㅋ삽질 엄청나게함
구현한거랑 뭐땜에 글케 삽질했는지 적어보겠음
삽질 1.
google QR코드 자동으로 생성해주는 API가 지원이 종료됐다는데 이걸 모르고
내내 그 API를 쓰려고해서 QR이 안만들어졌다ㅠ
계속 이미지 에러가 떠서..
여차저차 찾다보니 API지원이 끝난걸알았고
구글링하면서 다른사람들이 qr생성하는 코드를 찾게되어서
참고해서 만들어봄
package com.example.demo.controller;
import com.example.demo.service.TotpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
@Controller
public class AuthenticationController {
private final TotpService totpService;
@Autowired
public AuthenticationController(TotpService totpService) {
this.totpService = totpService;
}
@GetMapping("/totp")
public String totpPage(Model model, HttpServletRequest request) {
String username = "Test";
String secretKey = totpService.generateSecretKey();
String qrUrl = totpService.generateQrUrl(username, secretKey);
try {
byte[] qrCodeImage = totpService.generateQrCodeImage(qrUrl);
String qrCodeBase64 = Base64.getEncoder().encodeToString(qrCodeImage);
model.addAttribute("qrCodeBase64", qrCodeBase64);
model.addAttribute("secretKey", secretKey);
} catch (Exception e) {
model.addAttribute("error", "QR 코드 생성 중 오류가 발생했습니다.");
}
return "totp";
}
@PostMapping("/verify")
public String verifyTotp(@RequestParam("code") String code, @RequestParam("secretKey") String secretKey, Model model) {
try {
boolean isValid = totpService.verifyOtp(secretKey, code);
if (isValid) {
model.addAttribute("message", "2단계 인증 성공!");
return "success";
} else {
model.addAttribute("message", "인증 코드가 잘못되었습니다.");
return "fail";
}
} catch (Exception e) {
model.addAttribute("message", "인증 처리 중 오류가 발생했습니다.");
return "fail";
}
}
}
username라고 설정해둔 부분이
나중에 어플에서 받아보면 인증번호를 구분할 수 있게 적혀있음
주석을 열심히 달아보았으니
설명은 생략해도 될 것 같다
package com.example.demo.service;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import dev.samstevens.totp.secret.DefaultSecretGenerator;
import dev.samstevens.totp.secret.SecretGenerator;
import org.apache.commons.codec.binary.Base32;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@Service
public class TotpService {
private static final String ALGORITHM = "HmacSHA1"; // OTP 생성 시 사용할 알고리즘
private static final int CODE_LENGTH = 6; // OTP 코드 길이
private static final int TIME_STEP = 30; // OTP 생성 간격 (초)
// QR 코드 URL 생성
public String generateQrUrl(String username, String secretKey) {
String issuer = "KWater";
return String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s", issuer, username, secretKey, issuer);
}
// QR 코드 생성 (이미지로 반환)
public byte[] generateQrCodeImage(String qrUrl) throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Map<EncodeHintType, Object> hintMap = new HashMap<>();
hintMap.put(EncodeHintType.MARGIN, 1);
BitMatrix bitMatrix = qrCodeWriter.encode(qrUrl, BarcodeFormat.QR_CODE, 250, 250, hintMap);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
return outputStream.toByteArray();
}
// 비밀 키 생성
public String generateSecretKey() {
SecretGenerator secretGenerator = new DefaultSecretGenerator();
return secretGenerator.generate();
}
// OTP 생성
public String generateOtp(String secretKey) throws NoSuchAlgorithmException, InvalidKeyException {
long timeIndex = System.currentTimeMillis() / 1000 / TIME_STEP;
byte[] secretBytes = new Base32().decode(secretKey);
byte[] data = new byte[8];
for (int i = 7; i >= 0; i--) {
data[i] = (byte) (timeIndex & 0xFF);
timeIndex >>= 8;
}
SecretKeySpec signingKey = new SecretKeySpec(secretBytes, ALGORITHM);
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(signingKey);
byte[] hash = mac.doFinal(data);
int offset = hash[hash.length - 1] & 0x0F;
int otp = ((hash[offset] & 0x7F) << 24) | ((hash[offset + 1] & 0xFF) << 16)
| ((hash[offset + 2] & 0xFF) << 8) | (hash[offset + 3] & 0xFF);
otp = otp % (int) Math.pow(10, CODE_LENGTH);
return String.format("%0" + CODE_LENGTH + "d", otp);
}
// OTP 검증
public boolean verifyOtp(String secretKey, String code) throws NoSuchAlgorithmException, InvalidKeyException {
String generatedOtp = generateOtp(secretKey); // Generate OTP based on the secret key
return generatedOtp.equals(code); // Compare the generated OTP with the user-provided code
}
}
삽질 2
디버깅으로 시간을 계속 잡아먹은 이유는 삽질3에 적을건데
코드는 30초마다 갱신된다
현재 시간을 30초 단위로 나눈 값을 사용하여 OTP 생성.
비밀 키와 시간을 HMAC-SHA1으로 처리하여 OTP 코드를 생성.
6자리 코드를 생성하여 사용자와 비교.
시간을 기준으로 OTP 생성: System.currentTimeMillis() / 1000 / TIME_STEP은 현재 시간을 30초 간격으로 나누어 시간 인덱스를 구하는 부분.
HMAC-SHA1 알고리즘을 사용하여 **secretKey**와 시간 인덱스를 결합하여 OTP를 생성.
OTP 검증은 generateOtp(secretKey) 메서드를 통해 생성된 OTP와 사용자가 입력한 OTP를 비교함.
OTP 검증 과정:
사용자는 Google Authenticator에서 생성된 6자리 OTP 코드를 입력하고,.
서버는 **generateOtp(secretKey)**를 호출하여 동일한 6자리 OTP 코드를 생성함.
두 코드를 비교하여 일치하면 인증 성공, 일치하지 않으면 실패.
6자리 OTP는 시간에 기반하여 생성되므로, 이를 비교하는 방식으로 OTP 검증을 수행.
QR 코드에서 생성된 **secretKey**는 비밀 키로 사용되고, 이를 바탕으로 시간 기반 OTP 코드를 생성하여 Google Authenticator와 비교하는 방식임
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<h1>QR 코드 생성 테스트</h1>
<p>google Authenticator 어플에 스캔하고 인증 코드를 받으세요</p>
<img th:src="'data:image/png;base64,' + ${qrCodeBase64}" alt="QR Code" />
<!--p>비밀 키: <span th:text="${secretKey}"></span></p-->
<form action="/verify" method="post">
<label>인증 코드를 입력하세요:</label>
<input type="hidden" name="secretKey" th:value="${secretKey}" />
<input type="text" name="code" required />
<button type="submit">확인</button>
</form>
</body>
</html>
화면은 VScode Copilot 이 뚝 ~ 딱 ! 만들어줌 ㅋㅋㅋㅋ
ㅋㅋㅋㅋㅋㅋ
삽질 3
진짜 이건 just 삽질이었는데ㅋㅋㅋㅋ
html 코드 보면 secretKey는 지금은 주석처리했지만
뭔 원리로 큐알이랑 인증코드를 매칭하는건지 모르겠고 어쩌구 저쩌구해서 내가 보려고
화면에 나오게 했었는데
input 박스에 code들어갈 자리에 secretKey 로 지정해놓고
값이 안들어온다
왜 안들어오냐
값을 받은 다음에 디코딩을 하는데 왜 뻘한 값이 들어오는걸까를
찾아 헤매면서 디버깅으로 한시간은 보낸듯 ㅠ
그리고 또 컨트롤러에 parameter 순서 뒤집어있었음 ㅋ
정신차리자~~~~
쨋든
화면 띄우고
Google Authenticator 어플로 로그인하면
인증코드 6자리가 생성되어있음
Google Authenticator 어플에서 보이는 화면!
이걸 근데 나는 매회 받아야된ㄷㅏ고생각했는데
그것도 아닌게 한 번 받고나면 그 번호로 쭉 쓰고
뭔가 디비에다가 토큰을 저장해둔다고했나???? 하면된ㄷㅏ 했는데
안쓰게됏으니 패스!!!
입력한 후에 확인버튼누르면 인증 성공!
근데,, 결국 이 방식 안쓰기로하고
네이버 SENS 를 쓰게도ㅣ었다,,
그건 다음 포스팅으로 가져올게요,,,,,,,~
'Chapter01 > Sping boot' 카테고리의 다른 글
[ node ] npm 명령어 (0) | 2025.01.13 |
---|---|
[ 보안 취약점 ] main() 메서드 (0) | 2025.01.10 |
[ Spring Boot ] J2EE와의 관계 (0) | 2025.01.08 |
[ Springboot ] final (0) | 2024.12.28 |
[ 보안 취약점 ] 부적절한 예외 처리 catch(Exception e) (0) | 2024.12.27 |