본문 바로가기
Spring

[OAuth2.0 + JWT + Spring] 스프링 시큐리티로 OAuth2.0 로그인 구현(카카오)

by 개미가되고싶은사람 2024. 7. 1.

목차

    이 글에서는 카카오 개발자센터 세팅부터 application.yml 세팅, SecurityConfig 구현, OAuth2ServiceImpl 구현, OAuth2SuccessHandler 구현, 그리고 CustomOAuth2User 구현까지의 과정만 다루겠습니다.  JWT 관련해서는 다음에 포스팅 해보겠습니다!!!

     

    혹시 OAuth2.0에 대해서 모르시는 분들은 아래 포스팅을 읽어주세요!!

    https://pjstudyblog.tistory.com/29

     

    OAuth 2.0 기본 개념과 동작원리

    목차 OAuth 2.0은 현대 웹 애플리케이션의 인증 및 권한 부여 메커니즘에서 중요한 역할을 수행하고 있다. 이 글에서는 OAuth 2.0의 기본 개념과 동작 원리를 다루며, OAuth2.0 등장 배경과 OAuth 2.0의 동

    pjstudyblog.tistory.com

     

     

     


    1. 카카오 개발자센터 세팅

     

     

       1-1. 카카오 개발자센터에 접속하여 새로운 애플리케이션을 생성합니다.

     

    https://developers.kakao.com

     

    Kakao Developers

    카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

    developers.kakao.com

     

     

     

       1-2. 애플리케이션을 어떤 플랫폼에서 사용할건지 선택하고 도메인 주소를 입력

       (필자는 웹 사이트에서 사용할 예정)

        

     

     

     


       1-3. 플랫폼하고 도메인 주소를 입력했으면 카카오 로그인 사용 시 Redirect URI를 설정합니다.

       

    활성화 상태를 ON으로 설정한 뒤 Redirect URI를 설정하면 되는데,

    Redirect URI를 통해 서비스에서 요청한 인가 코드와 토큰을 전달합니다.

    Redirect URI를 올바르게 등록하지 않은 경우, 카카오 로그인 시 KOE006 에러가 발생합니다.


     

     

       1-4. 앱 키에서 REST API 키를 복사해둡니다.

     

     

    Admin키 아래에 각 키의 목적에 대한 설명이 있습니다

     

     

    1-5. 보안에서 Client Secret 키를 생성합니다.

     

     

     

     

    2. application.yml 세팅

       - application.yml 파일에 카카오 API 키와 관련 설정을 추가합니다.

    spring:
      security:
        oauth2:
          client:
            registration:
              kakao:
                client-id: [카카오 디벨로퍼 REST API 키 값]
                client-secret: [카카오 디벨로퍼 client secret 키 값]
                redirect-uri: "{baseUrl}/oauth2/{registrationId}" // 카카오 로그인 페이지 URI
                authorization-grant-type: authorization_code
                client-authentication-method: client_secret_post
                scope: profile_nickname
                
                
            provider:
              kakao:
                authorization-uri: https://kauth.kakao.com/oauth/authorize
                token-uri: https://kauth.kakao.com/oauth/token
                user-info-uri: https://kapi.kakao.com/v2/user/me
                user-name-attribute: id

     

     

     

     

    3. SecurityConfig 구현

    @EnableWebSecurity
    @Configuration
    @Configurable
    @RequiredArgsConstructor
    public class SecurityConfig {
    
        private final JwtAuthenticationFilter jwtAuthenticationFilter;
        private final DefaultOAuth2UserService oAuth2UserService;
        private final OAuth2SuccessHandler oAuth2SuccessHandler;
    
    
        /**
         * HttpSecurity 설정
         */
        @Bean
        protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception {
    
            httpSecurity.cors(cors -> cors
                            .configurationSource(configurationSource())
                    )
                    .csrf(CsrfConfigurer::disable) // CSRF 보안 설정을 비 활성화
                    .httpBasic(HttpBasicConfigurer::disable) // 스프링 서큐리티 기본 인증 설정을 비 활성화(기본 인식 방식이 basic)
                    .sessionManagement(sessionManagement -> sessionManagement
                            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    )
                    // HTTP 요청에 대한 권한 설정
                    .authorizeHttpRequests(request -> request
                                    .requestMatchers("/oauth2/**")
                                    .anyRequest().authenticated() 
                    )
                    .oauth2Login(oauth2 -> oauth2
                            .authorizationEndpoint(endpoint -> endpoint.baseUri("/api/oauth2"))
                            .redirectionEndpoint(endpoint -> endpoint.baseUri("/oauth2/*"))
                            .userInfoEndpoint(endpoint -> endpoint.userService(oAuth2UserService))
                            .successHandler(oAuth2SuccessHandler)
                    )
                    .exceptionHandling(exceptionHandling -> exceptionHandling
                            .authenticationEntryPoint(new FailedAuthenticationEntryPoint())
                            .accessDeniedHandler(new CustomAccessDeniedHandler())
                    )
                    // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 이전에 추가
                    .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    
    
            return httpSecurity.build();
        }
    
        /**
         * CORS 구성 설정
         */
        @Bean
        protected CorsConfigurationSource configurationSource() {
    
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.addAllowedOrigin("*");
            corsConfiguration.addAllowedMethod("*");
            corsConfiguration.addAllowedHeader("*");
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", corsConfiguration); // 모든 경로에 대한 CORS 구성 추가
    
            return source;  
        }
    }
    
    /**
     * 인증 실패 시 처리할 클래스
     */
    @Slf4j
    class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response,
                             AuthenticationException authException) throws IOException, ServletException {
    
            String exception = (String) request.getAttribute("exception");
    
            response.setContentType("application/json; charset=UTF-8");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.getWriter().write("{\"code\": \"NP\", \"message\": \"No Permission\"}");
    
        }
    
    
    }

    OAuth2 로그인 설정 코드를 보면 OAuth2 인증 요청 엔드포인트를 /api/oauth2로, 리디렉션 엔드포인트를 /oauth2/*로 설정하며, 사용자 정보는 oAuth2UserService로 처리하고, 로그인 성공 시 oAuth2SuccessHandler를 호출하도록 설정합니다.

    현재 필자는 리다렉션 엔드포인트를 백엔드 서버로 보내고 있습니다. 그런데 백엔드 서버는 사용자가 브라우저로 직접 접속하기 위해 존재하는 것이 아닙니다. 결국 백엔드에서 사용자를 다시 프론트엔드 URI로 리디렉션 시켜야합니다.!!!

     

     

     

    4. OAuth2ServiceImpl 구현

    일반적으로 OAuth2ServiceImpl는 OAuth2.0 로그인 시 사용자의 정보를 가져와서 데이터베이스에 저장합니다.

    @Service
    @Slf4j
    @RequiredArgsConstructor
    public class OAuth2ServiceImpl extends DefaultOAuth2UserService {
    
        private final UserRepository userRepository;
    
        @Override
        @Transactional
        public OAuth2User loadUser(OAuth2UserRequest request) throws OAuth2AuthenticationException {
    
            OAuth2User oAuth2User = super.loadUser(request); // OAuth2 정보
            String oauthClientName = request.getClientRegistration().getClientName(); // OAuth2 서비스 (kakao, naver)
            try {
                log.info("oAuth2UserInfo={}", new ObjectMapper().writeValueAsString(oAuth2User.getAttributes()));
                log.info("oauthClientName={}", request.getClientRegistration().getClientName());
    
            } catch (Exception exception) {
                exception.printStackTrace();
            }
    
    
            if (oauthClientName.equals("kakao")) {
                Map<String, String> responseMap = (Map<String, String>) oAuth2User.getAttributes().get("properties");
                userId = "kakao_" + oAuth2User.getAttributes().get("id");
                userNickname = responseMap.get("nickname");
                userType = "kakao";
    
                userRepository.insertIfNotExists(userId, "ROLE_USER", userNickname, userType);
                
    
            }
    
    
            return new CustomOAuth2User(userId);
    
    
        }
    
    
    }

     

     

     

     

     

    5. OAuth2SuccessHandler 구현

    @Component
    @RequiredArgsConstructor
    @Slf4j
    public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    
        private final JwtProvider jwtProvider;
    
        @Override
        public void onAuthenticationSuccess(
                HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
            CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
    
            String userId = oAuth2User.getName();
            String token = jwtProvider.create(userId);
    
            response.sendRedirect("https://flowbowl.web.app/auth/oauth-response?token=" +
                    token + "&expirationTime=3600);
        }
    }

    OAuth2.0 로그인 성공 시 백엔드 서버로 리다렉트하기 때문에 

     

     

     

     

    6. CustomOAuth2User 구현

      - 사용자 정보를 자유롭게 커스텀할 수 있습니다.

    @NoArgsConstructor
    @AllArgsConstructor
    public class CustomOAuth2User implements OAuth2User {
    
        private String userId;
    
        @Override
        public Map<String, Object> getAttributes() {
            return null;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return null;
        }
    
        @Override
        public String getName() {
            return this.userId;
        }
    }