Chapter01/Sping boot

[ 2Factor 인증] Google Authenticator 인증 java코드

EmmaDev_v 2025. 4. 25. 18:18

관리자 모드로 로그인 할 때 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 를 쓰게도ㅣ었다,,

 

그건 다음 포스팅으로 가져올게요,,,,,,,~ 

 

반응형