2020년 8월 4일 화요일

간단하게 OAuth 2.0 사용해보기

간단하게 OAuth 2.0 사용해보기

  • OAuth란?

    • 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준

    • ID와 비밀번호 대신 엑세스 토큰기반으로 사용자를 식별할 수 있다. 토큰은 리소스 서버만 제공한다.

  • 기본 구조

    • Resource Owner : ID와 비밀번호를 이용해 Resource Client에게 권한을 인가하여 엑세스 토큰을 획득하게 될 주체

    • Resource Client : ResourceOwner로부터 사용 인가를 받아, 소유자 대신 엑세스 토큰을 획득하고 해당 토큰 을 통해 Resource Server의 API를 사용하는 주체

    • Resource Server : 보호된 리소스를 관리하며 리소스 클라이언트가 사용할 API를 제공하는 주체. 엑세스 토큰이 유효한지 확인하기 위해 Authorization Server와 통신을 주고 받기도함

    • Authorization Server : 엑세스 토큰과 인가 코드를 관리하는 서버. 엑세스 토큰 검증, 폐기

  • 기본 동작 구조

    • ex) 리소스 오너 : 사용자 / 리소스 클라이언트 : 쇼핑몰 웹(앱) / 리소스 서버, 인가 서버 : 카카오
    • 사용자 -> 쇼핑몰 실행 -> 로그인 -> 카카오 로그인 버튼 클릭
    • 웹 -> 카카오로 인증페이지 요청 -> 카카오 로그인화면 이동
    • 사용자는 카카오 ID,PW 입력(토큰 요청) -> 카카오는 인증페이지를 요청했던 쇼핑몰에 엑세스 토큰 발급
    • 쇼핑몰에선 해당 엑세스 토큰을 가지고 다시 카카오API 호출 -> 정상적인 토큰일 경우 사용자 정보 리턴
  • 내가 적용한 예시

    • 아래의 두 가지 방법 중 하나로 가능하다. 특히 Spring Security가 제공하는 OAuth2ClientAuthenticationProcessingFilter 를 사용하면 토큰받기, 사용자정보 받기 과정을 자동으로 할 수 있다.

    • REST API 직접 호출을 통한 구현

      • 카카오로부터 AccessToken 받기

        public JsonNode getKakaoAccessToken(String code) {
        
        
          final String RequestUrl = "https://kauth.kakao.com/oauth/token";
          final List<NameValuePair> postParams = new ArrayList<NameValuePair>();
        
        
          //포스트 파라미터의 grant_type이라는 명칭에 authorization_code를 추가한다 아래도 동일
          postParams.add(new BasicNameValuePair("grant_type", "authorization_code"));
          postParams.add(new BasicNameValuePair("client_id", kakaoClientId));
          postParams.add(new BasicNameValuePair("redirect_uri", redirectUri));
          postParams.add(new BasicNameValuePair("code", code));
        
        
          final HttpClient client = HttpClientBuilder.create().build();
          final HttpPost post = new HttpPost(RequestUrl);
        
        
          JsonNode returnNode = null;
        
        
          try {
              post.setEntity(new UrlEncodedFormEntity(postParams));
              final HttpResponse response = client.execute(post);
              ObjectMapper mapper = new ObjectMapper();
              returnNode = mapper.readTree(response.getEntity().getContent());
          } catch (UnsupportedEncodingException e) {
              e.printStackTrace();
          } catch (ClientProtocolException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
        
        
          return returnNode;
        }
        
      • 받은 토큰을 통해 사용자 프로필 받기

        public JsonNode getKakaoUserProfile(String access_token) {
        
        
            final String RequestUrl = "https://kapi.kakao.com/v2/user/me";
        
        
            final HttpClient client = HttpClientBuilder.create().build();
            final HttpPost post = new HttpPost(RequestUrl);
        
        
            post.addHeader("Authorization", "Bearer " + access_token);
        
        
            JsonNode returnNode = null;
        
        
            try {
                final HttpResponse response = client.execute(post);
        
        
                ObjectMapper mapper = new ObjectMapper();
                returnNode = mapper.readTree(response.getEntity().getContent());
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        
        
            return returnNode;
        }
        
    • Security 필터 사용

      • SecurityConfig

        @Configuration
        @EnableWebSecurity
        public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        
            private final Filter ssoFilter;
        
        
            public SecurityConfig(Filter ssoFilter) {
                this.ssoFilter = ssoFilter;
            }
        
        
            @Override
            public void configure(WebSecurity web) throws Exception {
                web.ignoring().antMatchers("/resources/**","resources/**");
                web.httpFirewall(new DefaultHttpFirewall());
            }
        
        
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.csrf().disable();
                http.authorizeRequests()
                        .antMatchers("/", "/course/admin/**").hasAuthority("ROLE_ADMIN")
                        .antMatchers("/", "/admin/**").hasAuthority("ROLE_ADMIN")
                        .and()
                            .formLogin()
                            .loginPage("/")
                            .loginProcessingUrl("/loginProcess.ajax")
                            .usernameParameter("id")
                            .passwordParameter("password")
                            .successForwardUrl("/loginCheck.ajax")
                            .permitAll()
                        .and()
                            .logout()
                            .logoutUrl("/logout")
                            .logoutSuccessUrl("/")
                            .deleteCookies("JSESSIONID")
                            .invalidateHttpSession(true)
                        .and()
                            .addFilterBefore(ssoFilter, BasicAuthenticationFilter.class)
                        .httpBasic();
            }
        
        
            @Bean
            public PasswordEncoder passwordEncoder(){
                // 버전차이로 PasswordEncoderFactories.createDelegatingPasswordEncoder(); 는 사용못하는 것 같음
                return new BCryptPasswordEncoder();
            }
        
        
        }
        
        
        
      • OAuthConfig

        @Configuration
        @EnableOAuth2Client
        @PropertySource(value = "classpath:socialInfo.properties")
        public class OAuthConfig {
        
        
            @Autowired
            @Qualifier("oauth2ClientContext")
            OAuth2ClientContext oauth2ClientContext;
        
        
            @Bean
            public Filter ssoFilter() {
                CompositeFilter filter = new CompositeFilter();
                List<Filter> filterList = new ArrayList<>();
        
        
                filterList.add(kakaoSsoFilter());
                filterList.add(googleSsoFilter());
                filter.setFilters(filterList);
        
        
                return filter;
            }
        
        
            public Filter kakaoSsoFilter() {
                OAuth2ClientAuthenticationProcessingFilter oauth2Filter = new OAuth2ClientAuthenticationProcessingFilter("/login/kakao/oauth");
                OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(kakaoClient(), oauth2ClientContext);
                oauth2Filter.setRestTemplate(oAuth2RestTemplate);
                oauth2Filter.setTokenServices(new UserInfoTokenServices(kakaoResource().getUserInfoUri(), kakaoClient().getClientId()));
                oauth2Filter.setAuthenticationSuccessHandler(kakaoSuccessHandler());
        
        
                return oauth2Filter;
            }
        
        
            public Filter googleSsoFilter() {
                OAuth2ClientAuthenticationProcessingFilter oauth2Filter = new OAuth2ClientAuthenticationProcessingFilter("/google/googleSignInCallback");
                OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(googleClient(), oauth2ClientContext);
                oauth2Filter.setRestTemplate(oAuth2RestTemplate);
                oauth2Filter.setTokenServices(new UserInfoTokenServices(googleResource().getUserInfoUri(), googleClient().getClientId()));
                oauth2Filter.setAuthenticationSuccessHandler(successHandler());
        
        
                return oauth2Filter;
            }
        
        
            @Bean
            public AuthenticationSuccessHandler successHandler(){
                return (request, response, authentication) -> {
        
        
                    response.sendRedirect("/login/googleSignIn");
                };
            }
        
        
            @Bean
            public AuthenticationSuccessHandler kakaoSuccessHandler(){
                return (request, response, authentication) -> {
        
        
                    response.sendRedirect("/login/kakaoSignin");
                };
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "google.client")
            public OAuth2ProtectedResourceDetails googleClient() {
                return new AuthorizationCodeResourceDetails();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "google.resource")
            public ResourceServerProperties googleResource() {
                return new ResourceServerProperties();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "kakao.client")
            public OAuth2ProtectedResourceDetails kakaoClient() {
                return new AuthorizationCodeResourceDetails();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "kakao.resource")
            public ResourceServerProperties kakaoResource() {
                return new ResourceServerProperties();
            }
        
        
            @Bean
            public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
                FilterRegistrationBean registration = new FilterRegistrationBean();
                registration.setFilter(filter);
                registration.setOrder(-100);
                return registration;
            }
        }
        

참고 및 출처

댓글 없음:

댓글 쓰기