간단하게 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; } }
참고 및 출처
- 위키백과 https://ko.wikipedia.org/wiki/OAuth
- 생활코딩 OAuth2.0 https://opentutorials.org/course/2473/16571
- 카카오 로그인 가이드 https://developers.kakao.com/docs/latest/ko/kakaologin/common
- 책: 학교에서 알려주지 않는 17가지 실무 개발 기술