티스토리 뷰

Spring/Security

oauth2 - 구글 로그인

Bong Gu 2020. 10. 27. 19:13
728x90

google_logo

Spring

SpringSecurity-OAuth2

최근 서비스들은 자체적으로 계정을 가입하고, 접근하는 방식도 있지만, 구글, 카카오, 네이버, 페이스북등으로 계정관리를 하고있습니다.
이번 포스팅에서는 Spring Security & 구글 OAuth를 사용하여 계정 권한관리에 대해 알아보려고 합니다.
빌드 도구로는 gradle을 사용해 보려고 합니다.


구글 OAuth

의존성 추가

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

    compileOnly 'org.projectlombok:lombok'

    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'mysql:mysql-connector-java'

    annotationProcessor 'org.projectlombok:lombok'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testCompile('org.springframework.security:spring-security-test')
    testCompile('io.rest-assured:rest-assured:3.0.3')

    compile("org.mariadb.jdbc:mariadb-java-client")
    compile group: 'org.springframework.security.oauth', name: 'spring-security-oauth2', version: '2.3.5.RELEASE'
}
  • SpringBoot 버전이 2점대로 올라가면서 의존성 추가 방법이 조금 다를 수 있다.
  • rest-assured은 Rest API Test를 위한 의존성

application.yml 설정

spring:
  profiles: local

  jpa:
    show-sql: true
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: true

  logging:
    level:
      org.hibernate.type: trace # JPA로 생성되는 쿼리의 파라미터 값 확인

SpringSecurity 기본인증 옵션 비활성화

  • SpringSecurity가 의존성에 있으면 기본인증이 자동으로 추가된다.

  • 현재 상황에 필요한 기능이 아님으로 비활성화

    • 기존 스프링부트 1점대에서는 properties파일 또는 yml 파일에서 security.basic.enabled: false로 변경할 수 있었다.

    • 2점대 부터는 아래와 WebConfigurerAdapter를 상속하는 @Configuration 클래스를 만들어서 설정해줘야한다.
      oauth2

    • 모든 요청에 대하여 인증을 하지 않은것으로 설정한다.
      oauth2

index.html 호출 test 파일작성

  • 기본 루트로 접속 시, index.html을 호출하는지 확인하기 위해 테스트 클래스를 작성한다.
    oauth2

구글 OAuth 등록

  • 구글 개발자 콘솔에 접속한다.

  • 프로젝트 생성
    oauth2

  • 프로젝트 정보 입력 및 생성
    oauth2

  • 만들어진 프로젝트를 선택한다.

  • 사용자 인증 정보 페이지로 이동한다.
    oauth2

  • OAuth 클라이언트 ID 생성
    oauth2

  • OAuth 동의 화면에서 어플리케이션 이름을 설정해준다.
    oauth2

  • 정보 입력
    oauth2

    • 이름(어플리케이션 이름이 아니다.)
    • 테스트용으로 localhost 주소를 사용했다.
  • 완료하게 되면 클라이언트 ID 및 보안(security)를 확인할 수 있다.

구글 인증 정보를 프로젝트에 적용하기

  • 구글 인증 정보를 프로젝트 yml파일에 등록한다.

       google:
         client:
           clientId: 
           clientSecret: 
           accessTokenUri: https://accounts.google.com/o/oauth2/token
           userAuthorizationUri: https://accounts.google.com/o/oauth2/auth
           clientAuthenticationScheme: form
           scope: email, Profile
         resource:
           userInfoUri: https://www.googleapis.com/oauth2/v2/userinfo
  • git 인증정보가 노출 될 수 있기 때문에 별도의 yml파일을 생성하고 gitignore설정을 해준다.
    oauth2

  • 프로젝트 실행 시, 호출될 수 있도록 Application.java에 추가해준다.(여러개일때는 , 필수)
    oauth2

  • SecurityConfig에 아래와 같은 설정을 해준다.

    @Configuration
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
                .authorizeRequests()
                .antMatchers("/", "/h2-console/**", "/favicon.ico", "/login**").permitAll()
                .anyRequest().authenticated()
                .and().logout().logoutSuccessUrl("/").permitAll()
                .and().headers().frameOptions().sameOrigin()
                .and().csrf().disable();
    }
    }
    • csrf를 끈 이유
      • 로컬환경에서 H2 Console에 접근하기위해
      • 불필요한 테스트 코드 작성을 줄이기 위해
    • 루트, h2-console, 파비콘, login 등에 권한체크 제외설정을 해준다.

Google OAuth 설정

  • OAuth 설정 파일 생성

    • @Configuration

    • @EnableOAuth2Client

      • OAuth2ClientContext가 빈으로 등록이 가능하다.
    • 구글로그인 정보를 받을 빈을 등록한다.

      @Bean
      @ConfigurationProperties("google.client")
      public OAuth2ProtectedResourceDetails googleClient() {
          return new AuthorizationCodeResourceDetails();
      }
      
      @Bean
      @ConfigurationProperties("google.resource")
      public ResourceServerProperties googleResource() {
          return new ResourceServerProperties();
      }    
    • 필터를 빈으로 등록하고 설정한다.

      @Bean
      public Filter ssoFilter(){
          OAuth2ClientAuthenticationProcessingFilter oauth2Filter = new OAuth2ClientAuthenticationProcessingFilter("/login/google");
          OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(googleClient(), oAuth2ClientContext);
          oauth2Filter.setRestTemplate(oAuth2RestTemplate);
          oauth2Filter.setTokenServices(new UserInfoTokenServices(googleResource().getUserInfoUri(), googleClient().getClientId()));
          oauth2Filter.setAuthenticationSuccessHandler(google);
          return oauth2Filter;
      }
      • OAuth2ClientAuthenticationProcessingFilter의 인자값 = OAuth 로그인 시작 포인트
      • OAuth2RestTemplate 설정 및 필터에 설정
      • UserInfoTokenServices 설정 (토큰을 얻을 클라이언트 설정)
      • SuccessHandler 등록
    • 필터를 등록 할 FilterRegistrationBean을 빈으로 등록

      @Bean
      public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
          FilterRegistrationBean registration = new FilterRegistrationBean();
          registration.setFilter(filter);
          registration.setOrder(-100);
          return registration;
      }
    • Authentication 객체를 주입받아 로그인 정보를 얻는다.
      private LoginAccount getGoogleUser(Authentication authentication) { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication; return objectMapper.convertValue(oAuth2Authentication.getUserAuthentication().getDetails(), LoginAccount.class); }

      • Authentication객체를 OAuth2Authentication로 형변환
      • ObjectMapper 객체를 미리 만들어놓은 LoginAccount 객체와 매핑하여 컨버트 한다.
  • 문제발생..

    • 스프링부트 2점대 버전이 되면서 Spring Security OAuth 기능의 일부가 Spring Security로 마이그레이션 되는중이라 지원안하는 기능 발생
    • UserInfoTokenService, ResourceServerProperties 클래스 및 @EnableOAuth2Sso 애노테이션 사용이 불가능
    • 2점대에서 사용하는 방법을 숙지하고 싶었으나.. OAuth2에 대한 이해가 적고, SpringSecurity에 대한 이해가 부족하여 임시방편을 사용하였다.
      • spring-security-oauth2 → spring-security-oauth2-autoconfigure로 변경
      • spring-cloud-security로 변경하는것도 가능하다고 한다.
  • 생성한 ssoFilter가 Security를 거치도록 설정한다.
    oauth2

    • OAuthConfig에서 생성한 ssoFilter 추가
    • 테스트 파일 oauth2
      • google.yml파일을 테스트 시에도 사용할 수 있도록 @TestPropertySource로 추가
      • OAuth2 로그인의 경우 인증코드(code)를 발급 받고, 발급 받은 인증코드로 AccessToken을 다시 발급 받는 과정이 있어 중간 리다이렉션이 발생합니다.
        • 이런 과정들로 login URL 테스트 결과를 확인하기 어려워 리다이렉션을 방지 하였다.
          >302코드
          "3xx Redirection"클래스에 속한다.
          301 : 완전히 새로운 URL로 이동
          302 : 임시적으로 새로운 URL로 이동

브라우저에서 확인

로그인 세션 관리

  • OAuth2를 사용하는것은 사용자 인증 및 허가된 정보를 가져오는것

  • 인증된 정보를 통해 로그인 세셔관리가 필요하다.

  • 세션을 사용하는 방법

    • 톰캣 세션 사용
      • HttpSession을 사용할 경우
      • 2대이상의 WAS가 구동되는 환경에서는 톰캣들간의 세션 공유를 위한 추가설정이 필요
    • Database를 세션저장소로 사용(진행할 예정)
      • WAS들간의 공용 세션을 사용할 수 있는 가장 쉬운 방법
      • 많은 설정이 필요없지만, 결국 로그인 요청마다 DB IO가 발생하여 성능상 이슈가 발생할 수 있다.
      • 보통 로그인이 요청이 많이 없는 백오피스, 사내시스템 용도에서 사용합니다.
    • Redis, Memcached등의 메모리 DB를 세션 저장소로 사용
      • 사용자 서비스에서 가장 많이 사용되는 방식입니다.
      • 실제 서비스로 사용하기 위해서는 Embedded Redis와 같은 방식이 아닌 외부 메모리 서버가 필요합니다.
      • 참고 : Havi님 블로그

Database를 세션저장소로 사용

의존성 추가

  • jdbc 및 session 의존성을 추가한다.
compile group: 'org.springframework.session', name: 'spring-session', version: '1.3.5.RELEASE'
compile('org.springframework.boot:spring-boot-starter-jdbc')

Session 테이블 생성

  • 세션의 저장을 DB에 하기 위해서 세션 테이블이 생성되어야한다.

    • SpringSession에서 어떠한 형태로 쿼리문을 사용하는지 알려준다.

    • 맥/intelij 기준으로 , Command + Shift + o schema-검색하면 아래와 같이 DBMS에 맞춰 스키마 쿼리를 확인할 수 있다.
      oauth2

    • 복사하여 새로 작성한다.

      • resources 아래 schema-h2.sql
  • 새로 작성한 스키마를 적용하도록 application.yml에 설정한다.

spring:
  datasource:
    data: classpath:schema-h2.sql

JdbcSession 옵션 활성화

SuccessHandler 생성

  • AuthenticationSuccessHandler를 implements하는 클래스 생성

    @Component
    public class GoogleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private HttpSession httpSession;
    private ObjectMapper objectMapper;
    
    public GoogleAuthenticationSuccessHandler(HttpSession httpSession, ObjectMapper objectMapper) {
        this.httpSession = httpSession;
        this.objectMapper = objectMapper;
    }
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        httpSession.setAttribute(SessionConstants.LOGIN_USER, getGoogleUser(authentication)); 
        /* 간단한 구글계정 정보를 세션에 저장*/
        response.sendRedirect("/me");
    }
    
    /* OAuth 인증정보를 통해 GoogleUser 인스턴스 생성 */
    private GoogleUser getGoogleUser(Authentication authentication) { 
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        return objectMapper.convertValue(oAuth2Authentication.getUserAuthentication().getDetails(), GoogleUser.class);
    }
    }
    • ObjectMapper의 경우 스프링부트에서 빈으로 등록해준다.(과거에는 아니었지만..)
    • 이 SuccessHandler는 로그인 유저의 기본적인 정보를 LOGIN_USER라는 키값에 담는다.
  • SessionConstants.class 생성
    oauth2

  • 리다이렉트 되는 "/me" 컨트롤러
    oauth2

  • GoogleUser.class 생성
    oauth2

    • ObjectMapper가 필드를 명확히 인식하기 위해 @JsonProperty로 필드명을 지정
    • @JsonIgnoreProperties(ignoreUnknown = true) : 멤버변수로 지정되지 않은 필드는 무시
    • Serializable

SuccessHandler 적용

  • 완성된 SuccessHandler를 필터에 적용시킨다.
    oauth2

결과

  • localhost:8080/me 접속
    • 인증 오류 403 발생
      oauth2
  • localhost:8080/login 접속 및 google 로그인
    • 유저 정보 확인
      oauth2

많은 자료들이 스프링부트 1점대를 기준으로 작성되어 작업이 생각보다 오래걸렸습니다..😂
구글로그인을 해보고나니, 여러가지 다른 인증들도 해보고 싶어졌습니다.
음.. 이번 내용들을 블로깅하면서 내가 security부분도 많이 부족하고 강의를 보고 복습도했지만..
아직 스프링 그리고 스프링부트를 완벽히 이해하고 있지 않다는 생각이 들었습니다.



728x90
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday