기본 콘텐츠로 건너뛰기

Dolphin 2

별도의 회원가입 없이 소셜 로그인 기능 제공으로 간편한 로그인 가능.
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 를 호출하면 네이버 로그인 절차가 진행됨을 확인 할 수 있습니다.

이 블로그의 인기 게시물

Docker Hadoop Hbase

Docker Hadoop Zookeeper 글에서 만든 컨테이너에서 계속 작업합니다. 우선 hduser로 접속합니다. 그리고 hbase 바이너리를 다운로드 받습니다. 바이너리 파일을 /usr/local/hbase 로 옮겨주고 권한을 줍니다. su hduser cd /tmp wget http://archive.apache.org/dist/hbase/stable/hbase-1.4.7-bin.tar.gz tar xvf hbase-1.4.7-bin.tar.gz sudo mkdir /usr/local/hbase sudo mv hbase-1.4.7/* /usr/local/hbase sudo chown -R hduser:hadoop /usr/local/hbase ./conf/hbase-env.sh 파일에서 JAVA_HOME 환경변수를 설정합니다. vim ./conf/hbase-env.sh export JAVA_HOME=/usr/lib/jvm/java-8-oracle 이제 로컬 파일시스템으로 동작하는 hbase를 실행해 보겠습니다. 다음의 명령어를 입력하면 hbase shell 로 접속할 수 있습니다. ./bin/start-hbase.sh ./bin/hbase shell Pseudo-Distributed Local System 으로 동작시키기 위해서는 환경설정을 몇가지를 추가하면 됩니다. 먼저, hbase가 실행중이라면 중지합니다. ./bin/stop-hbase.sh hbase-site.xml 파일을 편집합니다. vim ./conf/hbase-site.xml <property> <name>hbase.cluster.distributed</name> <value>true</value> </property> <property> <name>hbase.rootdir</name> <value>hdfs://localhost:9000/...

냉장고 가계부 프로젝트 31

fridge-web에서 fridge 개인화 영역을 아직 구현하지 못했다. 개인화 데이터의 관리는 별도의 도메인으로 관리하기 위해서 fridge-member 프로젝트를 생성한다. fridge-member 의존성 라이브러리는 다음과 같다. <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-java8</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Spring Cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-cli...

냉장고 가계부 프로젝트 9

HATEOAS 적용을 일관성있게 하기위해서 FoodController 메서드 중 새로운 식품을 등록하는 메서드인 postFood 메서드도 리턴유형을 ResponseEntity<Food> 에서 ResponseEntity<FoodResource> 로 수정한다. 우선 테스트 메서드 postSave 부터 수정한다. @Test public void postSave() throws Exception { given(jpaFoodService.save(any(Food.class))).willReturn(milk); final ResultActions result = mvc.perform(post("/foods") .content(mapper.writeValueAsString(milk)) .contentType(MediaType.APPLICATION_JSON_UTF8)); result.andExpect(status().isCreated()) .andExpect(redirectedUrlPattern("**/foods/{id:\\d+}")); verifyResultContent(result); } 변경된 부분은 mvc.perform 메서드를 ResultActions 형의 result 변수에 할당하고있다. 할당한 변수를 내부검증 메서드인 verfiyResultContent 메서드에 파라미터로 전달하고 있다. 다음은 FoodController 의 postFood 메서드이다. @PostMapping public ResponseEntity<FoodResource> postFood(@RequestBody final Food food) { jpaFoodService.save(food); URI location = ...