단순한 ID/PW 조합만으로는 더 이상 우리의 소중한 개인정보를 지키기 힘든 시대입니다. 각종 해킹 사고와 보안 위협이 급증하면서, 2단계 인증(2FA, Two-Factor Authentication)은 선택이 아닌 필수가 되었습니다.
최근 한 기업용 시스템 프로젝트를 진행하며, 강력한 보안 요구사항을 충족시키기 위해 직접 2단계 인증 시스템을 구축해보기로 결심했습니다. 이 포스팅은 Spring Boot, Spring Security, 그리고 Java 21을 활용해 TOTP(Time-based One-Time Password) 기반의 2단계 인증 시스템을 구현한 경험과 깊이 있는 기술적 내용을 다룹니다.
이 프로젝트를 통해 복잡한 보안 요구사항을 어떻게 해결했는지, 그리고 개발 과정에서 마주했던 흥미로운 문제들과 그 해결책을 공유하고자 합니다.
이 프로젝트는 단순한 기능 구현을 넘어, 여러 기술적 난관을 극복하는 과정이었습니다.
구글 OTP와 같은 외부 앱과 호환되려면 RFC 6238 표준을 완벽히 따라야 했습니다. HMAC-SHA 알고리즘, Base32 인코딩, 그리고 시간 기반 해시 값 생성 등 복잡한 로직을 정확하게 구현하는 것이 핵심 과제였습니다.
사용자가 쉽게 OTP를 등록하도록 QR 코드를 제공해야 했습니다. 특히, 한글 사용자명이 포함된 URL을 올바르게 인코딩하고, 다양한 OTP 앱에서 호환성을 확보하는 것이 중요했습니다.
기존 로그인 시스템에 2단계 인증을 자연스럽게 추가하기 위해 Spring Security의 필터 체인과 인증 플로우를 깊이 이해하고 통합해야 했습니다.
전체 시스템은 다음과 같이 구성되었습니다.
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);
}
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);
}
로그인 후 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으로 철저히 테스트하여 정확성을 확보했습니다.이 프로젝트는 저에게 단순한 구현 이상의 의미를 주었습니다.
이 프로젝트는 단순히 코드를 작성하는 것을 넘어, 보안성과 사용자 편의성이라는 두 마리 토끼를 모두 잡기 위한 고민의 결과물입니다. 앞으로도 새로운 기술을 학습하고, 실제 문제를 해결하며, 사용자에게 가치를 제공하는 시스템을 만들어 나가고 싶습니다.