🔐 Spring Boot로 구축한 OTP 2단계 인증 시스템, 개발 회고 및 심층 분석

📌 도입: 왜 2단계 인증이 필요한가?

단순한 ID/PW 조합만으로는 더 이상 우리의 소중한 개인정보를 지키기 힘든 시대입니다. 각종 해킹 사고와 보안 위협이 급증하면서, 2단계 인증(2FA, Two-Factor Authentication)은 선택이 아닌 필수가 되었습니다.

최근 한 기업용 시스템 프로젝트를 진행하며, 강력한 보안 요구사항을 충족시키기 위해 직접 2단계 인증 시스템을 구축해보기로 결심했습니다. 이 포스팅은 Spring Boot, Spring Security, 그리고 Java 21을 활용해 TOTP(Time-based One-Time Password) 기반의 2단계 인증 시스템을 구현한 경험과 깊이 있는 기술적 내용을 다룹니다.

이 프로젝트를 통해 복잡한 보안 요구사항을 어떻게 해결했는지, 그리고 개발 과정에서 마주했던 흥미로운 문제들과 그 해결책을 공유하고자 합니다.

💡 프로젝트의 시작: 마주했던 기술적 도전들

이 프로젝트는 단순한 기능 구현을 넘어, 여러 기술적 난관을 극복하는 과정이었습니다.

1. TOTP 알고리즘의 표준 구현

구글 OTP와 같은 외부 앱과 호환되려면 RFC 6238 표준을 완벽히 따라야 했습니다. HMAC-SHA 알고리즘, Base32 인코딩, 그리고 시간 기반 해시 값 생성 등 복잡한 로직을 정확하게 구현하는 것이 핵심 과제였습니다.

2. QR 코드 생성과 사용자 경험 최적화

사용자가 쉽게 OTP를 등록하도록 QR 코드를 제공해야 했습니다. 특히, 한글 사용자명이 포함된 URL을 올바르게 인코딩하고, 다양한 OTP 앱에서 호환성을 확보하는 것이 중요했습니다.

3. Spring Security와의 매끄러운 통합

기존 로그인 시스템에 2단계 인증을 자연스럽게 추가하기 위해 Spring Security의 필터 체인과 인증 플로우를 깊이 이해하고 통합해야 했습니다.

🏗️ 시스템 아키텍처 및 핵심 컴포넌트

전체 시스템은 다음과 같이 구성되었습니다.

  • 프론트엔드 (Thymeleaf): 사용자 인터페이스를 담당하며, OTP 설정 화면, QR 코드 표시, OTP 코드 입력 폼 등을 제공합니다.
  • 백엔드 (Spring Boot): 모든 비즈니스 로직을 처리하는 핵심입니다. TOTP 생성, QR 코드 URL 생성, OTP 유효성 검증 등의 역할을 수행합니다.
  • 데이터베이스 (H2): 개발 편의성을 위해 인메모리 DB인 H2를 사용했습니다. 사용자의 OTP 비밀키를 안전하게 저장합니다.

✨ 기술 구현 상세 분석

1. TOTP 알고리즘 구현: HMAC-SHA256 활용

TOTP는 HMAC(Hash-based Message Authentication Code)를 사용해 현재 시간을 기반으로 일회용 비밀번호를 생성하는 알고리즘입니다. 보안 강화를 위해 HMAC-SHA256 알고리즘을 사용했으며, 30초 주기로 변경되는 토큰을 구현했습니다. 특히, OTP 앱과의 호환성을 위해 Base32 인코딩을 사용했습니다.

// 핵심 로직: 시간 기반 HMAC-SHA256 OTP 생성
public String generateTOTP(String secret) {
    // 1. Base32로 인코딩된 비밀키 디코딩
    Base32 base32 = new Base32();
    byte[] keyBytes = base32.decode(secret);

    // 2. 현재 시간을 30초 주기로 나누어 타임 스탬프 생성
    long time = Instant.now().getEpochSecond() / PERIOD;

    // 3. HMAC-SHA256 알고리즘으로 해시 생성
    byte[] hash = generateHMAC(keyBytes, time);
    
    // 4. 해시 값을 6자리 숫자로 변환
    return truncateHash(hash);
}

2. QR 코드 생성: 한글 지원 및 호환성 확보

OTP 앱은 otpauth:// 스키마를 사용하는 특정 URL 형식으로 QR 코드를 인식합니다. 이 URL에 한글이 포함된 경우, **URLEncoder.encode**를 사용하여 UTF-8 인코딩을 적용해 깨짐 문제를 해결했습니다. 또한, QR 코드의 크기를 256x256 픽셀로 최적화하여 스마트폰 카메라로 쉽게 인식되도록 했습니다.

public String generateQRUrl(String username, String secret) {
    // 한글과 특수문자를 URL 인코딩하여 호환성 확보
    String encodedUsername = URLEncoder.encode(username, StandardCharsets.UTF_8);
    String encodedIssuer = URLEncoder.encode(ISSUER, StandardCharsets.UTF_8);

    // otpauth://totp/ 형식의 URL 생성
    return String.format("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA256&digits=%d&period=%d",
            encodedIssuer, encodedUsername, secret, encodedIssuer, DIGITS, PERIOD);
}

3. Spring Security 통합: 세션 기반의 안전한 인증

로그인 후 2단계 인증 페이지로 리다이렉트되도록 Spring Security의 인증 플로우를 커스터마이징했습니다. 특정 URL(dashboard, otp-setup)은 인증이 필요하도록 설정하고, 세션 기반으로 사용자의 OTP 활성화 상태를 관리하여 보안을 강화했습니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // API를 위해 비활성화
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login", "/register", "/otp-verify").permitAll()
                .requestMatchers("/dashboard", "/otp-setup").authenticated() // 인증이 필요한 경로
                .anyRequest().permitAll()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard", true) // 로그인 성공 시 대시보드로 이동
            );
        return http.build();
    }
}

📈 테스트 및 품질 관리

  • 단위 테스트: 핵심 로직인 OtpService의 TOTP 생성 및 검증 로직을 JUnit으로 철저히 테스트하여 정확성을 확보했습니다.
  • 통합 테스트: Spring Boot 컨텍스트를 로드하여 데이터베이스 연동 및 Spring Security 설정이 올바르게 동작하는지 확인했습니다.

🚀 배포 및 운영을 위한 고려사항

  • 데이터베이스: 개발 단계에서는 H2를 사용했지만, 프로덕션 환경에서는 PostgreSQL과 같은 영구 데이터베이스로 전환하는 것이 필수적입니다.
  • 보안: 실제 운영 환경에서는 HTTPS를 적용하여 통신 과정에서 데이터가 노출되는 것을 방지해야 합니다.
  • 확장성: 향후 사용자 수가 늘어날 경우를 대비해 Redis를 활용한 세션 및 OTP 캐싱을 고려했습니다.

📚 프로젝트를 통해 얻은 기술적 성장

이 프로젝트는 저에게 단순한 구현 이상의 의미를 주었습니다.

  • 보안 깊이 이해: HMAC-SHA256, Base32 인코딩, 세션 관리 등 실용적인 보안 기술을 직접 구현하며 이론을 체화할 수 있었습니다.
  • 문제 해결 능력 향상: 한글 URL 인코딩, 다양한 OTP 앱과의 호환성 문제 등 예상치 못한 난관을 해결하는 과정을 통해 문제 해결 능력이 크게 향상되었습니다.
  • 전체 시스템 설계: 백엔드와 프론트엔드를 아우르며 하나의 완성된 시스템을 설계하고 구축하는 경험을 얻었습니다.

✨ 마무리하며

이 프로젝트는 단순히 코드를 작성하는 것을 넘어, 보안성과 사용자 편의성이라는 두 마리 토끼를 모두 잡기 위한 고민의 결과물입니다. 앞으로도 새로운 기술을 학습하고, 실제 문제를 해결하며, 사용자에게 가치를 제공하는 시스템을 만들어 나가고 싶습니다.

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글