다단계 인증(MFA)은 사용자가 시스템에 접근할 때 두 가지 이상의 인증 요소를 요구하는 보안 절차입니다. 이는 단순히 아이디와 비밀번호를 사용하는 단일 인증(Single-Factor-Authentication, SFA)에 비해 보안을 크게 강화합니다.
MFA는 보통 다음 세 가지 인증 요소 중 두 가지 이상을 결합하여 사용합니다.
1. 지식 요소(Something you know) : ex) 비밀번호, PIN, 보안 질문 등
2. 소유 요소(Something you have) : ex) 스마트폰, 보안, 토큰, OTP 생성기 등
3. 고유 요소(Something you are) : ex) 지문, 얼굴 인식, 음성 인식 등
// 예시: Spring Security 설정 클래스
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/mfa").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.successHandler(authenticationSuccessHandler())
.and()
.addFilterAfter(mfaFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new CustomAuthenticationSuccessHandler();
}
@Bean
public OncePerRequestFilter mfaFilter() {
return new MFAFilter();
}
}
// 예시: MFA 필터
public class MFAFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated() && !isMFACompleted(auth)) {
response.sendRedirect("/mfa");
return;
}
filterChain.doFilter(request, response);
}
private boolean isMFACompleted(Authentication auth) {
// MFA 완료 여부 확인 로직
return (boolean) auth.getDetails();
}
}
취업 준비를 하시는 신입 Java/Spring 백엔드 개발자 분께 실습을 통해 MFA(Multi-Factor Authentication)를 직접 구현해보는 것은 매우 유익한 경험이 될 것입니다. 아래에 단계별로 따라 할 수 있는 실습 예제를 제안드릴게요. 이 실습을 통해 이론을 실제로 적용해보며 이해도를 높일 수 있습니다.
// User.java
@Entity
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String email;
// getters and setters
}
// UserRepository.java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/register", "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}
}
// AuthController.java
@RestController
public class AuthController {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(user);
return ResponseEntity.ok("User registered successfully");
}
// 로그인은 Spring Security가 처리
}
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>
<version>1.4.0</version>
</dependency>
// OtpService.java
@Service
public class OtpService {
private final GoogleAuthenticator gAuth = new GoogleAuthenticator();
public String generateSecretKey() {
final GoogleAuthenticatorKey key = gAuth.createCredentials();
return key.getKey();
}
public boolean verifyCode(String secret, int code) {
return gAuth.authorize(secret, code);
}
}
// CustomAuthenticationSuccessHandler.java
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private OtpService otpService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// 로그인 성공 후 OTP 단계로 리다이렉트
response.sendRedirect("/mfa");
}
}
// MFAController.java
@RestController
public class MFAController {
@Autowired
private OtpService otpService;
@PostMapping("/mfa")
public ResponseEntity<?> verifyOtp(@RequestParam String username, @RequestParam int otp) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// 사용자별 비밀키 저장 필요 (예: DB에 저장)
boolean isValid = otpService.verifyCode(user.getSecretKey(), otp);
if(isValid){
return ResponseEntity.ok("MFA Success");
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid OTP");
}
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
spring.mail.host=smtp.sendgrid.net
spring.mail.port=587
spring.mail.username=your_sendgrid_username
spring.mail.password=your_sendgrid_password
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
// EmailService.java
@Service
public class EmailService {
@Autowired
private JavaMailSender mailSender;
public void sendOtpEmail(String to, String otp) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject("Your OTP Code");
message.setText("Your OTP code is: " + otp);
mailSender.send(message);
}
}
// MFAController.java (추가)
@Autowired
private EmailService emailService;
@PostMapping("/send-otp")
public ResponseEntity<?> sendOtp(@RequestParam String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
String secret = user.getSecretKey();
int otp = gAuth.getTotpPassword(secret);
emailService.sendOtpEmail(user.getEmail(), String.valueOf(otp));
return ResponseEntity.ok("OTP sent to email");
}
/register 엔드포인트를 통해 새로운 사용자 등록/login 엔드포인트로 로그인 (Spring Security가 처리)/send-otp 엔드포인트 호출하여 OTP 전송/mfa 엔드포인트로 OTP 입력 및 검증이 실습을 통해 Spring Boot와 Spring Security를 사용하여 기본적인 사용자 인증 시스템을 구축하고, MFA를 추가하여 보안을 강화하는 방법을 배울 수 있습니다. 실제 프로젝트에서 이러한 기능을 구현해보며 경험을 쌓는 것이 중요합니다. 실습 중에 발생하는 문제나 궁금한 점이 있다면 언제든지 질문해 주세요!
성공적인 취업 준비를 응원합니다!