안녕하세요. 컴퓨터 공학과 복수전공을 하며 힘겹게 과제를 하는 대학생입니다.
저는 카카오 로그인을 활용해 기존에 제가 만든 일반적인 회원가입 및 로그인 방식과 합쳐 데이터베이스에 저장하는 부분을 구현하고 있습니다.
지식이 많이 부족하고, 질문을 하는 방법조차 잘 몰라 두서없이 글을 남깁니다.
ID : 1132097
Failed to get access token, response code: 400, response: {“error”:“invalid_grant”,“error_description”:“authorization code not found for code=8yTFsxkVbgaXe2LzFdKyQqJsQCtI6iijAAAAAQo9dGkAAAGSCwhcALG7d-HwzTGR”,“error_code”:“KOE320”}
우선 인텔리제이에서 뜨는 오류는 이렇습니다.
찾아보니 인가 코드 재사용 으로 인한 문제라더군요.
하지만 저는 도저히 재사용된 부분을 찾을 수가 없습니다…
이유를 모르겠어요.
조언 부탁드립니다.
package com.example.project.controller;
import com.example.project.service.OAuthService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import lombok.RequiredArgsConstructor;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@RequiredArgsConstructor
@RequestMapping(“/oauth”)
public class OAuthController {
private final OAuthService oAuthService;
// 인가 코드 재사용 방지를 위한 메모리 저장소 (실제 구현에서는 DB나 Redis 사용 추천)
private final Map<String, Boolean> usedAuthorizationCodes = new ConcurrentHashMap<>();
@ResponseBody
@GetMapping("/kakao")
public synchronized ResponseEntity<Map<String, String>> kakaoCallback(@RequestParam String code) {
Map<String, String> response = new HashMap<>();
// 인가 코드 재사용 방지 처리
if (usedAuthorizationCodes.containsKey(code)) {
response.put("error", "Authorization code has already been used");
return ResponseEntity.status(400).body(response);
}
try {
// 인가 코드를 사용한 것으로 기록
usedAuthorizationCodes.put(code, true);
// 인가 코드를 이용해 액세스 토큰을 가져옴
String accessToken = oAuthService.getKakaoAccessToken(code);
// 액세스 토큰이 잘 가져왔는지 확인
System.out.println("Access Token: " + accessToken);
// JSON 응답 생성
response.put("access_token", accessToken);
return ResponseEntity.ok(response);
} catch (RuntimeException e) {
// 인가 코드 사용 실패 시 기록을 삭제하여 다음 시도를 가능하게 함
usedAuthorizationCodes.remove(code);
if (e.getMessage().contains("KOE320")) {
response.put("error", "Authorization code has already been used or is invalid");
return ResponseEntity.status(400).body(response);
} else {
// 일반적인 오류 처리
response.put("error", "Failed to retrieve access token");
return ResponseEntity.status(500).body(response);
}
}
}
}
package com.example.project.service;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
@Service
public class OAuthService {
private static final Logger logger = LoggerFactory.getLogger(OAuthService.class);
@Value("${kakao.client-id}")
private String clientId;
@Value("${kakao.redirect-uri}")
private String redirectUri;
public String getKakaoAccessToken(String code) {
String access_Token = "";
String reqURL = "https://kauth.kakao.com/oauth/token";
try {
URL url = new URL(reqURL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// POST 요청 설정
conn.setRequestMethod("POST");
conn.setDoOutput(true);
// 요청 파라미터 설정
try (BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()))) {
StringBuilder sb = new StringBuilder();
sb.append("grant_type=authorization_code");
sb.append("&client_id=").append(clientId);
sb.append("&redirect_uri=").append(redirectUri);
sb.append("&code=").append(code);
bw.write(sb.toString());
bw.flush();
}
// 응답 코드 확인
int responseCode = conn.getResponseCode();
if (responseCode != 200) {
String errorResponse = "";
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream()))) {
String line;
while ((line = br.readLine()) != null) {
errorResponse += line;
}
}
logger.error("Failed to get access token, response code: {}, response: {}", responseCode, errorResponse);
throw new RuntimeException("Failed to get access token: " + errorResponse);
}
// JSON 타입의 응답 메시지 읽기
StringBuilder result = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
result.append(line);
}
}
// JSON 파싱
JsonElement element = JsonParser.parseString(result.toString());
access_Token = element.getAsJsonObject().get("access_token").getAsString();
} catch (IOException e) {
logger.error("Exception occurred while getting access token", e);
throw new RuntimeException("Failed to get access token", e);
}
return access_Token;
}
}
아래는 안스 주요 코드입니다.
private void performKakaoLogin() {
if (isKakaoLoginInProgress) return; // 중복 클릭 방지
isKakaoLoginInProgress = true;
UserApiClient.getInstance().loginWithKakaoAccount(Login.this, (token, error) -> {
isKakaoLoginInProgress = false;
if (error != null) {
Toast.makeText(Login.this, "카카오 로그인 실패: " + error.getMessage(), Toast.LENGTH_SHORT).show();
Log.e("KakaoLogin", "카카오 로그인 실패", error);
} else if (token != null) {
Toast.makeText(Login.this, "카카오 로그인 성공", Toast.LENGTH_SHORT).show();
Log.i("KakaoLogin", "카카오 로그인 성공! Access Token: " + token.getAccessToken());
// 서버로 Access Token을 보내기
sendAuthorizationCodeToServer(token.getAccessToken());
}
return null; // 반환값 명시
});
}
private void sendAuthorizationCodeToServer(String authCode) {
OAuthApi oAuthApi = ApiClient.getOAuthApiService(this);
Call<String> call = oAuthApi.getKakaoAccessToken(authCode);
call.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
if (response.isSuccessful() && response.body() != null) {
String accessToken = response.body();
Log.i("KakaoLogin", "서버로부터 받은 Access Token: " + accessToken);
// Access Token을 저장하고 필요한 후속 작업 수행
saveAccessToken(accessToken);
navigateToMainPage();
} else {
Toast.makeText(Login.this, "서버로부터 Access Token을 받지 못했습니다.", Toast.LENGTH_SHORT).show();
Log.e("KakaoLogin", "서버 응답 실패: " + response.code());
}
}
@Override
public void onFailure(Call<String> call, Throwable t) {
Toast.makeText(Login.this, "서버 요청 실패", Toast.LENGTH_SHORT).show();
Log.e("KakaoLogin", "서버 요청 실패", t);
}
});
}
private void saveAccessToken(String accessToken) {
SharedPreferences sharedPreferences = getSharedPreferences("MyAppPrefs", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("access_token", accessToken);
editor.apply();
}