카카오톡 소셜 로그인 성공 후 오류

문의 시, 사용하시는 개발환경과 디벨로퍼스 앱ID를 알려주세요.
Java 17/ Spring boot 3.1.5
ID 1018014


안녕하세요! Spring security를 사용해서 카카오톡 소셜 로그인을 다루다가 오류 사항이 생겨… 혹시나 여기에 여쭤보려고 합니다.

우선 카카오톡 소셜 로그인은 성공하여 log에도 제 카카오톡 정보가 잘 뜨는 것을 확인하였습니다.

이후 successHandler에서 http://localhost:3000/main으로 이동하게 하였고, 이동도 완료한 모습입니다.

로그인이 완료되었다고 생각되었지만 이후 postman을 사용해서 어떤 API를 호출하든 response 부분에는 kakao 소셜 로그인 화면 html만 응답됩니다. ㅠㅠ

아래의 continueurl을 접속해보면
image

해당 화면이 뜨고 다시 kakao 버튼을 누르면 잘 로그인 되어 http://localhost:3000/main로 이동하게 됩니다…

혹시 Spring Security 문제라면 모르겠지만 oauth2 로그인이 잘 안된 것인지, 인증에 문제가 있는지 하여 여기에 질문드립니다. 감사합니다!

안녕하세요!

Spring에서 로그인 성공한것과 포스트맨에서 카카오 로그인 하는 것은 다른 것입니다.

  • 카카오 로그인은 (1) 인가코드요청 (카카오 계정 로그인페이지로 리다이렉트) (2) 리다이렉트 URI 로 302 리다이렉트 후 (3) 액세스 토큰 발급, 사용자 정보조회 과정을 거칩니다.

  • 카카오 로그인은 Oauth2 방식 소셜로그인으로 브라우저 기반으로 동작합니다. Spring을 통한 로그인은 해당 브라우저에서만 유효하고, 카카오 로그인 관련해서도 발급 받은 액세스토큰이 있어야 다른 곳에서 조회 가능합니다.

즉, 포스트맨에서 kauth.kakao.com을 이용한 인가요청은 할 수 없고 액세스 토큰을 이용한 API 호출만 가능합니다.

안녕하세요. 답변에 감사드립니다.

하지만 포스트맨에서 카카오 로그인을 진행한 것은 아닙니다…!

Spring Security를 사용하여 oauth2 로그인을 진행했고 http://localhost:3000/main 화면으로 리다이렉트까지 완료했습니다.

그 후에 제가 개발에서 사용하는 API 아무거나 사용해도 kakao login html이 뜹니다.

<!doctype html>
<html lang="ko">

<head>
    <meta charset="utf-8">
    <meta name="viewport"
        content="user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, width=device-width">
    <meta name="next-head-count" content="2">
    <meta http-equiv="x-ua-compatible" content="IE=edge">
    <meta content="website" property="og:type">
    <meta content="카카오계정" property="og:title">
    <meta content="https://accounts.kakaocdn.net/images/og_kakao.png" property="og:image">
    <script type="text/javascript" src="//t1.daumcdn.net/tiara/js/v1/tiara.min.js" defer></script>
    <link rel="preload" href="https://accounts.kakaocdn.net/_next/static/css/9771546e3178239c.css" as="style">
    <link rel="stylesheet" href="https://accounts.kakaocdn.net/_next/static/css/9771546e3178239c.css" data-n-g="">
    <link rel="preload" href="https://accounts.kakaocdn.net/_next/static/css/aa36f5129b871672.css" as="style">
    <link rel="stylesheet" href="https://accounts.kakaocdn.net/_next/static/css/aa36f5129b871672.css" data-n-p="">
    <link rel="preload" href="https://accounts.kakaocdn.net/_next/static/css/ace5cd78e5997324.css" as="style">
    <link rel="stylesheet" href="https://accounts.kakaocdn.net/_next/static/css/ace5cd78e5997324.css" data-n-p="">
    <noscript data-n-css=""></noscript>
    <script defer nomodule="" src="https://accounts.kakaocdn.net/_next/static/chunks/polyfills-5cd94c89d3acac5f.js">
    </script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/webpack-775174cedae781f8.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/framework-f8115f7fae64930e.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/main-a7d45bce11193232.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/pages/_app-b23efa0eb85a011c.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/29107295-f5d3d9a71e7e292a.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/70368-58b0a348debb6a04.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/69695-9ee32dd2295c9b6f.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/74534-ef40f6ad4c13b9c2.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/70224-19e687475e621ef1.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/3889-727e358a678e140d.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/chunks/pages/login/login-adfc2432b308a5cc.js" defer>
    </script>
    <script src="https://accounts.kakaocdn.net/_next/static/MtCeP08OTF9ONzZYVHnoR/_buildManifest.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/MtCeP08OTF9ONzZYVHnoR/_ssgManifest.js" defer></script>
    <script src="https://accounts.kakaocdn.net/_next/static/MtCeP08OTF9ONzZYVHnoR/_middlewareManifest.js" defer>
    </script>
</head>

<body class="os_other pc type_responsive">
    <div id="__next" data-reactroot=""></div>
    <script id="__NEXT_DATA__" type="application/json">
        {"props":{"pageProps":{"pageContext":{"commonContext":{"locale":"ko","uaClass":"os_other  pc","responsiveView":true,"responsivePopup":false,"mobile":false,"webview":{"app":"web","webViewType":"none","appVersion":"","os":"other","osVersion":"","supportNavigation":false,"supportFilePicker":true,"supportExecUrlScheme":false,"supportMarketUrlScheme":true},"supportRefererMetaTag":false,"showHeader":false,"showFooter":true,"linkParams":{},"showDarkMode":null,"_csrf":"3a4ee3d2-81e7-48e0-8948-e0e8c9cb0bc1","kage_file_max_size":100,"upload_kage_url":"https://up-api1-kage.kakao.com/up/kaccount-p/","p":"0mauVEPOFjLBn0DsRnWy9v8AtHz-RLgdpaAmpj4l3_8"},"context":{"webType":"web","defaultEmail":null,"showStaySignIn":true,"defaultStaySignIn":false,"appendStaySignedIn":false,"defaultCountryCode":"KR_82","showQrLogin":true,"showWebTalkLogin":false,"showDeviceFormLogin":false,"needCaptcha":false,"showIpSecurity":false,"loginUrl":"/login?continue=https%3A%2F%2Fkauth.kakao.com%2Foauth%2Fauthorize%3Fscope%3Dprofile_nickname%2520profile_image%2520account_email%26response_type%3Dcode%26state%3DxqnTVTa1fwwGM2EYVYrlKa1Tw-4grav3xJN6Ie1AmCE%253D%26redirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8080%252Flogin%252Foauth2%252Fcode%252Fkakao%26through_account%3Dtrue%26client_id%3D1ca922b87213dcdd6ecc7bc89a84113f","continueUrl":"https://kauth.kakao.com/oauth/authorize?scope=profile_nickname%20profile_image%20account_email&response_type=code&state=xqnTVTa1fwwGM2EYVYrlKa1Tw-4grav3xJN6Ie1AmCE%3D&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Flogin%2Foauth2%2Fcode%2Fkakao&through_account=true&client_id=1ca922b87213dcdd6ecc7bc89a84113f","useSimpleLogin":true,"exceedSimpleLoginLimit":false,"defaultSaveSignIn":false,"isTalkLoginError":false,"linkParams":{"lang":["ko"]},"requests":{"check_daum_sso":["get","https://logins.daum.net/accounts/endpoint/favicon.ico"]}}}}},"page":"/login/login","query":{},"buildId":"MtCeP08OTF9ONzZYVHnoR","assetPrefix":"https://accounts.kakaocdn.net","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}
    </script>
</body>

</html>

또한, http://localhost:3000/main 에서도 cors 오류가 뜨면서 accounts.kakao.com에서의 fetch가 실패했다고 뜹니다…

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*") 
                .allowedMethods("*");
    }
}

CORS 관리는 모두 허용해놓았는데 혹시 여기에서 더 추가해야 하는 부분이 있을까요?

정리하자면

  1. 로그인 이후 어떤 API 사용에서도 kakao login html이 뜬다.
  2. 로그인 이후 리다이렉트된 http://localhost:3000/main에서 호출하는 API에서 account.kakao.com에서 fetch가 실패했다며 CORS 오류가 나온다.
  3. 로그인 이후 frontend에서 로그인 되었다는 것을 판단하는 부분이 있는지, 아니면 따로 JWT같은 추가적인 보안을 구현하여 로그인을 판단하는 것인지

너무 많은 것을 여쭤보아서 죄송합니다… 정말 구글을 열심히 보아도 해결책이 나오지 않아서 혹시나 아시는 부분이 있는지 궁금하여 한번 여쭤봅니다…!

안녕하세요.

구현 내용이 잘 이해되지 않아 문의 드립니다.

카카오 로그인은 어디서 하셨나요? (postman or 웹브라우저)

카카오 로그인은 웹브라우저에서 버튼을 누르면 연결되게 하였습니다.

<button>
          <a href="http://localhost:8080/oauth2/authorization/kakao">
          <img
              src="img/kakao_login_medium_narrow.png"
              width="222"
              alt="카카오 로그인 버튼"
          />
          </a>
</button>

image

이 버튼으로 접근하고, 해당 카카오 로그인 화면이 떠서 로그인을 진행합니다.

oauth2로그인이 완료되면 successhandler에서 http://localhost:3000/main로 접근하게 하였고, 접근하면 바로 post형식으로 API가 호출되게 제작하였습니다.
그래서 API를 호출하는 과정에서 CORS 오류를 접하였고, account.kakao.com에서 fetch하는 과정에서 오류가 난다고 되어있습니다.

여기서 어떤 API를 사용하시나요?
서비스측 API 라면, URL과 URL 내부에서 사용하는 카카오측 API 모두 기재 부탁드립니다.

URL 내부에서 사용하는 카카오측 API는 없고 그저 DB에 등록한 일정들을 가져오는 API 입니다…

  static async getCalender(
    id: number,
    startDate: string,
    endDate: string
  ): Promise<CalenderData[]> {
    try {
      const res = await fetch(`http://localhost:8080/timetable/period`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',

        },
        body: JSON.stringify({
          user_id: id,
          startDate,
          endDate,
        }),
      });
      if (res.status === 200) {
        const data: CalenderData[] = await res.json();
        return data;
      }
      return Promise.reject(res.status);
    } catch (error) {
      return Promise.reject(500);
    }
  }

카카오 로그인 후, main 으로 리디렉션 전에 서비스측 인가처리(세션 또는 jwt 등 - 카카오 말고 서비스측 자체 인가처리)을 하고 리디렉션 하신게 맞으신가요?
정황상 서비스측 인가처리하지 않은상태로 API 요청 시, 로그인 페이지로 리디렉션 하는 것으로 보입니다.

spring security에서 인가처리를 진행해 주는 것으로 아는데, oauth2를 진행하면 그 정보로 한 번 더 인가처리를 해야할까요?

그렇다면 카카오 로그인 후, 카카오 회원정보를 통해서 인가처리를 진행해야 하는 것 인가요?

문의 글중 8080과 3000 포트 두개 보이는데요
인가처리와 호출하신 API 모두 동일 포트 사용하는 서버에서 하신게 맞으신가요?

현재는 카카오 로그인와 무관하게 서비스측 인가가 되지 않아 계속 로그인 페이지로 리디렉션하는 것으로 보입니다.

현재 react와 연동하여서 사용 중이라서 8080과 3000을 이용하고 있습니다.

3000 포트에서 http://localhost:8080/oauth2/authorization/kakao를 진행하고, 로그인이 완료되면 다시 3000포트로 돌아가게끔 하였는데,

그러면 3000 에서 사용할 수 있게끔 인가처리를 또 진행해야 하는 것일까요?

프론트에서 http://localhost:8080/timetable/period 호출 시, 카카오 로그인 페이지로 리디렉션 된것인지 확인 부탁드립니다.
해당 URL에서 최종적으로 accounts.kakao.com으로 리디렉션 된것이라면, 8080 서버에서 인증확인을 못하고 있는 것으로 해당 서버의 구현 내용을 점검 해보셔야만 합니다.

spring을 통해 oauth2 로그인이 완료 되면 sendredirect를 통해 http://localhost:3000/main으로 리다이렉트 되었습니다.

@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {

        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

        log.info("Principal에서 꺼낸 OAuth2User = {}", oAuth2User);

        String targetUrl;

        targetUrl = UriComponentsBuilder.fromUriString("http://localhost:3000/main")
                .build().toUriString();

        getRedirectStrategy().sendRedirect(request, response, targetUrl);
//localhost:3000/main으로 리다이렉트 되는 부분
    }
}

로그인이 완료되었다는 것은 인증도 모두 진행이 된 것 아닌가요?


Network 상에서는 localhost:8080/timetable/period 호출 시
다시 kakao로 넘어가고 그렇게 authorize를 한번 더 호출하여 accounts.kakao.com으로 리디렉션되는 것 같습니다.

네, 카카오 로그인 이후 서비스측 인가 구현 부분에 문제가 있으시네요
oauth userService 구현부 코드 확인 부탁드립니다.

package com.capstone.AreyouP.oAuth2;

import com.capstone.AreyouP.oAuth2.OAuth2Attribute;
import com.capstone.AreyouP.Domain.User;
import com.capstone.AreyouP.Repository.UserRepository;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;

import java.util.Collections;
import java.util.Map;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class OAuth2UserService extends DefaultOAuth2UserService{
    //정상적인 유저 인증이 완료되면 여기로 오게 된다.
    //그 다음에 successhandler로 감

    private final UserRepository userRepository;
    private final HttpSession httpSession;

    //OAuth2User에는 개인정보 요청이 들어있음
    //아래 메소드를 바탕으로 인증 처리
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
        OAuth2User oAuth2User = super.loadUser(userRequest);
        //생성된 Service 객체로 부터 User를 받는다.

        //받은 User로 부터 user 정보를 받는다.
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        log.info(oAuth2User.toString());

        String id = oAuth2User.getAttributes().get("id").toString();

        processOAuth2User(oAuth2User);//회원가입

        //SuccessHandler가 사용할 수 있도록 등록해준다.
        OAuth2Attribute oAuth2Attribute = OAuth2Attribute.of(registrationId, id, oAuth2User.getAttributes());

        var memberAttribute = oAuth2Attribute.convertToMap();

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
                memberAttribute, "id");
    }

    private void processOAuth2User(OAuth2User oAuth2User) throws JSONException {
        Map<String, Object> attributes = oAuth2User.getAttributes();
        Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
        Long id = (Long) attributes.get("id");
        String name = (String) properties.get("nickname");

        Optional<User> userOptional = userRepository.findByUserId(String.valueOf(id));
        if (userOptional.isEmpty()){
            User user = User.builder()
                    .userId(String.valueOf(id))
                    .name(name)
                    .build();

            userRepository.save(user);
        }
    }
}

Service 부분 입니다. 여기에서 인가 구현을 따로 진행해야하나요?

아닙니다. 코드상 다른 문제는 없어보이는데요
다른 설정에 원인이 있을것 같은데요 가능하시면 전체 소스 공유 가능하실까요?

개인 메시지로 git 주소 주셔도 괜찮습니다.

git 주소 드리기 위해서 git에 push를 모두 진행했더니 갑자기 또 오류가 사라졌습니다 …

git에 push 하는 것도 oauth2 로그인에 관련이 있는 것인가요…?

안녕하세요.
git에 push 하는 것은 oauth2로그인과 무관합니다.
최신버전이 반영 안된 배포문제나 빌드 문제였을 것 같네요.