별도의 회원가입 없이 소셜 로그인 기능 제공으로 간편한 로그인 가능.Dolphin 프론트엔드는 회원가입 절차가 따로 없고, 간단하게 소셜 로그인 기능을 제공함으로써 회원가입, 인증절차를 생략했습니다.
우선, 간단하게 국내에서 잘 알려진 네이버의 API를 사용합니다.
네이버 개발자센터 웹사이트에서 네이버 아이디로 로그인 기능을 살펴봅니다.
개발 문서를 자세히 살펴보고 API 신청을 합니다.
애플리케이션의 이름을 정하고 필요한 정보를 필수/선택에 체크합니다.
밑으로 내려보면 로그인 API 서비스 환경 부분에서 환경 추가 셀렉트박스에서 PC 웹을 선택하면 다음과 같은 입력폼이 나타납니다.
서비스 URL은 내 웹애플리케이션의 URL이 될 예정입니다. 지금은 개발상태이므로 localhost를 입력했습니다.
Callback URL은 네이버 OAuth 인증 url로 호출하면, 인증절차 후 네이버에서 리다이렉트해줄 url을 뜻합니다. 저는 /naver/login 으로 했습니다.
등록이 완료되면 client-id와 client-secret 값을 제공해줍니다. 이 값을 이용해서 인증처리를 할 수 있습니다.
spring.io 웹사이트의 튜토리얼 문서를 보면 Facebook 소셜 로그인 기능에 대한 설명이 자세하게 나와있습니다. 이 부분을 참고합니다.
Spring Boot and OAuth2
pom.xml 파일에 의존성을 추가하고 Application 클래스에 @EnableOAuth2Sso 애너테이션을 선언합니다.
튜토리얼대로 application.yaml 파일(application.properties)에 설정 정보를 입력합니다.
clientId, clientSecret 값은 네이버 개발자센터에서 받은 대로 입력합니다.
accessTokenUri는 네이버의 경우 https://nid.naver.com/oauth2.0/token 입니다.
userAuthorizationUri는 네이버의 경우 https://nid.naver.com/oauth2.0/authorize 입니다.
tokenName 은 네이버의 경우 Authorization 입니다.
authenticationScheme 는 네이버의 경우 header 입니다.
clientAuthenticationScheme 는 네이버의 경우 query 입니다.
resource.userInfoUri 는 네이버의 경우 https://openapi.naver.com/v1/nid/me 입니다.
네이버는 개발자 문서에 나와있듯이 인증 요청시 header 에 Authorization 값을 보내라고 명시되어 있습니다.
프로필 정보에 접근하기 위해서는 authenticationScheme 가 header 방식으로 동작해야 합니다.
clientAuthenticationScheme 는 일반적은 query string 방식을 사용합니다.
Spring Security Configuration 클래스를 작성합니다.
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired OAuth2ClientContext oauth2ClientContext; @Autowired MemberService memberService; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/login", "/webjars/**", "/error**").permitAll() .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() .anyRequest().authenticated() .and() .exceptionHandling() .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) .and() .logout() .logoutUrl("/signout") .logoutSuccessUrl("/").permitAll() .and() .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .addFilterBefore(ssoFilter(), BasicAuthenticationFilter.class); } @Bean public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) { FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<OAuth2ClientContextFilter>(); registration.setFilter(filter); registration.setOrder(-100); return registration; } private Filter ssoFilter() { OAuth2ClientAuthenticationProcessingFilter naverFilter = new OAuth2ClientAuthenticationProcessingFilter( "/login/naver"); OAuth2RestTemplate naverTemplate = new OAuth2RestTemplate(naver(), oauth2ClientContext); naverFilter.setRestTemplate(naverTemplate); UserInfoTokenServices tokenServices = new UserInfoTokenServices(naverResource().getUserInfoUri(), naver().getClientId()); tokenServices.setPrincipalExtractor(new NaverPrincipalExtractor()); tokenServices.setRestTemplate(naverTemplate); naverFilter.setTokenServices(tokenServices); naverFilter.setAuthenticationSuccessHandler(new SocialAuthenticationSuccessHandler(memberService)); return naverFilter; } @Bean @ConfigurationProperties("naver.client") public AuthorizationCodeResourceDetails naver() { return new AuthorizationCodeResourceDetails(); } @Bean @ConfigurationProperties("naver.resource") public ResourceServerProperties naverResource() { return new ResourceServerProperties(); } private class NaverPrincipalExtractor implements PrincipalExtractor { private static final String RESPONSE_KEY = "response"; private static final String PRINCIPAL_KEY = "id"; @SuppressWarnings("unchecked") @Override public Object extractPrincipal(Map<String, Object> map) { if(map.containsKey(RESPONSE_KEY)) { Map<String, Object> response = (Map<String, Object>) map.get(RESPONSE_KEY); if(response.containsKey(PRINCIPAL_KEY)) { return response.get(PRINCIPAL_KEY); } } return null; } } }
눈여겨 볼 부분은 oauth2ClientFilterRegistration 빈 메서드를 선언해줌으로써 필터 순서를 조정합니다.
그리고, ssoFilter 메서드는 실제로 Filter 클래스를 반환해서 configure 메서드에서 addFilterBefore에 전달하고 있습니다. 이것은 두번째 인자값인 beforeFilter 클래스보다 먼저 수행할 Filter 를 등록하는 부분입니다. 즉, BasicAuthenticationFilter 보다 OAuth2ClientAuthenticationProcessingFilter 를 먼저 필터링한다는 뜻입니다.
OAuth2 인증을 위해서는 인증절차에 사용되는 OAuth2RestOperations 구현체이며 RestTemplate 클래스를 상속받은 OAuth2RestTemplate 클래스를 filter에 set해줍니다.
RestTemplate을 이용하여 HTTP 통신을 사용함을 알 수 있습니다.
OAuth2RestTemplate 의 생성자 첫번째 인자로 naver() 빈메서드가 전달됩니다.
application.yaml 파일에서 설정했던 naver.client 의 속성값들이 AuthorizationCodeResourceDetails 클래스에 인스턴스로 구현됩니다.
OAuth2ClientAuthenticationProcessingFilter 클래스는 ResourceServerTokenServices 클래스를 세터로 전달받습니다.
OAuth2RestTemplate 으로 accessToken 을 얻어내면, UserInfoTokenService 클래스를 이용해서 Resource 정보(ex. 프로필정보) 등을 받아냅니다.
Naver 프로필 정보에서 Principal을 만들기 위해서 PrincipalExtractor 클래스를 구현한 NaverPrincipalExtractor 내부 클래스를 작성합니다.
Naver API 에서 사용자의 프로필정보에는 고유번호인 id 필드가 전달됨으로, 이 값을 Principal 의 name으로 사용합니다.
마지막으로, naverFilter 변수에 authenticationSuccessHandler 를 전달합니다.
필터의 마지막에 인증이 성공한 경우 AuthenticationSuccessHandler 의 onAuthenticationSuccess 메서드가 호출됩니다.
이 메서드에서, Authentication 정보가 전달되는데, 이를 토대로 Dolphin 서버에 프로필 정보를 저장합니다.
SocialAuthenticationSuccessHandler 클래스 소스코드는 다음과 같이 구현했습니다.
AbstractAuthenticationProcessingFilter 추상클래스에서는 authenticationSuccessHandler 클래스를 별도로 지정하지 않는 경우, 디폴트로 SavedRequestAwareAuthenticationSuccessHandler 클래스를 사용함을 알 수 있습니다.
이 클래스를 그대로 사용하되, 내부에 Dolphin Member 로 등록하는 과정만을 추가하였습니다.
이제, OAuth2ClientAuthenticationProcessingFilter 생성자 인자로 전달했던, defaultFilterProcessingUrl 인 /login/naver 를 호출하면 네이버 로그인 절차가 진행됨을 확인 할 수 있습니다.