Oauth로 카카오 로그아웃 구현 에러

문의 시, 사용하시는 개발환경과 디벨로퍼스 앱ID를 알려주세요.


인텔리제이에서 oauth와 jwt를 사용해서 카카오 로그인을 구현하는데 로그아웃을 할 때 카카오 계정과 함께 로그아웃 페이지가 뜨지 않고 바로 로그아웃이 됩니다. 해결하고 싶은데 이유가 뭔지를 모르겠습니다.

이전 오류에서는 KOE207가 발생했지만 파라미터를 수정하지 않고 다른 부분을 수정하니 KOE207오류가 발생하지 않고 토큰이 사라지며 로그아웃만 됩니다.

코드들

  • CustomLogoutHanlder.java
package com.example.app.config.auth.logoutHandler;

import com.example.app.config.auth.PrincipalDetails;
import com.example.app.config.auth.jwt.JwtProperties;
import com.example.app.config.auth.jwt.JwtTokenProvider;
import groovyjarjarpicocli.CommandLine;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

import javax.swing.*;
import java.util.Arrays;

@Component
public class CustomLogoutHandler implements LogoutHandler {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;


    //client-id 추가
    @Value("${kakao.rest-api}")
    private String KAKAO_CLIENT_ID;


    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) {

        //Authentication 객체에 접근
         Authentication authentication = null;
        String token = Arrays.stream(request.getCookies())
                .filter(cookie->cookie.getName().equals(JwtProperties.COOKIE_NAME)).findFirst()
                .map(cookie -> cookie.getValue())
                .orElse(null);

        if(token!=null) {
            authentication = jwtTokenProvider.getAuthentication(token);
        }

        System.out.println("CustomLogoutHandler's logout()..");
        System.out.println("Authentication.......... : " + authentication);
        if (authentication != null && authentication.getPrincipal() instanceof PrincipalDetails) {
            HttpSession session = request.getSession(false);
            if (session != null)
                session.invalidate();
        }

        //JWT 토큰 삭제
        response.addCookie(createExpiredCookie());


        //kakao Logout
        assert authentication != null;
        PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
        System.out.println("principalDetails!!!!!!!!!!"+principalDetails);
        String accessToken = principalDetails.getAccessToken();
        String snsType = principalDetails.getUserDto().getSnsType();

        System.out.println("SNSTYPE : "+snsType + ", ACCESSTOKEN : " + accessToken);

        String url = "https://kapi.kakao.com/v1/user/logout";
//        String url = "https://kauth.kakao.com/oauth/logout";

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer "+accessToken);

        MultiValueMap<String,String> params = new LinkedMultiValueMap<>();

        HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity(params,headers);

        RestTemplate rt = new RestTemplate();
        try {
            ResponseEntity<String> resp = rt.exchange(url, HttpMethod.POST, entity, String.class);

            if(resp.getStatusCode() == HttpStatus.OK){
                System.out.println("Successfully logout from kakao");
            }else{
                System.out.println("Failed to logout from kakao. Status code : "+resp.getStatusCode());
            }
        }catch (HttpClientErrorException ex){
            System.out.println("Failed to logout from kakao. Error : "+ex.getMessage());
        }catch (Exception ex){
            System.out.println("An error occurred while logout from kakao. Error : "+ ex.getMessage());
        }


    }
    private Cookie createExpiredCookie(){
        Cookie cookie = new Cookie(JwtProperties.COOKIE_NAME, null);
        cookie.setMaxAge(0);
        cookie.setPath("/");
        return cookie;
    }
}
  • CustomLogoutSuccessHandler.java
package com.example.app.config.auth.logoutHandler;

import com.example.app.config.auth.PrincipalDetails;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URLEncoder;

@Component
//@PropertySource("classpath:application-SECRET-KEY.properties")
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Value("${kakao.rest-api}")
    private String KAKAO_CLIENT_ID;
    @Value("${spring.security.oauth2.client.kakao.logout.redirect.uri}")
//    private String KAKAO_LOGOUT_REDIRECT_URI="http://localhost:8080/th/member/login";
    private String KAKAO_LOGOUT_REDIRECT_URI;

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

        if(authentication != null) {
            PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
            String snsType = principalDetails.getUserDto().getSnsType();

            if (snsType != null && "kakao".equals(snsType)) {
                response.sendRedirect("https://kauth.kakao.com/oauth/logout?client_id=" + KAKAO_CLIENT_ID + "&logout_redirect_uri=" + KAKAO_LOGOUT_REDIRECT_URI);
                return;
            }
        }
        System.out.println("CustomLogoutSuccessHandler's onLogoutSuccess()");

        response.sendRedirect("/");


    }
}

이게 현재 로그아웃 관련 코드들입니다.

구현 하신 코드는 로그아웃 API를 호출하신 것으로 이 경우 사용자 토큰이 만료되며 정상적으로 REST API를 사용하셨습니다.

카카오 계정과 함께 로그아웃은 API로 호출 하는 것이 아닌 사용자 브라우저에서 해당 URL로 이동 시켜야만 합니다.
서비스는 사용자 의사와 무관하게 계정 로그아웃을 강제할 수 없습니다.

package com.example.app.config.auth.logoutHandler;

import com.example.app.config.auth.PrincipalDetails;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.net.URLEncoder;

@Component
//@PropertySource("classpath:application-SECRET-KEY.properties")
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {

    @Value("${kakao.rest-api}")
    private String KAKAO_CLIENT_ID;
    @Value("${spring.security.oauth2.client.kakao.logout.redirect.uri}")
//    private String KAKAO_LOGOUT_REDIRECT_URI="http://localhost:8080/th/member/login";
    private String KAKAO_LOGOUT_REDIRECT_URI;

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

        if(authentication != null) {
            PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
            String snsType = principalDetails.getUserDto().getSnsType();

            if (snsType != null && "kakao".equals(snsType)) {
                String kakaoLogoutWith = "https://kauth.kakao.com/oauth/logout?client_id=" + KAKAO_CLIENT_ID + "&logout_redirect_uri=" + URLEncoder.encode(KAKAO_LOGOUT_REDIRECT_URI,"UTF-8");
                response.sendRedirect(kakaoLogoutWith);
//                response.sendRedirect("https://kauth.kakao.com/oauth/logout?client_id=" + KAKAO_CLIENT_ID + "&logout_redirect_uri=" + KAKAO_LOGOUT_REDIRECT_URI);
                return;
            }
        }
        System.out.println("CustomLogoutSuccessHandler's onLogoutSuccess()");

        response.sendRedirect("/");


    }
}

이 코드가 CustomLogoutSuccessHandler 코드인데 여기서 onLogoutSuccess 안에 response.redirect() 로 카카오 계정과 함께 로그아웃하기 url을 지정했습니다. 그런데 그 페이지로 리다이렉트가 안되고 바로 메인페이지로 넘어갑니다.
심지어 카카오개발자센터에서 logout 시 리다이렉트 경로는 로그인화면으로 보내도록 설정했는데 막상 로그아웃하면 로그아웃에 성공했다는 문장이 콘솔창에 뜨면서 메인화면이 뜹니다…

안녕하세요.

확인을 위해 앱 ID 부탁드립니다.


앱ID
https://developers.kakao.com/ 의 내 애플리케이션>앱 설정>요약 정보 : 기본정보에 있는 앱 ID
숫자로된 ID 입니다

ex) 123456

ID 1081825 입니다

최근 1주간 앱 1081825에서 /oauth/logout 호출 이력이 확인되지 않습니다.
/oauth/logout 사용 시, 브라우저에 표시되는 URL 첨부 부탁드립니다.

localhost:8080/logout 으로 로그아웃하면 로그아웃되면서 개발자도구에서 jwt 토큰이 사라지고 브라우저에 주소창에는 localhost:8080 만 뜨게 됩니다.

/oauth/logout으로 리디렉션 하셨다면 브라우저의 개발자 도구의 네트워크 탭에서 이력 확인 가능하신데요
/oauth/logout 호출할 때 요청 URL 확인 부탁드립니다.

http://localhost:8080/oauth/logout 이렇게 하면 404 Not Found가 뜹니다.

해당 URL은 카카오계정과 함꼐 로그아웃 URL이 아닙니다.

구현하신 내용을 보아 서비스측 로그아웃 URL 호출 시, CustomLogoutSuccessHandler 에서 kakaoLogoutWith 으로 리디렉션하지 못하는 것으로 보입니다.

일반적으로 authentication 값은 SecurityContextLogoutHandler에 의해 전처리 되어 항상 null로 전달됩니다.
때문에, 우선 if 구문을 제거하고 테스트하여 확인 해 보시는게 좋을것 같습니다.