소셜 로그인은 사용자가 각 소셜 플랫폼(구글, 카카오, 네이버)에 저장된 계정을 통해 빠르고 안전하게 로그인할 수 있도록 지원한다. 이 글에서는 구글, 카카오, 네이버 소셜 로그인의 백엔드 구현 과정에서 각 파일들을 설명하고, 전체적인 흐름을 정리해 보겠다.
폴더 구조
Google, Kakao, Naver 소셜 로그인 구현을 마친 프로젝트 폴더 구조는 아래와 같다.

주요 파일 설명
src
└── main
├── java
│ └── com.example.Profee
│ ├── config
│ │ ├── SecurityConfig.java # CORS 및 인증 정책 설정
│ │ └── SwaggerConfig.java # API 테스트를 위한 Swagger 설정
│ ├── controller
│ │ └── OauthController.java # 소셜로그인 엔드포인트 처리
│ ├── service
│ │ └── OauthService.java # 소셜로그인 비즈니스 로직 구현
│ ├── oauth
│ │ ├── GoogleOauth.java # 구글 Oauth API 호출
│ │ ├── KakaoOauth.java # 카카오 Oauth API 호출
│ │ └── NaverOauth.java # 네이버 Oauth API 호출
│ └── dto
│ └── UserResponse.java # 사용자 응답 데이터 정의
└── resources
└── application.properties
SecurityConfig.java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() // 경로 허용
.anyRequest().authenticated() // 나머지 요청은 인증 필요
)
.csrf(csrf -> csrf.disable()); // CSRF 비활성화
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:8080")); // Swagger UI URL 허용
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
먼저 SecurityConfig 파일에서 Spring Security를 이용해 보안 정책을 정의한다. 소셜 로그인은 /auth/** 경로에서 시작되므로, 이 경로를 인증 없이 허용한다. 인증이 필요한 다른 경로와는 다르게 /auth/** 경로는 누구나 접근할 수 있어야 하기 때문이다. 또한, 프론트엔드(React Native)에서 서버로 요청을 보낼 수 있도록 CORS 정책을 설정한다. CORS 설정은 어떤 도메인에서 요청을 허용할지, 어떤 HTTP 메서드를 허용할지 등을 결정한다. 이 설정이 없다면 브라우저에서 발생하는 CORS 에러로 인해 프론트엔드에서 API를 호출할 수 없다.
SwaggerConfig.java
@Configuration
public class SwaggerConfig implements WebMvcConfigurer {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Social Login API")
.version("1.0")
.description("API for Social Login"));
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/swagger-ui/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
SwaggerConfig 파일에서는 API를 시각적으로 테스트할 수 있는 Swagger UI를 설정한다. 해당 파일을 통해 API 명세를 작성하고, 클라이언트나 개발자가 쉽게 API를 이해하고 테스트할 수 있다. 특히, 소셜 로그인과 같이 외부와 통신하는 API는 어떤 요청을 보내고 어떤 응답을 받는지 명확히 보여주는 것이 중요하다. Swagger를 통해 각 소셜 로그인 API를 설명하고, 예상되는 입력값과 반환값을 확인할 수 있다.
OauthService.java
@Service
@RequiredArgsConstructor
public class OauthService {
private final List<SocialOauth> socialOauthList; // 다양한 소셜 로그인 OAuth 처리 객체
private final UserRepository userRepository; // 사용자 정보를 저장하는 레포지토리
private static final Logger logger = LoggerFactory.getLogger(OauthService.class);
// 소셜 로그인 요청 URL을 반환하는 메서드
public String request(SocialLoginType socialLoginType) {
SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType); // 주어진 소셜 로그인 타입에 맞는 OAuth 객체 찾기
return socialOauth.getOauthRedirectURL(); // 해당 OAuth 객체의 리디렉션 URL 반환
}
// 인증 코드로 액세스 토큰을 요청하는 메서드
public String requestAccessToken(SocialLoginType socialLoginType, String code) {
SocialOauth socialOauth = this.findSocialOauthByType(socialLoginType); // 주어진 소셜 로그인 타입에 맞는 OAuth 객체 찾기
return socialOauth.requestAccessToken(code); // 액세스 토큰 요청
}
// JSON에서 액세스 토큰만 추출하는 메서드
private String extractAccessTokenFromJson(String accessTokenJson) {
// JSON을 파싱하여 액세스 토큰만 추출
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(accessTokenJson);
// 여기서 필요한 토큰만 반환
return jsonNode.get("access_token") != null ? jsonNode.get("access_token").asText() : null;
} catch (JsonProcessingException e) {
e.printStackTrace();
return null; // 실패할 경우 null 반환
}
}
// 액세스 토큰을 사용하여 사용자 정보를 가져오고, 사용자 정보가 있으면 저장하는 메서드
public User requestAccessTokenAndSaveUser(SocialLoginType socialLoginType, String code) {
System.out.println("socialLoginType & code : "+ socialLoginType + " \n & \n" + code +"\n********");
// 1. 액세스 토큰을 포함한 JSON 응답을 요청
String accessTokenJson = this.requestAccessToken(socialLoginType, code);
System.out.println("\n accesTokenJson : "+ accessTokenJson +"\n********");
// 2. JSON에서 액세스 토큰만 추출
String accessToken = extractAccessTokenFromJson(accessTokenJson);
if (accessToken == null) {
// 액세스 토큰이 없으면 예외 처리
throw new RuntimeException("Failed to extract access token.");
}
// 3. 액세스 토큰을 사용해 사용자 정보 요청
String userInfo = getUserInfo(socialLoginType, accessToken);
System.out.println("userinfo" + userInfo);
// 4. 사용자 정보를 파싱하여 User 객체 생성
User user = parseUserInfo(userInfo, socialLoginType, accessToken);
// 5. 기존 사용자 확인 후 처리
Optional<User> existingUser = userRepository.findBySocialId(user.getSocialId());
if (existingUser.isPresent()) {
// 이미 존재하는 사용자라면 로그인 처리 (액세스 토큰 갱신 등)
User existing = existingUser.get();
existing.setAccessToken(user.getAccessToken()); // 토큰 갱신
existing.setUpdatedAt(new Timestamp(System.currentTimeMillis())); // 수정 시간 갱신
return userRepository.save(existing); // 수정된 사용자 정보 저장
} else {
// 새 사용자라면 저장
user.setCreatedAt(new Timestamp(System.currentTimeMillis())); // 생성 시간 설정
return userRepository.save(user); // 새 사용자 정보 저장
}
}
// 실제 소셜 로그인 API에서 사용자 정보를 받아오는 메서드 (Google, Kakao, Naver 등)
private String getUserInfo(SocialLoginType socialLoginType, String accessToken) {
switch (socialLoginType) {
case GOOGLE:
return googleApiCall(accessToken); // Google API 호출 메서드
case KAKAO:
return kakaoApiCall(accessToken); // Kakao API 호출 메서드
case NAVER:
return naverApiCall(accessToken); // Naver API 호출 메서드
default:
throw new IllegalArgumentException("지원되지 않는 소셜 로그인 타입입니다."); // 지원되지 않는 로그인 타입 오류
}
}
// 각 소셜 로그인 제공자별 API 호출 (실제 API 호출 방식은 각 로그인 서비스의 문서를 참조해야 함)
// 구글 API 호출 시 응답 상태 코드와 메시지 출력
public String googleApiCall(String accessToken) {
try {
// accessToken을 URL 인코딩
logger.info("Starting Google API call with access token: {}", accessToken);
String encodedAccessToken = URLEncoder.encode(accessToken, "UTF-8");
logger.debug("Encoded access token: {}", encodedAccessToken);
String url = "https://www.googleapis.com/oauth2/v3/userinfo?access_token=" + encodedAccessToken;
logger.debug("Google API URL: {}", url);
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("Content-Type", "application/json");
int responseCode = con.getResponseCode();
logger.info("Google API response code: {}", responseCode);
if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
logger.info("Successfully received response from Google API.");
return response.toString();
} else {
// 실패 시 에러 메시지와 상태 코드 출력
BufferedReader in = new BufferedReader(new InputStreamReader(con.getErrorStream()));
String inputLine;
StringBuffer errorResponse = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
errorResponse.append(inputLine);
}
in.close();
logger.error("Google API call failed with response code: {}, error: {}", responseCode, errorResponse.toString());
throw new RuntimeException("Google API에서 사용자 정보를 가져오는 데 실패했습니다. 응답 코드: " + responseCode + ", 에러 메시지: " + errorResponse.toString());
}
} catch (IOException e) {
logger.error("Google API 호출 중 오류 발생: {}", e.getMessage(), e);
throw new RuntimeException("Google API 호출 중 오류 발생", e);
}
}
private String kakaoApiCall(String accessToken) {
try {
String url = "https://kapi.kakao.com/v2/user/me";
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
// Kakao의 경우 Authorization 헤더에 "Bearer" 토큰을 설정해야 합니다.
con.setRequestProperty("Authorization", "Bearer " + accessToken);
int responseCode = con.getResponseCode();
if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return response.toString();
} else {
throw new RuntimeException("Kakao API에서 사용자 정보를 가져오는 데 실패했습니다. 응답 코드: " + responseCode); // 오류 메시지 한국어로 수정
}
} catch (IOException e) {
throw new RuntimeException("Kakao API 호출 중 오류 발생", e); // 오류 메시지 한국어로 수정
}
}
private String naverApiCall(String accessToken) {
try {
String url = "https://openapi.naver.com/v1/nid/me";
URL obj = new URL(url);
HttpURLConnection con = (HttpURLConnection) obj.openConnection();
con.setRequestMethod("GET");
// Naver의 경우 Authorization 헤더에 "Bearer" 토큰을 설정해야 합니다.
con.setRequestProperty("Authorization", "Bearer " + accessToken);
int responseCode = con.getResponseCode();
if (responseCode == 200) {
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
String inputLine;
StringBuffer response = new StringBuffer();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
return response.toString();
} else {
throw new RuntimeException("Naver API에서 사용자 정보를 가져오는 데 실패했습니다. 응답 코드: " + responseCode); // 오류 메시지 한국어로 수정
}
} catch (IOException e) {
throw new RuntimeException("Naver API 호출 중 오류 발생", e); // 오류 메시지 한국어로 수정
}
}
// 사용자 정보를 파싱하여 User 객체 생성
private User parseUserInfo(String userInfo, SocialLoginType socialLoginType, String accessToken) {
JsonObject jsonObject = JsonParser.parseString(userInfo).getAsJsonObject();
// socialId와 name을 소셜 로그인 타입별로 분리
String socialId = "";
String name = "";
if (socialLoginType == SocialLoginType.GOOGLE) {
socialId = jsonObject.get("sub").getAsString(); // Google은 "sub"를 ID로 사용
name = jsonObject.get("name").getAsString(); // Google에서 제공하는 이름
} else if (socialLoginType == SocialLoginType.KAKAO) {
socialId = jsonObject.get("id").getAsString(); // Kakao는 "id"를 사용자 ID로 사용
name = jsonObject.getAsJsonObject("properties").get("nickname").getAsString(); // Kakao에서 제공하는 nickname
} else if (socialLoginType == SocialLoginType.NAVER) {
JsonObject response = jsonObject.getAsJsonObject("response"); // Naver의 데이터는 response 필드 안에 존재
socialId = response.get("id").getAsString(); // Naver는 "id"를 사용자 ID로 사용
name = response.get("name").getAsString(); // Naver에서 제공하는 이름
}
// User 객체에 정보 세팅
User user = new User();
user.setSocialId(socialId); // 소셜 ID 설정
user.setName(name); // 사용자의 이름 설정
user.setProvider(socialLoginType.name()); // 로그인 제공자 설정
user.setAccessToken(accessToken); // 액세스 토큰 설정
return user;
}
// 주어진 소셜 로그인 타입에 맞는 OAuth 객체를 찾는 메서드
private SocialOauth findSocialOauthByType(SocialLoginType socialLoginType) {
return socialOauthList.stream()
.filter(x -> x.type() == socialLoginType)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("알 수 없는 SocialLoginType 입니다.")); // 지원되지 않는 소셜 로그인 타입 오류
}
}
OauthService는 각 소셜 플랫폼의 로그인 요청을 처리하는 서비스 계층이다. 사용자가 로그인 요청을 보내면, 이 요청을 GoogleOauth, KakaoOauth, NaverOauth 중 적절한 구현체에 위임한다.
이 계층은 비즈니스 로직을 담당하며, 컨트롤러와 각 소셜 API 로직 간의 중간 다리 역할을 한다.
OauthController.java
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/auth")
@Slf4j
@Tag(name = "OAuth", description = "소셜 로그인 인증을 시작하고 콜백을 처리하는 API를 제공합니다.")
public class OauthController {
private final OauthService oauthService;
@Operation(
summary = "소셜 로그인 프로세스 시작",
description = "이 API는 사용자를 소셜 로그인 페이지로 리다이렉트하여 인증 절차를 시작합니다.",
operationId = "startSocialLogin",
parameters = {
@Parameter(name = "socialLoginType", description = "소셜 로그인 유형 (예: Kakao, Google 등).", required = true)
})
@GetMapping(value = "/{socialLoginType}")
public ResponseEntity<String> socialLoginType(
@PathVariable(name = "socialLoginType") SocialLoginType socialLoginType) {
log.info(">> 사용자로부터 SNS 로그인 요청을 받음 :: {} Social Login", socialLoginType);
String redirectURL = oauthService.request(socialLoginType);
return ResponseEntity.ok(redirectURL); // 리다이렉션 URL을 응답으로 반환
}
@Operation(
summary = "소셜 로그인 콜백 처리 (백엔드에서 사용 X)",
description = "사용자가 소셜 로그인 후 콜백 URL로 받은 코드를 통해 액세스 토큰을 요청합니다.",
operationId = "handleSocialLoginCallback",
parameters = {
@Parameter(name = "socialLoginType", description = "소셜 로그인 유형 (예: Kakao, Google 등).", required = true),
@Parameter(name = "code", description = "소셜 로그인 API 서버로부터 받은 인증 코드.", required = true)
})
@GetMapping(value = "/{socialLoginType}/callback")
public ResponseEntity<String> callback(
@PathVariable(name = "socialLoginType") SocialLoginType socialLoginType,
@RequestParam(name = "code") String code) {
log.info(">> 소셜 로그인 API 서버로부터 받은 code :: {}", code);
// 액세스 토큰을 받아온 후, 사용자 정보를 DB에 저장
User user = oauthService.requestAccessTokenAndSaveUser(socialLoginType, code); // 서비스에서 사용자 정보 받아오기
if (user != null) {
log.info(">> 사용자 정보 DB 저장 완료 :: {}", user.getName());
return ResponseEntity.ok("로그인 성공, 사용자 정보 저장 완료");
} else {
log.error(">> 사용자 정보 저장 실패");
return ResponseEntity.status(500).body("사용자 정보 저장 실패");
}
}
}
OauthController는 클라이언트 요청을 처리하는 엔드포인트이다. 사용자가 /auth/{socialLoginType} 경로로 로그인 요청을 보내면, 적절한 socialLoginType(구글, 카카오, 네이버)을 OauthService로 전달한다.
GoogleOauth.java, KakaoOauth.java, NaverOauth.java
// 공통 interface를 구현할 소셜 로그인 각 타입별 Class 생성 (Google)
@Component
@RequiredArgsConstructor
public class GoogleOauth implements SocialOauth {
@Value("${sns.google.url}")
private String GOOGLE_SNS_BASE_URL;
@Value("${sns.google.client.id}")
private String GOOGLE_SNS_CLIENT_ID;
@Value("${sns.google.callback.url}")
private String GOOGLE_SNS_CALLBACK_URL;
@Value("${sns.google.client.secret}")
private String GOOGLE_SNS_CLIENT_SECRET;
@Value("${sns.google.token.url}")
private String GOOGLE_SNS_TOKEN_BASE_URL;
@Override
public String getOauthRedirectURL() {
Map<String, Object> params = new HashMap<>();
params.put("scope", "profile");
params.put("response_type", "code");
params.put("client_id", GOOGLE_SNS_CLIENT_ID);
params.put("redirect_uri", GOOGLE_SNS_CALLBACK_URL);
String parameterString = params.entrySet().stream()
.map(x -> x.getKey() + "=" + x.getValue())
.collect(Collectors.joining("&"));
return GOOGLE_SNS_BASE_URL + "?" + parameterString;
}
@Override
public String requestAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> params = new HashMap<>();
params.put("code", code);
params.put("client_id", GOOGLE_SNS_CLIENT_ID);
params.put("client_secret", GOOGLE_SNS_CLIENT_SECRET);
params.put("redirect_uri", GOOGLE_SNS_CALLBACK_URL);
params.put("grant_type", "authorization_code");
ResponseEntity<String> responseEntity =
restTemplate.postForEntity(GOOGLE_SNS_TOKEN_BASE_URL, params, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
return responseEntity.getBody();
}
return "구글 로그인 요청 처리 실패";
}
public String requestAccessTokenUsingURL(String code) {
try {
URL url = new URL(GOOGLE_SNS_TOKEN_BASE_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoOutput(true);
Map<String, Object> params = new HashMap<>();
params.put("code", code);
params.put("client_id", GOOGLE_SNS_CLIENT_ID);
params.put("client_secret", GOOGLE_SNS_CLIENT_SECRET);
params.put("redirect_uri", GOOGLE_SNS_CALLBACK_URL);
params.put("grant_type", "authorization_code");
String parameterString = params.entrySet().stream()
.map(x -> x.getKey() + "=" + x.getValue())
.collect(Collectors.joining("&"));
BufferedOutputStream bous = new BufferedOutputStream(conn.getOutputStream());
bous.write(parameterString.getBytes());
bous.flush();
bous.close();
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
if (conn.getResponseCode() == 200) {
return sb.toString();
}
return "구글 로그인 요청 처리 실패";
} catch (IOException e) {
throw new IllegalArgumentException("알 수 없는 구글 로그인 Access Token 요청 URL 입니다 :: " + GOOGLE_SNS_TOKEN_BASE_URL);
}
}
}
// 공통 interface를 구현할 소셜 로그인 각 타입별 Class 생성 (Kakao)
@Slf4j
@Component
@RequiredArgsConstructor
public class KakaoOauth implements SocialOauth {
@Value("${sns.kakao.url}")
private String KAKAO_SNS_BASE_URL;
@Value("${sns.kakao.client.id}")
private String KAKAO_SNS_CLIENT_ID;
@Value("${sns.kakao.callback.url}")
private String KAKAO_SNS_CALLBACK_URL;
@Value("${sns.kakao.client.secret}")
private String KAKAO_SNS_CLIENT_SECRET;
@Value("${sns.kakao.token.url}")
private String KAKAO_SNS_TOKEN_BASE_URL;
@Override
public String getOauthRedirectURL() {
Map<String, Object> params = new HashMap<>();
params.put("response_type", "code");
params.put("client_id", KAKAO_SNS_CLIENT_ID);
params.put("redirect_uri", KAKAO_SNS_CALLBACK_URL);
String parameterString = params.entrySet().stream()
.map(x -> x.getKey() + "=" + x.getValue())
.collect(Collectors.joining("&"));
return KAKAO_SNS_BASE_URL + "?" + parameterString;
}
@Override
public String requestAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();
// 1. HTTP 헤더 설정 (Content-Type을 application/x-www-form-urlencoded로 설정)
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 2. 요청 파라미터를 문자열로 인코딩
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", code);
params.add("client_id", KAKAO_SNS_CLIENT_ID);
params.add("client_secret", KAKAO_SNS_CLIENT_SECRET);
params.add("redirect_uri", KAKAO_SNS_CALLBACK_URL);
params.add("grant_type", "authorization_code");
// 3. HttpEntity 생성 (헤더와 파라미터 포함)
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
// 4. POST 요청 보내기
ResponseEntity<String> responseEntity =
restTemplate.postForEntity(KAKAO_SNS_TOKEN_BASE_URL, requestEntity, String.class);
// 5. 응답 확인 및 반환
if (responseEntity.getStatusCode() == HttpStatus.OK) {
return responseEntity.getBody();
}
return "카카오 로그인 요청 처리 실패";
}
public String requestAccessTokenUsingURL(String code) {
try {
URL url = new URL(KAKAO_SNS_TOKEN_BASE_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setDoOutput(true);
Map<String, Object> params = new HashMap<>();
params.put("code", code);
params.put("client_id", KAKAO_SNS_CLIENT_ID);
params.put("client_secret", KAKAO_SNS_CLIENT_SECRET);
params.put("redirect_uri", KAKAO_SNS_CALLBACK_URL);
params.put("grant_type", "authorization_code");
String parameterString = params.entrySet().stream()
.map(x -> x.getKey() + "=" + x.getValue())
.collect(Collectors.joining("&"));
BufferedOutputStream bous = new BufferedOutputStream(conn.getOutputStream());
bous.write(parameterString.getBytes());
bous.flush();
bous.close();
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
if (conn.getResponseCode() == 200) {
return sb.toString();
}
return "카카오 로그인 요청 처리 실패";
} catch (IOException e) {
throw new IllegalArgumentException("알 수 없는 카카오 로그인 Access Token 요청 URL 입니다 :: " + KAKAO_SNS_TOKEN_BASE_URL);
}
}
}
// 공통 interface를 구현할 소셜 로그인 각 타입별 Class 생성 (Naver)
@Component
@RequiredArgsConstructor
public class NaverOauth implements SocialOauth {
@Value("${sns.naver.url}")
private String NAVER_SNS_BASE_URL;
@Value("${sns.naver.client.id}")
private String NAVER_SNS_CLIENT_ID;
@Value("${sns.naver.callback.url}")
private String NAVER_SNS_CALLBACK_URL;
@Value("${sns.naver.client.secret}")
private String NAVER_SNS_CLIENT_SECRET;
@Value("${sns.naver.token.url}")
private String NAVER_SNS_TOKEN_BASE_URL;
@Override
public String getOauthRedirectURL() {
Map<String, Object> params = new HashMap<>();
params.put("response_type", "code");
params.put("client_id", NAVER_SNS_CLIENT_ID);
params.put("redirect_uri", NAVER_SNS_CALLBACK_URL);
params.put("state", "random_state_value"); // CSRF 방지를 위한 state 파라미터 추가
String parameterString = params.entrySet().stream()
.map(x -> x.getKey() + "=" + x.getValue())
.collect(Collectors.joining("&"));
return NAVER_SNS_BASE_URL + "?" + parameterString;
}
@Override
public String requestAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();
// Header 설정
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 파라미터 설정 (application/x-www-form-urlencoded)
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", code);
params.add("client_id", NAVER_SNS_CLIENT_ID);
params.add("client_secret", NAVER_SNS_CLIENT_SECRET);
params.add("redirect_uri", NAVER_SNS_CALLBACK_URL);
params.add("grant_type", "authorization_code");
params.add("state", "random_state_value");
// HTTP Entity 생성
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
// 요청 전송
ResponseEntity<String> responseEntity =
restTemplate.postForEntity(NAVER_SNS_TOKEN_BASE_URL, requestEntity, String.class);
if (responseEntity.getStatusCode() == HttpStatus.OK) {
return responseEntity.getBody();
}
return "네이버 로그인 요청 처리 실패";
}
}
이 세 파일은 각각의 소셜 플랫폼의 API와 통신하는 구현체이다.
- GoogleOauth: 구글 OAuth 2.0 인증을 처리한다. Access Token을 발급받고, 이를 통해 사용자 정보를 가져온다.
- KakaoOauth: 카카오 로그인 API와 통신한다. 카카오에서 제공하는 Access Token을 기반으로 사용자 정보를 요청한다.
- NaverOauth: 네이버 로그인 API를 사용해 사용자 정보를 가져온다. 네이버는 사용자 인증 후, 별도로 추가 요청을 통해 사용자 정보를 반환한다.
UserResponse.java
public class UserResponse {
private String name;
private String accessToken;
private String provider;
public UserResponse(String name, String accessToken, String provider) {
this.name = name;
this.accessToken = accessToken;
this.provider = provider;
}
// Getter, Setter 추가
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getProvider() {
return provider;
}
public void setProvider(String provider) {
this.provider = provider;
}
}
UserResponse는 사용자 정보를 담아 클라이언트에 전달하는 DTO(Data Transfer Object)이다. 이 파일에서 서버가 클라이언트에 반환할 데이터를 정의한다. 사용자가 소셜 로그인을 완료하면, 서버는 사용자 이름(name), 액세스 토큰(accessToken), 그리고 로그인 제공자(provider, 예: 구글, 카카오, 네이버)를 반환한다. 이 데이터는 프론트엔드에서 사용자 상태를 관리하거나 이후 요청에 인증 정보를 포함하는 데 사용된다.
전체 흐름
- 클라이언트 요청: 사용자가 프론트엔드에서 소셜 로그인 버튼을 클릭하면, /auth/{socialLoginType} 경로로 로그인 요청이 전달된다.
예: /auth/google, /auth/kakao, /auth/naver. - OauthController: 요청이 들어오면 적절한 socialLoginType(구글, 카카오, 네이버)을 OauthService로 전달한다.
- OauthService: socialLoginType에 따라 GoogleOauth, KakaoOauth, NaverOauth 중 적합한 로직을 호출한다.
- GoogleOauth, KakaoOauth, NaverOauth: 각 플랫폼의 API와 통신하여 Authorization Code로 Access Token을 발급 받고, Access Token으로 사용자 정보를 받아온다.
- UserResponse: 사용자 정보를 UserResponse 형식으로 변환해 클라이언트에 반환한다.
테스트 결과
Swagger를 이용해 구글, 카카오, 네이버 순서로 소셜로그인 API 테스트를 진행했다.

API가 로직대로 작동하여 MySQL에 토큰 정보와 유저 데이터가 잘 저장된 것을 확인할 수 있었다. 😀
'프로젝트 > Profee' 카테고리의 다른 글
[RN] Error: Unable to resolve module ./App.js 오류 해결 (1) | 2024.10.29 |
---|---|
[RN] Error resolving plugin [id: 'com.facebook.react.settings'] 오류 해결 (0) | 2024.10.14 |
[RN] 안드로이드 에뮬레이터 까만 화면만 나올 때 (0) | 2024.10.14 |
[SpringBoot] Naver 소셜 로그인 구현: OAuth2.0 & Spring Security 활용 (0) | 2024.09.11 |
[SpringBoot] Kakao 소셜 로그인 구현: OAuth2.0 & Spring Security 활용 (0) | 2024.09.11 |