Spring boot 구글 OTP 연동

hyeokjin·2022년 6월 19일
0
post-thumbnail

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의 권한을 동적으로 설정하여
유저별 접근권한에 대한 내용을 작성해보겠다.

profile
노옵스를향해

0개의 댓글