카카오 로그인 REST API 개발 시 발생하는 에러 원인을 모르겠습니다

안녕하세요.
곧 상용 예정 중인 사이트의 카카오 로그인을 연동하기 위해 개발 중 입니다.

콘솔창에 하나씩 log찍어서 확인 결과 키값과 인가코드, 인가코드 요청시 redirect_uri와 동일한 redirect-uri를 사용중인것 같은데 Bad Request 400 에러가 나는데 원인을 모르겠습니다 ㅠㅠ

1.카카오 로그인 버튼 클릭

2.카카오 인가코드 발급

  1. 발급받은 인가코드로 access_token 발급받기

3번 과정중에서
java.io.IOException: Server returned HTTP response code: 400 for URL: https://kauth.kakao.com/oauth/token
400에러가 뜹니다

@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String client_id;

@Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
private String client_secret;

@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String redirect_uri;

//카카오 access token가져오기
	public String getAccessToken (String authorize_code) {
        String access_Token = "";
        String refresh_Token = "";
        String reqURL = "https://kauth.kakao.com/oauth/token";
        
        log.info("authorize_code :: {} " + authorize_code);
        log.info("redirect_url :: {} " + "&redirect_uri="+redirect_uri+"");
        
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            //    POST 요청을 위해 기본값이 false인 setDoOutput을 true로
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);

            //    POST 요청에 필요로 요구하는 파라미터 스트림을 통해 전송
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
            StringBuilder sb = new StringBuilder();
            sb.append("grant_type=authorization_code");
            sb.append("&client_id="+client_id+"");
            sb.append("&redirect_uri=http://localhost:14902/login/oauth2/code/kakao");
            sb.append("&code=" + authorize_code);
            sb.append("&client_secret="+client_secret+"");
            bw.write(sb.toString());
            bw.flush();
            
                 log.info("sb :: {} " + sb.toString());
            log.info("conn.getOutputStream() :: {} " + conn.getOutputStream());

            //    결과 코드가 200이라면 성공
            int responseCode = conn.getResponseCode();
            String responseMessage = conn.getResponseMessage();
            System.out.println("responseCode : " + responseCode);
            System.out.println("responseMessage : " + responseMessage);

responseCode : 400
responseMessage : Bad Request

application.properties에서 키값을 value로 가져와 사용합니다
log에는 authorize_code 정상적으로 출력됩니다

에러메시지 :
java.io.IOException: Server returned HTTP response code: 400 for URL: https://kauth.kakao.com/oauth/token
at java.base/jdk.internal.reflect.GeneratedConstructorAccessor336.newInstance(Unknown Source)
at

앱ID : 998076 입니다

콘솔창에 log확인해보니 client-secret키와 내 어플리케이션에서 발급받은 키와 동일합니다.

안녕하세요.

response body 값 확인 부탁드립니다.
카카오측 로그에는 KOE320 오류가 발생하고 있으며, 이 오류는 유효하지 않은 인가코드를 사용 시, 발생하는 오류 입니다.
인가코드는 단한번만 사용 가능합니다. 두번째 사용 부터는 유효하지 않은것으로 처리되오니 참고 부탁드립니다.

location.href = “${ctx }/oauth2/authorization/kakao”;
이 경로로 접속후
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
request.getParameter(“code”)
이렇게 인가코드 가져오고 있는데 이게 유효하지 않은 인가코드인가요

response body는
// 요청을 통해 얻은 JSON타입의 Response 메세지 읽어오기
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); --오류 발생
String line = “”;
String result = “”;

            while ((line = br.readLine()) != null) {
                result += line;
            }
            System.out.println("response body : " + result);

response body 출력 전 오류가 발생해서 출력되지 않고 있습니다…

서비스에서 접근토큰 발급 시, 동일 인가코드를 2번 사용하고 있습니다.
0.2초 내로 호출되는 케이스가 많은데요. 서비스측 로직 확인 부탁드립니다.

오류 내용은 getErrorStream 에서 확인 부탁드립니다.

아 동일한 인가코드 2번 사용하는곳 찾았습니다
감사합니다!

동일한 인가코드 사용하는곳 찾아서 1곳만 사용중으로 수정했는데
아직
java.io.IOException: Server returned HTTP response code: 400 for URL: https://kauth.kakao.com/oauth/token
같은 에러가 발생합니다

혹시 카카오측 로그에서 이전과 같은 오류가 발생하나요?

네, 동일한 오류 발생하고 있습니다.

동일한 인가코드 사용하는거라면
https://kauth.kakao.com/oauth/token 를 2번 호출하는곳이 있다는건가요

네, 0.2초 간격으로 바로 호출되는 것으로 보아 react hook 같은 곳에서 인가코드를 백앤드로 전달하는게 아니신지 점검 부탁드립니다.

현재 spring boot 로 개발중인데 react는 사용하지 않습니다

image
https://kauth.kakao.com/oauth/token 를 호출하는곳은 한곳밖에 없고 나머진 주석처리 되어있는데
현재 token을 가져오는 로직에서만 사용중입니다.

@Service
@Slf4j
public class KakaoService{

@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String client_id;

@Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
private String client_secret;

@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String redirect_uri;

@Value("${spring.security.oauth2.client.provider.kakao.token-uri}")
private String token_uri;

//카카오 access token가져오기
	public String getAccessToken (String authorize_code) {
        String access_Token = "";
        String refresh_Token = "";
        String reqURL = token_uri;
        
        log.info("reqURL :: {} " + reqURL);
        
        log.info("authorize_code :: {} " + authorize_code);
        log.info("redirect_url :: {} " + "&redirect_uri="+redirect_uri+"");
        log.info("redirect_url :: {} " + "&code="+authorize_code+"");
        log.info("redirect_url :: {} " + "&code="+authorize_code);
        
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            //    POST 요청을 위해 기본값이 false인 setDoOutput을 true로
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);
            conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");

            //    POST 요청에 필요로 요구하는 파라미터 스트림을 통해 전송
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
            StringBuilder sb = new StringBuilder();
            sb.append("grant_type=authorization_code");
            sb.append("&client_id="+client_id+"");
            sb.append("&redirect_uri=http://localhost:14902/login/oauth2/code/kakao");
            sb.append("&code="+authorize_code+"");
            sb.append("&client_secret="+client_secret+"");
            bw.write(sb.toString());
            bw.flush();
            
            log.info("sb :: {} " + sb.toString());
            log.info("conn.getOutputStream() :: {} " + conn.getOutputStream());
            log.info("conn.getContentType() :: {} " + conn.getContentType());
            log.info("에러 본문 : {}  " + conn.getErrorStream().toString());

            //    결과 코드가 200이라면 성공
            int responseCode = conn.getResponseCode();
            String responseMessage = conn.getResponseMessage();
            System.out.println("responseCode : " + responseCode);
            System.out.println("responseMessage : " + responseMessage);
            
            log.info("conn.getInputStream() :: {} " + conn.getContentEncoding());

            //    요청을 통해 얻은 JSON타입의 Response 메세지 읽어오기
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = "";
            String result = "";

            while ((line = br.readLine()) != null) {
                result += line;
            }
            System.out.println("response body : " + result);

            //    Gson 라이브러리에 포함된 클래스로 JSON파싱 객체 생성
            JsonParser parser = new JsonParser();
            JsonElement element = parser.parse(result);

            access_Token = element.getAsJsonObject().get("access_token").getAsString();
            refresh_Token = element.getAsJsonObject().get("refresh_token").getAsString();

            System.out.println("access_token : " + access_Token);
            System.out.println("refresh_token : " + refresh_Token);

            br.close();
            bw.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        return access_Token;
    }

카카오톡으로 로그인
function openKakao(){
setCookie();
location.href = “${ctx }/oauth2/authorization/kakao”;
}

//카카오 로그인
private OAuth2User kakao(OAuth2UserRequest userRequest) {
OAuth2User oAuth2User = super.loadUser(userRequest);

	ModelAndView mv = new ModelAndView();
	
	log.info("======================카카오 로그인 =====================");
	
	
	log.info("************** 사용자 정보 *************");
	log.info("Name : {} ", oAuth2User.getName());
	log.info("Attributes : {} " , oAuth2User.getAttributes());
	log.info("auth :: {} " , oAuth2User.getAuthorities());
	
	log.info("카카오 로그인, oAuth2User ::::::::: {} " + oAuth2User);
	
	
	Map<String, Object> map = oAuth2User.getAttributes();
	
	log.info("카카오 맵 :: {} " + map);
	
	//key들 꺼내기
	Iterator<String> keys = map.keySet().iterator();
	
	while(keys.hasNext()) {	//열거한 후 다음게 있냐고 물어봄
		String key = keys.next();
		
		log.info("카카오 KEY ==>> {} " , key);
		
	}
	
	
	log.info("ClassName : {} " , oAuth2User.getAttribute("properties").getClass());
	
	Map<String, String> lm = oAuth2User.getAttribute("properties");
	Map<String, Object> ks = oAuth2User.getAttribute("kakao_account");

	log.info("======== (USERSOCIALSERVICE) lm :::: {} " + lm);
	log.info("======== (USERSOCIALSERVICE) ks :::: {} " + ks);
	log.info("======== (USERSOCIALSERVICE) oAuth2User.getAttributes() ::: {}  " + oAuth2User.getAttributes());
	
	String social  = userRequest.getClientRegistration().getRegistrationId();
	log.info("social ::: {} "  + social);
	
	UserVO userVO = new UserVO();
	
	String userId = userMapper.getUserId(oAuth2User.getName());
	
	String kakaoPhone = (String)ks.get("phone_number");
	
	//특수문자 및 공백 제거 ex) +82 10-1234-5678
	String newKakaoPhone = kakaoPhone.replaceAll("[^0-9]", "");
	
	//앞에 82를 0으로 치환
	String realKakaoPhone = newKakaoPhone.replaceFirst("^82", "0");
	
	userVO.setUserId(userId);
	
	userVO.setKakaoId(oAuth2User.getName());
	
	//이미 아이디가 있는지 확인
	
	
	log.info("count if문 ");
	userVO.setKakaoEmail(ks.get("email").toString());
	userVO.setKakaoHp(realKakaoPhone);
	userVO.setKakaoNm(ks.get("name").toString());
	userVO.setUserAthr("01");
	userVO.setAttributes(oAuth2User.getAttributes());
	userVO.setSocial(social);
	log.info("======== (USERSOCIALSERVICE) set후 oAuth2User.getAttributes() ::: {}  " + oAuth2User.getAttributes());
	
	
	Map<String, Object> check = new HashMap<String, Object>();
	check.put("hp", userVO.getKakaoHp());
	
	log.info("check :: {} " + check);
	
	int checkBlockYn = userMapper.checkBlockYnBySocial(check);
	log.info("checkBlockYn :: {} " + checkBlockYn);
	
	if(checkBlockYn > 0) {
		throw new OAuth2AuthenticationException(new OAuth2Error("user_blocked", "User is blocked", "로그인이 차단되었습니다."));

	}
	
	log.info("카카오 전화번호 ::::::: {} " + userVO.getKakaoHp());
	//기존 우리 사이트 회원인지 
	int checkUser = userMapper.checkAlreadyUserByKakao(userVO);
			
	//기존 카카오 회원인지
	int count = userMapper.checkExistingUserByKakao(userVO);
	
	log.info("이미 회원임 ???? count ::: {} " + count);
	
	
	if(checkUser == 1) {
		log.info("=============(UserSocialService) 기존에 우리 사이트 가입한 적 있음 (일반 OR 네이버) ================");
			
		if(count == 1) {
			log.info("=============(UserSocialService) 기존에 카카오로 가입한 적 있음  ================");
			userMapper.updateKakaoInfo(userVO);
			
		}else {
			log.info("================= (UserSocialService) 기존에 카카오로 가입한 적 없음 ===================");
			userMapper.updateKakaoInfo(userVO);
		}
			
	}else {
		log.info("===============(UserSocialService) 우리 사이트 가입한 적  없음 (일반 OR 네이버) ===============");
		userMapper.signUpKakao(userVO);
	}
	
	log.info("========== UserSocialService userVO ::: {} " + userVO);
	userVO = userMapper.getKakaoLogin(userVO.getKakaoId());
	
	return userVO;
}

@Component
@Slf4j
public class SocialLoginSuccess extends SimpleUrlAuthenticationSuccessHandler{

@Autowired
private UserMapper userMapper;

@Autowired
private KakaoService kakaoService;

@Autowired
private NaverService naverService;


//소셜로그인 성공시
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
	
	log.info("============= 소셜 로그인 성공 ==========");
	log.info("ididididi :: {} " , authentication.getName());
	log.info("소셜 현재 정보 :: {} " , authentication.getPrincipal());
	log.info("소셜로그인 세션 ::: {} " , request.getSession());
	log.info("소셜로그인 세션 ::: {} " , request.getRequestedSessionId());
	log.info("소셜로그인 :::: request.authenticate(response) {} " + request.authenticate(response));

// log.info("소셜 로그인 Success핸들러 진짜 AccessToken :::: {} " + request.getParameter(“code”));

	String authCode = request.getParameter("code");
	
	String naverState = request.getParameter("state");
	
	request.getSession().setAttribute("userId", authentication.getName());

// userMapper.insertKakaoAccessToken(map);

	UserVO userVO = new UserVO();
	
	userVO.setUserId(authentication.getName());
	userVO = userMapper.getMyPage(userVO);
	log.info("소셜로그인 성공시 userVO :: {} " + userVO.getSocial());
	
	
	if(userVO.getSocial().equals("kakao")) {
		log.info("발급받은 카카오 인가코드 ::: {} " + request.getParameter("code"));
		String kakako_accessToken = kakaoService.getAccessToken(authCode);
		log.info("카카오 로그인 성공시 accessToken :: {} " + kakako_accessToken);
	}else if(userVO.getSocial().equals("naver")) {
		log.info("발급받은 네이버 인가코드 ::: {} " + request.getParameter("code"));
		String naver_accessToken;
		try {
			log.info("네이버 트라이");
			naver_accessToken = naverService.getAccessToken(authCode, naverState);
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

// log.info("네이버 로그인 성공시 accessToken :: {} " + naver_accessToken);
}

	request.getSession().setAttribute("user", userVO);

// request.getSession().setAttribute(“kakaoAT”, request.getParameter(“code”));

	response.sendRedirect("/hp/user/main");
}

잘못된 부분이 있을까요

Spring Security OAuth 사용하시면 해당 모듈에서 접근토큰 발급 및 사용자 정보조회까지 자동으로 진행 됩니다.
첨부해 주신 코드처럼 직접 호출하시고자 하는경우 accessTokenResponseClient를 재정의해야 하는데 그러한 코드는 보이지 않는걸로 보아 Spring에서 기본동작하는 코드 후, 구현하신 코드가 이중으로 도는듯 합니다.

감사합니다
근데 userRequest.getAccessToken().getTokenValue() 이걸로 접근토큰은 가져올수있지만
refreshToken도 가져올수있나요?

조금 아쉬운 부분이기도 하지만 불가합니다.
Spring Security OAuth2 client의 기본제공 스팩이외의 값을 획득하시려면 accessTokenResponseClient를 재정의 해야만 합니다.

accessTokenResponseClient 이걸 재정의 하면 아까처럼 인가코드 중복사용 에러 뜨지 않을까요?
아님 다른 방법이 있을까요?

재정의 하면 위에 직접 구현하신 코드는 제거 하셔야 합니다.
재정의 하는 코드에서 필요한 구성을 마무리 하셔야 합니다.

Spring Oauth2 사용 시, 해당 스팩 구현을 준수하셔야 하고 이와 다르게 구현하고자 하시는 경우 필요한 필터와 서비스를 직접 개발하셔야 합니다.