[TIL] 37. 카카오 OAuth 로그인

김지수·2024년 6월 12일

TIL

목록 보기
37/53

OAuth

OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는 접근 위임을 위한 개방형 표준입니다.
사용자가 애플리케이션에게 모든 권한을 넘기지 않고 사용자 대신 서비스를 이용할 수 있게 해주는 HTTP 기반의 보안 프로토콜 입니다.
OAuth를 사용하는 서비스 제공자는 대표적으로 구글, 페이스북 등이 있습니다.
국내에는 대표적으로 네이버와 카카오가 있습니다.

아래는 카카오 로그인의 큰 흐름을 설명해 주는 그림입니다.

1 . 회원가입

  1. 내 애플리케이션 메뉴 선택 > 애플리케이션 추가하기

  2. 앱 아이콘, 앱 이름, 사업자명 저장

  3. 사이트 도메인 등록하기

5.카카오로 로그인 했을 때 인가토큰을 받게 될 Redirect URI (callback) 를 설정하기

활성화 설정

6. 동의 항목 설정하기

    1. 닉네임을 '필수 동의'로 받습니다.



    1. 카카오계정(이메일) 정보를 '선택 동의'로 받습니다.

UserController

@GetMapping("/user/kakao/callback")
  public String kakaoLogin(@RequestParam String code, HttpServletResponse response)
      throws JsonProcessingException {
    String token = kakaoService.kakaoLogin(code);

// 여기서 substring을 7을 해야지 띄어쓰기 불가라서 오류가 안뜬다.
    Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7)); 
    cookie.setPath("/");
    response.addCookie(cookie);

    return "redirect:/";
  }

KakaoService

@Slf4j(topic = "KAKAO Login")
@Service
@RequiredArgsConstructor
public class KakaoService {

  private final PasswordEncoder passwordEncoder;
  private final UserRepository userRepository;
  private final RestTemplate restTemplate;
  private final JwtUtil jwtUtil;

  public String kakaoLogin(String code) throws JsonProcessingException {
    // 1. "인가 코드"로 "액세스 토큰" 요청

    String accessToken = getToken(code);

    // 2. 토큰으로 카카오 API 호출 : "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
    KakaoUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);

    // 3. 필요시에 회원가입
    User kakaoUser = registerKakaoUserIfNeeded(kakaoUserInfo);

    // 4. JWT 토큰 반환
    String createToken = jwtUtil.createToken(kakaoUser.getUsername(), kakaoUser.getRole());
    return createToken;
  }

  private String getToken(String code) throws JsonProcessingException {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
        .fromUriString("https://kauth.kakao.com")
        .path("/oauth/token")
        .encode()
        .build()
        .toUri();

    // HTTP Header 생성
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    // HTTP Body 생성
    MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
    body.add("grant_type", "authorization_code");
    body.add("client_id", "1087cc9dfec17cd0be2e807bf580869c");
    body.add("redirect_uri", "http://localhost:8080/api/user/kakao/callback");
    body.add("code", code);

    RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
        .post(uri)
        .headers(headers)
        .body(body);

    // HTTP 요청 보내기
    ResponseEntity<String> response = restTemplate.exchange(
        requestEntity,
        String.class
    );

    // HTTP 응답 (JSON) -> 액세스 토큰 파싱
    JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
    return jsonNode.get("access_token").asText();
  }

  private KakaoUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
    // 요청 URL 만들기
    URI uri = UriComponentsBuilder
        .fromUriString("https://kapi.kakao.com")
        .path("/v2/user/me")
        .encode()
        .build()
        .toUri();

    // HTTP Header 생성
    HttpHeaders headers = new HttpHeaders();
    headers.add("Authorization", "Bearer " + accessToken);
    headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

    RequestEntity<MultiValueMap<String, String>> requestEntity = RequestEntity
        .post(uri)
        .headers(headers)
        .body(new LinkedMultiValueMap<>());

    // HTTP 요청 보내기
    ResponseEntity<String> response = restTemplate.exchange(
        requestEntity,
        String.class
    );

    JsonNode jsonNode = new ObjectMapper().readTree(response.getBody());
    Long id = jsonNode.get("id").asLong();
    String nickname = jsonNode.get("properties")
        .get("nickname").asText();
    String email = jsonNode.get("kakao_account")
        .get("email").asText();

    log.info("카카오 사용자 정보: " + id + ", " + nickname + ", " + email);
    return new KakaoUserInfoDto(id, nickname, email);
  }

  private User registerKakaoUserIfNeeded(KakaoUserInfoDto kakaoUserInfo) {
    // DB 에 중복된 Kakao Id 가 있는지 확인
    Long kakaoId = kakaoUserInfo.getId();
    User kakaoUser = userRepository.findByKakaoId(kakaoId).orElse(null);

    if (kakaoUser == null) {
      // 카카오 사용자 email 동일한 email 가진 회원이 있는지 확인
      String kakaoEmail = kakaoUserInfo.getEmail();
      User sameEmailUser = userRepository.findByEmail(kakaoEmail).orElse(null);
      if (sameEmailUser != null) {
        kakaoUser = sameEmailUser;
        // 기존 회원정보에 카카오 Id 추가
        kakaoUser = kakaoUser.kakaoIdUpdate(kakaoId);
      } else {
        // 신규 회원가입
        // password: random UUID
        String password = UUID.randomUUID().toString();
        String encodedPassword = passwordEncoder.encode(password);

        // email: kakao email
        String email = kakaoUserInfo.getEmail();

        kakaoUser = new User(kakaoUserInfo.getNickname(), encodedPassword,
        email, UserRoleEnum.USER, kakaoId);
      }

      userRepository.save(kakaoUser);
    }
    return kakaoUser;
  }

여기서 private final RestTemplate restTemplate;
RestTemplate으로 주입을 받을 수 없어서

RestTemplateConfig 로 빈을 설정한다

@Configuration
public class RestTemplateConfig  {
  @Bean
  public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
    return restTemplateBuilder
        // RestTemplate 으로 외부 API 호출 시 일정 시간이 지나도 응답이 없을 때
        // 무한 대기 상태 방지를 위해 강제 종료 설정
        .setConnectTimeout(Duration.ofSeconds(5)) // 5초
        .setReadTimeout(Duration.ofSeconds(5)) // 5초
        .build();
  }
}

오늘의 회고


오늘은 카카오 OAuth를 활용하여 로그인 기능을 구현했다. 사용자 인증 과정에서 발생할 수 있는 문제들 해결하며 보완성을 높였다. 이번 작업을 통해 OAuth의 원리를 더 깊이 이해하게 되어 좋았다.

profile
서툴고 부족한 점이 많지만, 배우고 발전하며 성장하기 위해 노력하겠습니다.

0개의 댓글