티스토리 뷰

Development/JAVA

Spring Boot security OAuth2 연동

쥬리리리 2021. 7. 27. 22:58

프로젝트를 진행하다보니 쇼핑몰이나 다른 사이트에서 많이 보던 sns 로그인을 연동하여 사용하기로 했다

 

기존에 security로 로그인이 구현되어 있는 상태이다.

 

1. POM.XML oauth2 client add

<dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-oauth2-client</artifactId>
 </dependency>

 

2. clientId와 clientSecret setting, application.yml (해당 사이트에서 발급) - 따로 포스팅 되어 있지 않으니 발급 절차는 검색하세요

spring:
  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: 발급받은 client-id key
            client-secret: 발급받은 client-secret key
          google:
            client-id: 발급받은 client-id key
            client-secret: 발급받은 client-secret key
          naver:
            client-id: 발급받은 client-id key
            client-secret: 발급받은 client-secret key
          facebook:
            client-id: 발급받은 client-id key
            client-secret: 발급받은 client-secret key

 

3. WebSecurityConfigurerAdapter를 상속받은 class에서 ouath2Login 추가 (기존 security configure class에 추가)

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers().frameOptions().disable()
        .and()
            .csrf().disable()
        .authorizeRequests()
        	.antMatchers("/member/update").authenticated()
            .anyRequest().permitAll()
        .and()
            .exceptionHandling()
            .authenticationEntryPoint(new CustomAuthenticationEntryPoint("/" + loginPage))
        .and()
            .formLogin()
            .loginPage("/" + loginPage)
            .permitAll()
            .usernameParameter("j_username")
            .passwordParameter("j_password")
            .loginProcessingUrl("/j_spring_security_check")
            .defaultSuccessUrl("/", false)
            .failureUrl("/" + loginPage + "?login=fail")
         .and()
            .oauth2Login()
                .userInfoEndpoint()
                    .userService(customOauth2UserService)
        .and()
            .defaultSuccessUrl("/",false)
         .and()
            .logout().permitAll()
            .logoutUrl("/j_spring_security_logout")
            .logoutSuccessUrl("/")
            .deleteCookies("JSESSIONID")
            .invalidateHttpSession(true)
        .and()
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.NEVER)
            .sessionFixation().migrateSession()
            .maximumSessions(3)
        ;

    }

 

 

4. CustomOAuth2Provider.class 추가 (기본적으로 구글, 페이스북은 CommonOAuth2Provider를 지원, 

지원하지 않는 네이버, 카카오를 클래스에 추가해준다)

※ 로그인 인증 후에는 기본적으로 CustomOauth2UserService.java에 오버라이드 되어 있는 loadUser 메소드로 들어오게 되어 있는데  구글은 기본적으로 지원되는 CommonOAuth2Provider scope에 openid이 포함되어 있어 인증 후에는 OidcUserService를 통해 인증이 진행된다.

따라서, ​CustomOauth2UserService.java loadUser에서 후 처리를 해주려면 openid를 제외해주면 된다.

 

 

public enum CustomOAuth2Provider {
   KAKAO {
       public ClientRegistration.Builder getBuilder(String registrationId) {
           ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.POST, DEFAULT_LOGIN_REDIRECT_URL);
           builder.scope("profile");
           builder.authorizationUri("https://kauth.kakao.com/oauth/authorize");
           builder.tokenUri("https://kauth.kakao.com/oauth/token");
           builder.userInfoUri("https://kapi.kakao.com/v2/user/me");
           builder.userNameAttributeName("id");
           builder.clientName("Kakao");
           return builder;
       }

   },
   NAVER {
       public ClientRegistration.Builder getBuilder(String registrationId) {
           ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.POST, DEFAULT_LOGIN_REDIRECT_URL);
           builder.scope("profile");
           builder.authorizationUri("https://nid.naver.com/oauth2.0/authorize");
           builder.tokenUri("https://nid.naver.com/oauth2.0/token");
           builder.userInfoUri("https://openapi.naver.com/v1/nid/me");
           builder.userNameAttributeName("response");//네이버는 response로 지정
           builder.clientName("Naver");
           return builder;
       }

   },
    GOOGLE {
        public ClientRegistration.Builder getBuilder(String registrationId) {
            ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.POST, DEFAULT_LOGIN_REDIRECT_URL);
            builder.scope(new String[]{"profile", "email"});
            builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
            builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
            builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
            builder.userNameAttributeName("sub");
            builder.clientName("Google");
            return builder;
        }
    };

    private static final String DEFAULT_LOGIN_REDIRECT_URL = "{baseUrl}/login/oauth2/code/{registrationId}";

    protected final ClientRegistration.Builder getBuilder( String registrationId, ClientAuthenticationMethod method, String redirectUri) {
        ClientRegistration.Builder builder = ClientRegistration.withRegistrationId(registrationId);
        builder.clientAuthenticationMethod(method);
        builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
        builder.redirectUriTemplate(redirectUri);
        return builder;
    }

    public abstract ClientRegistration.Builder getBuilder(String registrationId);
}

 

 

5. SecurityConfig.java 파일에 ClientRegistrationRepository 설정
(기본적으로 spring.security.oauth2.client.registration 안에 있는 값들을 프로젝트 빌드 시 모두 주입 시킨다)

 

@Bean
    public ClientRegistrationRepository clientRegistrationRepository(OAuth2ClientProperties oAuth2ClientProperties){
        
        List<ClientRegistration> registrations = oAuth2ClientProperties.getRegistration().keySet().stream()
                .map(client -> getRegistration(oAuth2ClientProperties, client))
                .filter(Objects::nonNull) .collect(Collectors.toList());

        return new InMemoryClientRegistrationRepository(registrations);
    }

    public ClientRegistration getRegistration(OAuth2ClientProperties oAuth2ClientProperties, String client) {

        OAuth2ClientProperties.Registration registration = oAuth2ClientProperties.getRegistration().get(client);
        String clientId = registration.getClientId();
        String clientSecret = registration.getClientSecret();

        if (clientId == null) {
            return null;
        }

        switch (client){//구글, 페이스북은 제공, 네이버 카카오는 따로 Provider 선언해줘야함
            case "google":
                return customOAuth2Provider.GOOGLE.getBuilder(client)
                        .clientId(clientId).clientSecret(clientSecret).build();
            case "facebook":
                return CommonOAuth2Provider.FACEBOOK.getBuilder(client)
                        .clientId(clientId).clientSecret(clientSecret).build();
            case "kakao":
                return customOAuth2Provider.KAKAO.getBuilder(client)
                        .clientId(clientId)
                        .clientSecret(clientSecret).build();
            case "naver":
                return customOAuth2Provider.NAVER.getBuilder(client)
                        .clientId(clientId)
                        .clientSecret(clientSecret).build();
        }
        return null;
    }

 

 

6. 인증이 완료되면 security에서 설정해준 userService인 CustomOauth2UserService로 리턴되게 되어있다

   ( override받은 loadUser로 떨어짐)

@Service
public class CustomOauth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    @Autowired
    SecurityMapper securityMapper;

    @Autowired
    ServiceDocument serviceDocument;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        
        OAuth2UserService delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        Map<String, Object> userMap = new HashMap<>();
        String registrationId = userRequest.getClientRegistration().getRegistrationId();

        switch (registrationId){
            case "kakao" :
                Map<String, Object> profile = oAuth2User.getAttribute("kakao_account");
                Map<String, Object> temp = (Map<String, Object>) profile.get("profile");

                userMap.put("memberId", oAuth2User.getAttribute("id").toString());
                userMap.put("email", profile.get("email"));
                userMap.put("nickName", temp.get("nickname"));
                userMap.put("accessToken",userRequest.getAccessToken().getTokenValue());
        }

        userMap.put("loginType",registrationId);
        serviceDocument.oauth2UserInfo(userMap);

        return oAuth2User;

    }
}

 

인증 링크는 기본적으로 따로 설정하지 않으면 (http://localhost:8080/oauth2/authorization/{registrationId})를 탄다 CustomOAuth2Provider.class에 설정된 곳으로 인증을 태움

클라이언트 페이지에서 하단 예시와 같이 링크를 태우면 바로 로그인 창으로 떨어진다.

 

<a href="http://localhost:8080/oauth2/authorization/kakao">
   <input type="button"  value="카카오로그인" style="background-color: yellow;">
</a>

기본적인 redirect Uri =  localhost:8080/login/oauth2/code/{registrationId}

 


security에서 authentication 리턴 타입이 두 가지(OAuth2User type, UserDetails type)가 있다 

만약 두 가지를 다 사용해야 한다면 UserDetails와 OAuth2User는 상속받은 class들이 너무 달라 cast 하는데 문제가 있으니 사용하는 곳에서 따로 타입 체크 후 개발하는 것을 추천하다.

 

'Development > JAVA' 카테고리의 다른 글

[JAVA] Bearer token Authorization  (0) 2021.08.22
[JAVA] URL 한글 인코딩  (0) 2021.08.20
[JAVA] 페이징 pageable example (without jpa)  (0) 2021.08.20
[JAVA] ElasticSearch 키워드 검색  (0) 2021.08.11
[JAVA] XSS Filter Config  (0) 2021.08.03
댓글
링크
최근에 올라온 글
Total
Today
Yesterday