[기술 정리] 21. 카카오 로그인 구현(3) - SSR

KangHo Lee's avatar
Dec 11, 2024
[기술 정리] 21.  카카오 로그인 구현(3) - SSR

User

@NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "user_tb") @Getter @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @Column(unique = true, nullable = false) private String username; @Column(nullable = false) private String password; private String email; @Enumerated(EnumType.STRING) private ProviderEnum provider; @CreationTimestamp private Timestamp createdAt; @Builder public User(Integer id, String username, String password, String email, ProviderEnum provider, Timestamp createdAt) { this.id = id; this.username = username; this.password = password; this.email = email; this.provider = provider; this.createdAt = createdAt; } }

UserController

@RequiredArgsConstructor @Controller public class UserController { private final UserService userService; private final HttpSession session; @GetMapping("/oauth") public String oauth(@RequestParam("code") String code) { User sessionUser = userService.카카오로그인(code); session.setAttribute("sessionUser", sessionUser); return "redirect:/"; } // 카카오 로그인 메서드 외에는 생략 }

UserRepository

// JpaRepository 상속하면 기본 CRUD 만들어줍니다. public interface UserRepository extends JpaRepository<User, Integer> { // 추가적으로 필요한 메서드는 개발자가 작성 @Query("SELECT u FROM User u WHERE u.username = :username") Optional<User> findByUsername(@Param("username") String username); }

UserService(로그인 핵심 로직)

@RequiredArgsConstructor @Transactional(readOnly = true) @Service public class UserService { private final UserRepository userRepository; 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(); } }
  • String username = "kakao_" + idTokenDTO.getSub();
    • id 토큰의 sub는 카카오 로그인 내에서는 중복되지 않습니다 → username으로 활용
  • password(UUID.randomUUID().toString())
    • 사용할 일이 없는 데이터입니다. 중복만 주의하면 됩니다.
  • userRepository.save(user)
    • JpaRepository에 정의되어 있습니다.
    • <S extends T> S save(S entity)
  • provider(ProviderEnum.KAKAO)
    • Enum 클래스를 활용해 provider 분류를 했습니다.
public enum ProviderEnum { LOCAL, KAKAO, APPLE...; }
💡
userPS → 영속성 컨텍스트(Persistence Context)임을 표시
userOP → Optional임을 표시

카카오 로그인 요청

MyHttpUtil
public class MyHttpUtil { // 카카오 로그인 요청 메서드 public static UserResponse.KakaoDTO post(String code){ String redirectUri = "http://localhost:8080/oauth"; String clientId = "730a8ec7e91f04ab2647991ef34f1f81"; 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; } }
  • RestTemplate
    • Spring에서 제공하는 클래스 중 하나로, RESTful 웹 서비스와의 통신을 쉽게 구현할 수 있도록 도와줍니다.
    • 이를 통해 HTTP 요청(예: GET, POST, PUT, DELETE 등)을 간단하게 보낼 수 있습니다.
  • UserResponse.KakaoDTO
    • access_token 정보와 id_token 정보가 담겨 있습니다.

UserResponse

public class UserResponse { @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; } @Data public static class KakaoDTO { @JsonProperty("access_token") private String accessToken; @JsonProperty("id_token") private String idToken; } }
  • @JsonProperty("access_token")
    • JSON 문자열의 access_token 정보를 accessToken에 넣어줍니다.
💡
자바는 언더바(_)를 쓰지 않는 것이 좋습니다.

Id token 검증

MyRSAUtil
// implementation 'com.nimbusds:nimbus-jose-jwt:9.31' 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()); } } }
  • UserResponse.IdTokenDTO
    • sub(username으로 사용), nickname을 포함한 여러 정보가 담겨 있습니다.
💡
위 과정은 SSR에 맞춰 작성한 코드이고 토큰을 활용할 경우 토큰 기반 인증(Token-Based Authentication)으로 무상태(stateless) 애플리케이션(CSR 방식)에 많이 씁니다.
 
Share article

devleekangho