카카오 간편 로그인 오류

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


앱ID : 704648
서비스 url : https://its-lil.com/

카카오 간편로그인에서 발생한 오류입니다.
안드로이드는 이상 없으나 ios에서 문제가 발생하고 있고, 사파리나 크롬 등 타브라우저에서는 문제없는데 카카오 인앱브라우저에서만 로그인이 안되고있습니다.
로그 확인 시 400 에러 발생하고있습니다.

	//카카오  로그인
	@RequestMapping(value = "/login/loginKakao")
	public @ResponseBody String loginKakao(HttpServletRequest request
			,HttpSession session
			//,@RequestParam(value = "fnCallBack", required = false) String fnCallBack
			) throws Exception {
		
		String kakaoKey =  configProperties.getString("kakao.key");
		String kakaoCallback =  configProperties.getString("kakao.callback");
		//session.setAttribute("fnCallBack",fnCallBack);
		
		String reqUrl = 
				"https://kauth.kakao.com/oauth/authorize"
				+ "?client_id="+kakaoKey
				+ "&redirect_uri="+kakaoCallback
				+ "&response_type=code"
				+ "&prompt=login";
		
		return reqUrl;
	}

	// 카카오 로그인 callback
	@RequestMapping(value = "/login/kakaoCallback")
	public String kakaoCallback(
			@RequestParam(value = "code", required = false) String code
			,@RequestParam(value = "error", required = false) String error
			,HttpSession session
			,Model model) throws Exception {
		
	    //카카오인증 세션 초기화
	    session.removeAttribute("snsLoginId");
	    
  	    // 카카오로그인 페이지에서 취소버튼 눌렀을경우
	    if (error != null) {
	        if (error.equals("access_denied")) {
	            return "redirect:/index";
	        }
	    }
	    
//		System.out.println("####authorization_code#####" + code);
		String authorization_code = code;
		HashMap<String, Object> tokenInfo = getAccessToken(authorization_code);
		System.out.println("###access_Token#### : " + tokenInfo.get("accessToken"));
        System.out.println("###refresh_Token#### : " + tokenInfo.get("refreshToken"));
        
        session.setAttribute("access_token", tokenInfo.get("accessToken"));
        HashMap<String, Object> userInfo = getUserInfo((String)tokenInfo.get("accessToken"));
... 생략 ...

//토큰발급
	public HashMap<String, Object> getAccessToken (String authorize_code) {
        String access_Token = "";
        String refresh_Token = "";
        String reqURL = "https://kauth.kakao.com/oauth/token";
        String kakaoKey =  configProperties.getString("kakao.key");
		String kakaoCallback =  configProperties.getString("kakao.callback");
		HashMap<String, Object> tokenInfo = new HashMap<String, Object>();
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            //  URL연결은 입출력에 사용 될 수 있고, POST 혹은 PUT 요청을 하려면 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="+kakaoKey);  //본인이 발급받은 key
            sb.append("&redirect_uri="+kakaoCallback);     // 본인이 설정해 놓은 경로
            sb.append("&code=" + authorize_code);
            bw.write(sb.toString());
            bw.flush();

            //    결과 코드가 200이라면 성공
            int responseCode = conn.getResponseCode();
            **System.out.println("responseCode : " + responseCode);**
... 생략
}

로그
responseCode : 400
java.io.IOException: Server returned HTTP response code: 400 for URL: https://kauth.kakao.com/oauth/token

전달 받은 인가코드를 한번 더 호출하는 구조가 아니여서 정확한 원인이 무엇인지 확인 부탁드리겠습니다.

Error Body로 전달된 상세 에러 메시지 기재 부탁드려요.

[rest api 예제] java (spring boot) - 카카오 로그인, 카카오 친구목록 조회, 메시지 발송

InputStream stream = conn.getErrorStream();
		    if (stream != null) {
			    try (Scanner scanner = new Scanner(stream)) {
			        scanner.useDelimiter("\\Z");
			        response = scanner.next();
			    }			
			    System.out.println("error response : " + response);
		    }

로컬 테스트가 어려운 상황이라 문의드린건데
모바일로 카카오 인앱브라우저에서 로컬 테스트가 가능할까요?

(1) iOS 카카오톡 인앱브라우저로 카카오 로그인해서 개발하신 리다이렉트 URI에 진입 후,

액세스 토큰 발급 및 사용자 정보조회 모두 정상처리됩니다.

(2) 400에러는 첨부하신 영상의 상황에서 새로고침해서 발생하는 것으로

이미 사용한 인가코드로 액세스 토큰 재발급해서 그렇습니다. 리다이렉트 URI에서 흰화면과 무관한 상황입니다.

(3) 카카오측 API 호출 처리가 모두 정상 처리되었으므로

v2/user/me 이후 로직 또는 front 코드에 원인이 있을 것으로 보입니다.

관련 코드 기재하시면 추가 확인해보겠습니다.

// 카카오 로그인 callback
	@RequestMapping(value = "/login/kakaoCallback")
	public String kakaoCallback(
			@RequestParam(value = "code", required = false) String code
			,@RequestParam(value = "error", required = false) String error
			,HttpSession session
			,Model model) throws Exception {
		
	    //카카오인증 세션 초기화
	    session.removeAttribute("snsLoginId");
	    
  	    // 카카오로그인 페이지에서 취소버튼 눌렀을경우
	    if (error != null) {
	        if (error.equals("access_denied")) {
	            return "redirect:/index";
	        }
	    }
	    
//		System.out.println("####authorization_code#####" + code);
		String authorization_code = code;
		HashMap<String, Object> tokenInfo = getAccessToken(authorization_code);  // responseCode : 400 발생
		System.out.println("###access_Token#### : " + tokenInfo.get("accessToken")); // NULL
        System.out.println("###refresh_Token#### : " + tokenInfo.get("refreshToken")); // NULL
        
        session.setAttribute("access_token", tokenInfo.get("accessToken"));
        HashMap<String, Object> userInfo = getUserInfo((String)tokenInfo.get("accessToken")); // responseCode : 401 발생

... 생략 
}
    //토큰발급
	public HashMap<String, Object> getAccessToken (String authorize_code) {
        String access_Token = "";
        String refresh_Token = "";
        String reqURL = "https://kauth.kakao.com/oauth/token";
        String kakaoKey =  configProperties.getString("kakao.key");
		String kakaoCallback =  configProperties.getString("kakao.callback");
		HashMap<String, Object> tokenInfo = new HashMap<String, Object>();
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            //  URL연결은 입출력에 사용 될 수 있고, POST 혹은 PUT 요청을 하려면 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="+kakaoKey);  //본인이 발급받은 key
            sb.append("&redirect_uri="+kakaoCallback);     // 본인이 설정해 놓은 경로
            sb.append("&code=" + authorize_code);
            bw.write(sb.toString());
            bw.flush();

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

            //    요청을 통해 얻은 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();
           
            tokenInfo.put("accessToken", access_Token);
            tokenInfo.put("refreshToken", refresh_Token);
            
//            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 tokenInfo;
    }
}

    //유저정보조회
    public HashMap<String, Object> getUserInfo (String access_Token) {

        //    요청하는 클라이언트마다 가진 정보가 다를 수 있기에 HashMap타입으로 선언
        HashMap<String, Object> userInfo = new HashMap<String, Object>();
        String reqURL = "https://kapi.kakao.com/v2/user/me";
        try {
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");

            //    요청에 필요한 Header에 포함될 내용
            conn.setRequestProperty("Authorization", "Bearer " + access_Token);

            int responseCode = conn.getResponseCode();
            System.out.println("responseCode : " + responseCode);

            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);

            JsonParser parser = new JsonParser();
            JsonElement element = parser.parse(result);

            //JsonObject properties = element.getAsJsonObject().get("properties").getAsJsonObject();
            JsonObject kakao_account = element.getAsJsonObject().get("kakao_account").getAsJsonObject();

            //String nickname = properties.getAsJsonObject().get("nickname").getAsString();
            String id = element.getAsJsonObject().get("id").getAsString();
            String email = kakao_account.getAsJsonObject().get("email").getAsString();
//            String birthyear = kakao_account.getAsJsonObject().get("birthyear").getAsString();
//            String birthday = kakao_account.getAsJsonObject().get("birthday").getAsString();
            String gender = kakao_account.getAsJsonObject().get("gender") == null ? "" : kakao_account.getAsJsonObject().get("gender").getAsString();
            
            
//            userInfo.put("accessToken", access_Token);
//            userInfo.put("nickname", nickname);
            userInfo.put("id", id);
            userInfo.put("email", email);
//            userInfo.put("birthyear", birthyear);
//            userInfo.put("birthday", birthday);
            userInfo.put("gender", gender);
            

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

        return userInfo;
    }

<script>
$(document).ready(function(){
	var kakaoInfo = '${kakaoInfo}';
    if(kakaoInfo != ""){
        var data = JSON.parse(kakaoInfo);
		window.opener.postMessage({email:data['email'],gender:data['gender'],result:'success'}, '*');
        window.close();	
    }else{
    	window.opener.postMessage({id:data['id'],result:'fail'}, '*');
    	alert("카카오로그인 오류 발생 했습니다. 다시 로그인 진행 해주세요.");
    	window.close();
    }
});
</script>

에러 발생 부분은 두 곳입니다.
토큰 생성과 사용자 정보 조회인 getAccessToken, getUserInfo 에서 각각 400과 401 에러가 발생하고 있습니다.

토큰을 찍어보면 NULL값이고 유효한 토큰 값이 아니기 때문에 사용자 정보 조회에서 401이 뜨 는것으로 예상됩니다. 그렇다면 토큰 발급 시점이 문제인 걸로 추측되는데요, 저 흰 페이지에서 URL을 복사해 보면 ~kakaoCallback?code=~ 정상적으로 인가 코드 값을 받아오고 있습니다.

말씀하신 대로라면 현재 상황이 2번으로 보여집니다만, 영상에서 따로 새로고침을 하진 않습니다.
그렇다면 front 어딘가에서 새로고침 또는 팝업이나 새창 띄우기 같은 행위를 해주는 것 같은데,

위 kakaoCallback 메서드로 들어오기 이전에 창 새로고침과 같은 현상이 발생해서
한번 사용된 인가 코드로 또 재호출하다 보니 이 같은 현상이 발생한다고 생각하면 되는 걸까요?

에러 발생 부분은 두 곳입니다.
토큰 생성과 사용자 정보 조회인 getAccessToken, getUserInfo 에서 각각 400과 401 에러가 발생하고 있습니다.

위에 설명드린 것처럼 흰화면에서 사용자가 직접 새로고침할때만 그렇게 되므로 실제 에러와 무관한내용입니다.

토큰을 찍어보면 NULL값이고 유효한 토큰 값이 아니기 때문에 사용자 정보 조회에서 401이 뜨 는것으로 예상됩니다. 그렇다면 토큰 발급 시점이 문제인 걸로 추측되는데요, 저 흰 페이지에서 URL을 복사해 보면 ~kakaoCallback?code=~ 정상적으로 인가 코드 값을 받아오고 있습니다.

새로고침해서 이미 토큰발급에 사용된 인가코드로 토큰을 다시 발급하려해 아래 에러가 발생했고 에러 응답으로 액세스 토큰 세팅 못한 상태에서 사용자 정보조회 해서 에러 발생한 것으로 흰화면과 무관한 에러입니다.

{"error":"invalid_grant","error_description":"authorization code not found for code=Puom1u6Wl5JCqLz6Oyu1_KkR7qkaTCpz5seVHZgvRNhucd7helF1WQAAAAQKKiUPAAABkFH1sWdtZc76WqiBKA","error_code":"KOE320"}

추가 기재한 코드를 보니 흰화면의 원인이 스크립트에 있는 것으로 보이네요.

iOS 는 사용자 액션없이 실행한 스크립트에서 앱 호출, 팝업호출 등. 액션을 허용하지 않습니다.

해당 코드 제거하시고 사용자 정보 조회가 정상적으로 조회되었는지 로깅 해보시면 좋을 것 같습니다.

말씀하신 부분이 뭔지 대략적으로 이해했습니다.
스크립트쪽 문제라면 window.opener.postMessage; 이 부분이 원인일 수 있겠네요?

그렇다면 왜 pc와 안드로이드, 타 브라우저 에서는 모두 정상적으로 로그인 연동이 되는데
ios에서 카카오 인앱 브라우저에서만 이 같은 현상이 발생하는 건가요?

iOS 인앱브라우저는 모두 동일한 것으로 확인됩니다.
참고부탁드려요.

아이폰에서도 정상적으로 동작된다는 말씀이신거죠?

그러면 같은 아이폰이여도 기기 버전마다 다를 수 있는건가요?

아이폰에서도 정상적으로 동작된다는 말씀이신거죠?

iOS 정책에 따라 인앱브라우저. 즉, 앱내 웹뷰에서 사용자 액션 없는 실행 스크립트가 작동하지 않습니다.

페이스북/인스타그램/라인메신저 인앱브라우저 등 타사 인앱브라우저로도 개발하신 사이트 작동하지 않는 것 확인 가능합니다.

네 이해했습니다.
답변 감사합니다.

1개의 좋아요

한가지 확인된게 있어 재문의드립니다.

[ios 환경]
아이폰 12 미니 ios 16.6 버전 로그인 연동 성공
아이폰 13 프로 ios 17.5.1 버전 로그인 연동 실패

버전 마다 차이가 있는건지 다시 한번 확인 부탁드리겠습니다.

https://forums.developer.apple.com/forums/thread/740376

17.1 부터 안된다는 이야기가 있는데요.

해당 정책은 카카오의 정책이 아니라 애플 iOS의 웹뷰 정책이므로 자세한 것은 애플에 문의 해보시면 좋을 것 같습니다.