내일배움캠프 Spring 38일차 TIL

Skadi·2024년 2월 16일
0

스프링 심화

1. OAuth를 이용한 로그인 구현

  • OAuth를 이용한 카카오 로그인을 위해서는 카카오 개발자 센터에서 카카오계정(이메일) 설정을 해야한다. 이미지 출처와 같은 곳 문서에서 확인할 수 있다.
<button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id={REST_API_KEY}">
    카카오로 로그인하기
</button>
  • 카카오 로그인 버튼을 누르면 해당 페이지로 이동
  • 해당 카카오 로그인 페이지에서 '동의하고 계속하기'를 클릭하면 설정한 Redirect URI로 인가코드 전달
  • 카카오 인가코드 처리 방법(@RequestParam에 인가코드가 담겨온다.)
@GetMapping("/user/kakao/callback") 
public String kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException { 
  // code: 카카오 서버로부터 받은 인가코드 Service 전달 후 인증 처리 및 JWT 반환

  String token = kakaoService.kakaoLogin(code); 
  // Cookie 생성 및 직접 브라우저에 Set 
  Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER, token.substring(7)); 
  cookie.setPath("/"); 
  response.addCookie(cookie); 
  return "redirect:/"; 
}
  • 인가 코드를 가지고 카카오 로그인 처리 => Service
  • 로그인 성공 시 "/" redirect => Controller
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); 
 
        return null; 
    } 
}
  • RestTemplate 사용을 위한 설정
@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(); 
    } 
}
  • 액세스 토큰 카카오에 요청
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", "{본인의 REST API 키}"); 
    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); 
}
  • 카카오 정보 JSON 형태
{
  "id": 1632335751, 
  "properties": { 
    "nickname": "르탄이", 
    "profile_image": "http://k.kakaocdn.net/...jpg", 
    "thumbnail_image": "http://k.kakaocdn.net/...jpg" 
  }, 
  "kakao_account": { 
    "profile_needs_agreement": false, 
    "profile": { 
      "nickname": "르탄이", 
      "thumbnail_image_url": "http://k.kakaocdn.net/...jpg", 
      "profile_image_url": "http://k.kakaocdn.net/...jpg" 
  }, 
  "has_email": true, 
  "email_needs_agreement": false, 
  "is_email_valid": true, 
  "is_email_verified": true, 
  "email": "letan@sparta.com" 
  } 
}
  • 가져온 사용자 정보로 회원가입하기(DB에 kakaoId를 가진 회원이 없을 경우에만 신규로 회원가입)
public String kakaoLogin(String code, HttpServletResponse response) 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 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; 
}
  • 기존 카카오 회원으로 로그인 한 사람이 내가 만든 기존 로그인 폼에 데이터를 입력할 경우를 생각
    1. 기존에 내가 만든 로그인 폼으로 로그인을 하던 사람의 이메일과 카카오에서 받은 이메일 정보가 같은지 확인
    1. 같을 경우 이미 회원가입은 되어있고 비어있는 KakaoId를 채움
    2. 같은 이메일이 없을 경우 회원가입 시켜줌

이미지 출처 : 카카오 개발자센터 문서(https://developers.kakao.com/docs/latest/ko/kakaologin/common)

0개의 댓글