Sptring boot + Maven 프로젝트에서
Spring Security + 구글 OTP를 이용한 2단계 보안인증을 하는 방법이다.
예제 : https://github.com/ihoneymon/spring-security-2step-verification
예제를 살펴보고 재료들은 모두 등록해놨다. 이후 핵심내용을 정리해본다.
1. 회원등록(구글 OTP QR주소를 메일에 전달)
2. 로그인시 구글 OTP 인증
application.yml 설정에서 발신자 정보를 넣는다
spring: datasource: initialize: true # data.sql 을 이용한 DB 초기화 작업 mail: default-encoding: UTF-8 username: #${username} password: #${password} host: smtp.gmail.com port: 587 protocol: smtp properties: mail.smtp.starttls.enable: true mail.smtp.auth: true h2: # jdbc ur: jdbc:h2:mem:testdb console: # http://localhost:8080/h2-console enabled: true
먼저 회원 등록시 구글 권한키를 발급하여 QR주소를 메일에 전송한다
아이디, 패스워드는 DB에 저장하고 정상적으로 처리되면 해당 메일(아이디)로 OTP QR주소를 전송한다 사용자는 스마트폰을 이용해 구글 OTP를 등록한다.public String sendUserOtp(User user) { String msg = ""; boolean result = false; GoogleAuthenticatorKey key = googleAuthenticator.createCredentials(); String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL(ISSUER, user.getUsername(), key); Map<String, Object> attributes = new HashMap<>(); attributes.put("qrCodeUrl", qrCodeUrl); try { User loginUser = userMapper.selectUserLogin(user.getUsername()); User vo = new User(); vo.setId(loginUser.getId()); vo.setOtpSecretKey(key.getKey()); result = userMapper.updateOtpKey(vo); } catch (Exception e) { System.out.print("sendUserOtp : " + e.getMessage()); } if (result) { MailMessage mailMessage = MailMessage.builder() .templateName(MailMessage.OTP_REGISTRATION) .to(new String[]{user.getUsername()}) .subject("Bonanza Plug OTP Registration mail") .attributes(attributes) .build(); mailService.send(mailMessage); msg = "otp_success"; } else { msg = "otp_fail"; } return msg; }
이는 로그인시 Spring Security config에서 구글 OTP 인증 필터를 거쳐서 인증이 된다. (authenticationDetailsSource)
(addFilterBefore는 추후 권한 설정에 사용되므로 무시하자)@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() //.antMatchers("/*").permitAll() //.antMatchers("/login").permitAll() //.antMatchers(HttpMethod.POST,"/userRegist").permitAll() .anyRequest().authenticated() .and() .formLogin().authenticationDetailsSource(new TOTPWebAuthenticationDetailsSource()) // ID, PW 이외의 추가 OTP 인증 .loginPage("/login").defaultSuccessUrl("/list") .failureUrl("/login?error").failureHandler(new ExtensibleAuthenticationFailureHandler()).permitAll() .and() .exceptionHandling() .accessDeniedHandler(accessDeniedHandler()) // 권한 인증 실패시 처리할 핸들러 .and() .logout() .logoutSuccessUrl("/login?logout").permitAll() .invalidateHttpSession(true) /*로그아웃시 세션 제거*/ .deleteCookies("JSESSIONID") /*쿠키 제거*/ .clearAuthentication(true) /*권한정보 제거*/ .permitAll() .and().sessionManagement() //.maximumSessions(1) /* session 허용 갯수 */ //.expiredUrl("/login") /* session 만료시 이동 페이지*/ //.maxSessionsPreventsLogin(true) /* 동일한 사용자 로그인시 x, false 일 경우 기존 사용자 session 종료*/ .and() .addFilterBefore(customFilterSecurityInterceptor(), FilterSecurityInterceptor.class); // 새로운 인가 필터 적용(권한셋팅) }
로그인 페이지에서 로그인시 아이디, 패스워드, OTP 번호를 입력하여 로그인한다. (재발급으로 표시된 부분은 무시해도 된다)
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User loginUser = null; User user = null; try { loginUser = userMapper.selectUserLogin(username); String password = loginUser.getPassword(); //String test = passwordEncoder.encode("1"); user = User.builder() .username(username) .password(password) //.roles("USER") .build(); user.setRole(loginUser.getRole()); if (loginUser.getRole() != null && !"".equals(loginUser.getRole())) { user.setAuthorities(Arrays.asList(new SimpleGrantedAuthority(loginUser.getRole()))); } // 재발급 (OTP 9000으로 재로그인) if ("9000".equals(loginUser.getOtpSecretKey()) ) { GoogleAuthenticatorKey key = googleAuthenticator.createCredentials(); User users = new User(); users.setId(loginUser.getId()); users.setOtpSecretKey(key.getKey()); boolean result = userMapper.updateOtpKey(users); if (result) { sendTOTPRegistrationMail(user, key); user.changeOtpSecretKey(key.getKey()); } } else { user.setOtpSecretKey(loginUser.getOtpSecretKey()); } } catch (Exception e) { System.out.print("loadUserByUsername : " + e.getMessage()); } return user; }
마지막으로 구글 권한키를 체크하여 결과적으로 값을 반환한다(ExtensibleUserDetailsAuthenticationProvider)
// OTP 인증 정보 상세를 만들어 반환하는 역할을 수행한다. @Bean AuthenticationProvider authenticationProvider(UserService userService) { ExtensibleUserDetailsAuthenticationProvider authenticationProvider = new ExtensibleUserDetailsAuthenticationProvider(); authenticationProvider.setPasswordEncoder(passwordEncoder()); authenticationProvider.setUserDetailsService(userService); authenticationProvider.setAuthenticator(googleAuthenticator()); return authenticationProvider; }
다음은 Spring Security의 권한을 동적으로 설정하여
유저별 접근권한에 대한 내용을 작성해보겠다.