카카오 로그인 구현하기

화낼거양's avatar
Dec 11, 2024
카카오 로그인 구현하기
 
 
💡
핵심적인 부분만 업로드 되어 있습니다.
 
notion image
 

카카오 로그인 버튼 script :

<script> function kakaoLogin() { let url = `https://kauth.kakao.com/oauth/authorize?client_id=caefd05138bce05df366ee94d807a224&redirect_uri=http://localhost:8080/oauth&response_type=code`; location.href = url; } </script>
 
  1. 함수 정의
      • kakaoLogin 함수는 카카오 로그인 프로세스를 시작합니다.
      • 함수가 호출되면 카카오 인증 서버의 URL을 생성하고 location.href를 사용하여 해당 URL로 리다이렉트합니다.
  1. URL 구성 요소:
      • https://kauth.kakao.com/oauth/authorize: 카카오 OAuth 인증 엔드포인트입니다.
      • client_id: 애플리케이션의 클라이언트 ID입니다. (예: caefd05138bce05df366ee94d807a224)
      • redirect_uri: 인증 후 리다이렉트 될 애플리케이션의 URL입니다. (예: http://localhost:8080/oauth)
      • response_type=code: 응답 타입을 인증 코드로 설정합니다. 카카오 서버는 인증 코드를 반환합니다.

동작 원리

  1. 사용자가 로그인 버튼을 클릭하면 kakaoLogin 함수가 호출됩니다.
  1. kakaoLogin 함수는 카카오 인증 서버의 URL을 생성합니다.
  1. 브라우저는 location.href를 통해 카카오 인증 서버로 리다이렉트됩니다.
  1. 사용자는 카카오 로그인 페이지에서 인증을 완료합니다.
  1. 인증이 성공하면, 카카오 서버는 지정된 redirect_uri로 인증 코드를 포함하여 리다이렉트합니다.
이 과정에서 인증 코드는 애플리케이션 서버로 전달되며, 서버는 이 코드를 사용하여 액세스 토큰을 발급받아 사용자의 정보에 접근할 수 있게 됩니다.
 
 
 

Controller :

@GetMapping("/oauth") public String oauth(@RequestParam("code") String code){ User sessionUser = userService.카카오로그인(code); session.setAttribute("sessionUser", sessionUser); return "redirect:/"; }
 
  • @GetMapping("/oauth"):
    • 이 어노테이션은 HTTP GET 요청을 /oauth 경로로 매핑합니다. 사용자가 카카오 로그인 인증 후 리다이렉트될 경로입니다.
  • 메서드 시그니처:
    • public String oauth(@RequestParam("code") String code)
    • @RequestParam("code"): 요청 파라미터에서 code 값을 가져옵니다. 이 code는 카카오 인증 서버가 리다이렉트할 때 URL에 포함된 인증 코드입니다.
    • 메서드의 리턴 타입은 String으로, 리다이렉트 URL을 반환합니다.
  • 사용자 서비스 호출:
    • `User sessionUser = userService.카카오로그인(code);`
    • userService.카카오로그인(code): userService를 호출하여 카카오 인증 코드를 사용해 로그인 처리를 수행합니다. 이 메서드는 code를 사용하여 카카오 서버로부터 액세스 토큰과 사용자 정보를 가져옵니다.
    • sessionUser: 인증된 사용자 정보를 포함하는 User 객체입니다.
  • 세션에 사용자 정보 저장:
    • `session.setAttribute("sessionUser", sessionUser);`
    • session.setAttribute("sessionUser", sessionUser): 세션에 사용자 정보를 저장합니다. 이렇게 하면 사용자는 로그인 상태를 유지할 수 있습니다.
 
 
 
 
 

Service :

public User 카카오로그인(String code) { // 1. 카카오 로그인 요청 UserResponse.KakaoDTO kakaoDTO = MyHttpUtil.post(code); // 2. id token 검증 UserResponse.IdTokenDTO idTokenDTO = MyRSAUtil.verify(kakaoDTO.getIdToken()); // 3. 회원가입 유무 확인 String username = "kakao_"+idTokenDTO.getSub(); Optional<User> userOP = userRepository.findByUsername(username); // 4. 안되어있다면 강제 회원가입 if(userOP.isEmpty()){ User user = User.builder() .username(username) .password(UUID.randomUUID().toString()) .provider(ProviderEnum.KAKAO) .build(); User userPS = userRepository.save(user); return userPS; } // 5. User 객체 리턴 return userOP.get(); }
 
💡
MyHttpUtil.post, MyRSAUtil.verify 등의 내용은 service 객체 설명 이후에 작성되어있습니다.
 
  1. 카카오 로그인 요청:
    1. UserResponse.KakaoDTO kakaoDTO = MyHttpUtil.post(code);
      • MyHttpUtil.post(code): 카카오 서버에 로그인 요청을 보내고 인증 코드를 사용하여 사용자 정보를 가져옵니다.
      • kakaoDTO: 카카오 서버로부터 받은 응답 데이터입니다. 이 데이터에는 ID 토큰 등이 포함되어 있습니다.
  1. ID 토큰 검증:
    1. UserResponse.IdTokenDTO idTokenDTO = MyRSAUtil.verify(kakaoDTO.getIdToken());
      • MyRSAUtil.verify(kakaoDTO.getIdToken()): 받은 ID 토큰을 검증합니다.
      • idTokenDTO: 검증된 ID 토큰 정보입니다. 이 정보에는 사용자의 고유 ID(sub) 등이 포함되어 있습니다.
  1. 회원가입 유무 확인:
    1. String username = "kakao_" + idTokenDTO.getSub(); Optional<User> userOP = userRepository.findByUsername(username);
      • username: 카카오 사용자 고유 ID를 기반으로 생성된 사용자 이름입니다.
      • userRepository.findByUsername(username): 데이터베이스에서 해당 사용자 이름으로 등록된 사용자가 있는지 확인합니다.
      • userOP: Optional로 반환된 사용자 객체입니다.
  1. 회원가입이 안되어 있다면 강제 회원가입:
    1. if (userOP.isEmpty()) { User user = User.builder() .username(username) .password(UUID.randomUUID().toString()) .provider(ProviderEnum.KAKAO) .build(); User userPS = userRepository.save(user); return userPS; }
      • 사용자가 데이터베이스에 존재하지 않으면 새로운 사용자 객체를 생성합니다.
      • UUID.randomUUID().toString(): 무작위 패스워드를 생성합니다.
      • userRepository.save(user): 새 사용자 객체를 데이터베이스에 저장합니다.
      • 저장된 사용자 객체(userPS)를 반환합니다.
  1. User 객체 반환:
    1. return userOP.get();
      • 이미 데이터베이스에 존재하는 사용자 객체를 반환합니다.
 
 
 
💡
Repository 내용은 생략합니다.
 
 
 

 
 

Service 메서드 Detail

 
 

1. 반환 값으로 사용한 DTO 클래스 :

public class UserResponse { @Data public static class KakaoDTO { @JsonProperty("access_token") private String accessToken; @JsonProperty("id_token") private String idToken; } @Data public static class IdTokenDTO { private String sub; private String nickname; // 해당 예제에서 아래의 필드들은 사용하지 않으나, 관련된 모든 필드를 작성하여야 예외가 발생하지 않기 때문에 작성 private String aud; @JsonProperty("auth_time") private String authTime; private String iss; private String exp; private String iat; } }
 
클릭하면 DTO 클래스의 세부 설명을 확인할 수 있습니다.
  • 정적 클래스 KakaoDTO:
    •  
      public static class KakaoDTO { @JsonProperty("access_token") private String accessToken; @JsonProperty("id_token") private String idToken; }
       
    • KakaoDTO 클래스는 카카오 로그인 응답 데이터 중 access_tokenid_token 필드를 매핑합니다.
    • @JsonProperty("access_token"): Jackson 라이브러리의 어노테이션으로, JSON 데이터의 access_token 필드를 자바 필드 accessToken에 매핑합니다.
    • @JsonProperty("id_token"): JSON 데이터의 id_token 필드를 자바 필드 idToken에 매핑합니다.
 
  • 정적 클래스 IdTokenDTO:
    •  
      public static class IdTokenDTO { private String sub; private String nickname; // 해당 예제에서 아래의 필드들은 사용하지 않으나, 관련된 모든 필드를 작성하여야 예외가 발생하지 않기 때문에 작성 private String aud; @JsonProperty("auth_time") private String authTime; private String iss; private String exp; private String iat; }
       
    • IdTokenDTO 클래스는 ID 토큰의 데이터를 매핑합니다.
    • sub: 사용자 고유 ID입니다.
    • nickname: 사용자의 닉네임입니다.
    • 추가 필드들(aud, authTime, iss, exp, iat)은 예제에서는 사용되지 않지만, JSON 응답 데이터의 모든 필드를 매핑하기 위해 작성되었습니다.
    • @JsonProperty("auth_time"): JSON 데이터의 auth_time 필드를 자바 필드 authTime에 매핑합니다.
 
 
※참고
JsonProperty 어노테이션:
 
@JsonProperty("access_token") private String accessToken; @JsonProperty("id_token") private String idToken;
 
  • Jackson 라이브러리의 @JsonProperty 어노테이션은 JSON 필드 이름과 자바 필드 이름을 매핑합니다.
  • JSON 데이터의 access_token 필드는 accessToken 필드에 매핑되고, id_token 필드는 idToken 필드에 매핑됩니다.
 
 
 
 

2. MyHttpUtil 클래스 :

public class MyHttpUtil { public static UserResponse.KakaoDTO post(String code){ String redirectUri = "http://localhost:8080/oauth"; String clientId = "caefd05138bce05df366ee94d807a224"; RestTemplate restTemplate = new RestTemplate(); String url = "https://kauth.kakao.com/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"); // 요청 바디 설정 String requestBody = """ grant_type=authorization_code&client_id=${clientId}&redirect_uri=${redirectUri}&code=${code} """.replace("${clientId}", clientId) .replace("${redirectUri}", redirectUri) .replace("${code}", code); // HttpEntity에 헤더와 바디 추가 HttpEntity<String> request = new HttpEntity<>(requestBody, headers); // POST 요청 ResponseEntity<UserResponse.KakaoDTO> response = restTemplate.exchange(url, HttpMethod.POST, request, UserResponse.KakaoDTO.class); // 응답 출력 UserResponse.KakaoDTO kakaoDTO = response.getBody(); return kakaoDTO; } }
 
클릭하면 MyHttpUtil 클래스 세부 설명을 확인할 수 있습니다.
 
  • 카카오 OAuth 토큰 요청:
    •  
      String redirectUri = "http://localhost:8080/oauth"; String clientId = "caefd05138bce05df366ee94d807a224";
       
    • redirectUri: 카카오 인증 후 리다이렉트될 URI입니다.
    • clientId: 애플리케이션의 클라이언트 ID입니다.
 
  • RestTemplate 객체 생성:
    •  
      RestTemplate restTemplate = new RestTemplate(); String url = "https://kauth.kakao.com/oauth/token";
       
    • RestTemplate: Spring에서 제공하는 HTTP 요청을 보내기 위한 객체입니다.
    • url: 카카오 인증 서버의 토큰 요청 URL입니다.
 
  • HTTP 헤더 설정:
    •  
      HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");
       
    • HttpHeaders: HTTP 요청 헤더를 설정하기 위한 객체입니다.
    • Content-Type: 요청 본문의 MIME 타입을 설정합니다.
 
  • 요청 본문 설정:
    •  
      String requestBody = """ grant_type=authorization_code&client_id=${clientId}&redirect_uri=${redirectUri}&code=${code} """.replace("${clientId}", clientId) .replace("${redirectUri}", redirectUri) .replace("${code}", code);
       
    • requestBody: POST 요청의 본문을 설정합니다. 문자열 템플릿을 사용하여 파라미터 값을 치환합니다.
 
  • HttpEntity 생성:
    •  
      HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
       
    • HttpEntity: HTTP 요청 본문과 헤더를 포함하는 객체입니다.
 
  • POST 요청 보내기:
    •  
      ResponseEntity<UserResponse.KakaoDTO> response = restTemplate.exchange(url, HttpMethod.POST, request, UserResponse.KakaoDTO.class);
       
    • restTemplate.exchange: POST 요청을 보내고 응답을 받습니다.
    • UserResponse.KakaoDTO.class: 응답 데이터를 매핑할 DTO 클래스입니다.
 
  • 응답 처리:
    •  
      UserResponse.KakaoDTO kakaoDTO = response.getBody(); return kakaoDTO;
       
    • 응답 본문에서 userResponse.KakaoDto 객체를 추출하여 반환합니다.
 
 

3. MyRSAUtil 클래스 :

public class MyRSAUtil { public static UserResponse.IdTokenDTO verify(String idToken){ String n = "qGWf6RVzV2pM8YqJ6by5exoixIlTvdXDfYj2v7E6xkoYmesAjp_1IYL7rzhpUYqIkWX0P4wOwAsg-Ud8PcMHggfwUNPOcqgSk1hAIHr63zSlG8xatQb17q9LrWny2HWkUVEU30PxxHsLcuzmfhbRx8kOrNfJEirIuqSyWF_OBHeEgBgYjydd_c8vPo7IiH-pijZn4ZouPsEg7wtdIX3-0ZcXXDbFkaDaqClfqmVCLNBhg3DKYDQOoyWXrpFKUXUFuk2FTCqWaQJ0GniO4p_ppkYIf4zhlwUYfXZEhm8cBo6H2EgukntDbTgnoha8kNunTPekxWTDhE5wGAt6YpT4Yw"; String e = "AQAB"; BigInteger bin = new BigInteger(1, Base64.getUrlDecoder().decode(n)); BigInteger bie = new BigInteger(1, Base64.getUrlDecoder().decode(e)); RSAKey rsaKey = new RSAKey.Builder(Base64URL.encode(bin), Base64URL.encode(bie)).build(); try { // 1. 파싱 SignedJWT signedJWT = SignedJWT.parse(idToken); // 2. 검증 RSASSAVerifier verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey()); if(signedJWT.verify(verifier)){ System.out.println("ID Token을 검증하였습니다"); String payload = signedJWT.getPayload().toString(); ObjectMapper om = new ObjectMapper(); UserResponse.IdTokenDTO idTokenDTO = om.readValue(payload, UserResponse.IdTokenDTO.class); return idTokenDTO; }else{ throw new RuntimeException("id토큰 검증 실패"); } } catch (Exception ex) { throw new RuntimeException(ex.getMessage()); } } }
 
클릭하면 MyRSAUtil 클래스 세부 설명을 확인할 수 있습니다.
 
  • RSA 키 구성 요소:
    •  
      String n = "qGWf6RVzV2pM8YqJ6by5exoixIlTvdXDfYj2v7E6xkoYmesAjp_1IYL7rzhpUYqIkWX0P4wOwAsg-Ud8PcMHggfwUNPOcqgSk1hAIHr63zSlG8xatQb17q9LrWny2HWkUVEU30PxxHsLcuzmfhbRx8kOrNfJEirIuqSyWF_OBHeEgBgYjydd_c8vPo7IiH-pijZn4ZouPsEg7wtdIX3-0ZcXXDbFkaDaqClfqmVCLNBhg3DKYDQOoyWXrpFKUXUFuk2FTCqWaQJ0GniO4p_ppkYIf4zhlwUYfXZEhm8cBo6H2EgukntDbTgnoha8kNunTPekxWTDhE5wGAt6YpT4Yw"; String e = "AQAB";
       
    • ne는 RSA 공개 키의 구성 요소입니다. n은 모듈러스이고, e는 공개 지수입니다.
 
  • RSA 키 생성:
    •  
      BigInteger bin = new BigInteger(1, Base64.getUrlDecoder().decode(n)); BigInteger bie = new BigInteger(1, Base64.getUrlDecoder().decode(e)); RSAKey rsaKey = new RSAKey.Builder(Base64URL.encode(bin), Base64URL.encode(bie)).build();
       
    • BigInteger 객체를 사용하여 ne를 디코딩하고, RSA 공개 키를 생성합니다.
    • RSAKey.Builder를 사용하여 RSAKey 객체를 빌드합니다.
 
  • JWT 파싱 및 검증:
    •  
      SignedJWT signedJWT = SignedJWT.parse(idToken); RSASSAVerifier verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey());
       
    • SignedJWT.parse(idToken): 입력된 ID 토큰을 파싱합니다.
    • RSASSAVerifier: RSA 공개 키를 사용하여 JWT 서명을 검증하기 위한 객체입니다.
 
  • 검증 수행:
    •  
      if(signedJWT.verify(verifier)) { System.out.println("ID Token을 검증하였습니다"); String payload = signedJWT.getPayload().toString(); ObjectMapper om = new ObjectMapper(); UserResponse.IdTokenDTO idTokenDTO = om.readValue(payload, UserResponse.IdTokenDTO.class); return idTokenDTO; } else { throw new RuntimeException("id토큰 검증 실패"); }
       
    • signedJWT.verify(verifier): JWT 서명을 검증합니다.
    • 검증에 성공하면 JWT의 페이로드를 가져와 문자열로 변환합니다.
    • ObjectMapper를 사용하여 페이로드 문자열을 IdTokenDTO 객체로 변환합니다.
    • 변환된 IdTokenDTO 객체를 반환합니다.
    • 검증에 실패하면 예외를 발생시킵니다.
 
  • 예외 처리:
    •  
      catch (Exception ex) { throw new RuntimeException(ex.getMessage()); }
       
    • 예외가 발생하면 런타임 예외로 변환하여 처리합니다.
 
 
 
 
 
 
 
 
 

카카오톡 로그인 후 h2-db 확인 결과

 
notion image
 
Share article

moohyun