코딩하는 털보

21.10.04 TIL 본문

Diary/Today I Learned

21.10.04 TIL

이정인 2021. 10. 4. 21:14

오늘 할 일

  1. 소셜 로그인 기능
  2. 3주차 강의 듣기
  3. 테스트 코드 작성 및 리팩터링

Oauth2 기반 카카오 Login 기능 추가하기

Redirect URL : http://localhost:8080/account/oauth/callback

인가 코드 받기

GET /oauth/authorize?client_id={REST_API_KEY}&redirect_uri={REDIRECT_URI}&response_type=code HTTP/1.1 
Host: kauth.kakao.com
<button id="login-kakao-btn" onclick="location.href='https://kauth.kakao.com/oauth/authorize?client_id="${kakaoKey}"&redirect_uri=http://localhost:8080/account/oauth/callback&response_type=code'">

Redirect URL

HTTP/1.1 302 Found
Content-Length: 0
Location: {REDIRECT_URI}?code={AUTHORIZE_CODE}
@GetMapping("/oauth/callback")
public String oauth2Callback(@RequestParam String code) {
    oauth2Service.login(code);
    return "redirect:/";
}
@Service
@RequiredArgsConstructor
public class Oauth2Service {
    private final AccountRepository accountRepository;
    private final PasswordEncoder passwordEncoder;
    private final ObjectMapper objectMapper;

    private String accessToken;

    public void login(String code) throws JsonProcessingException {
        accessToken = requestAccessToken(code);
        KakaoAccountDto kakaoAccountDto = requestKakaoAccountDto();
        Account account = getKakaoAccountBy(kakaoAccountDto);
        forceLogin(account);
    }

    private String requestAccessToken(String code) throws JsonProcessingException {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", "4afb46d8769cc2922b6999a892fc1646");
        body.add("redirect_uri", "http://localhost:8080/account/oauth/callback");
        body.add("code", code);

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> response = restTemplate.exchange(
                "https://kauth.kakao.com/oauth/token",
                HttpMethod.POST,
                request,
                String.class
        );
        return getAccessTokenFrom(response);
    }

    private String getAccessTokenFrom(ResponseEntity<String> response) throws JsonProcessingException {
        String responseBody = response.getBody();
        JsonNode jsonNode = objectMapper.readTree(responseBody);
        return jsonNode.get("access_token").asText();
    }

    private KakaoAccountDto requestKakaoAccountDto() throws JsonProcessingException {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> response = restTemplate.exchange(
                "https://kapi.kakao.com/v2/user/me",
                HttpMethod.POST,
                request,
                String.class
        );
        return getKakaoAccountDtoFrom(response);
    }

    private KakaoAccountDto getKakaoAccountDtoFrom(ResponseEntity<String> response) throws JsonProcessingException {
        String responseBody = response.getBody();
        JsonNode jsonNode = objectMapper.readTree(responseBody);
        Long id = jsonNode.get("id").asLong();
        String nickname = jsonNode.get("properties")
                .get("nickname").asText();
        return KakaoAccountDto.of(id, nickname);
    }

    private Account getKakaoAccountBy(KakaoAccountDto kakaoAccountDto) {
        Account account = getAccountByKakaoId(kakaoAccountDto.getId());
        if ( account == null ) {
            account = registerKakaoAccount(kakaoAccountDto);
        }
        return account;
    }

    private Account getAccountByKakaoId(Long kakaoId) {
        return accountRepository.findByKakaoId(kakaoId).orElse(null);
    }

    private Account registerKakaoAccount(KakaoAccountDto kakaoAccountDto) {
        kakaoAccountDto.setPassword(passwordEncoder.encode(kakaoAccountDto.getPassword()));
        Account account = Account.from(kakaoAccountDto);
        return accountRepository.save(account);
    }

    private void forceLogin(Account account) {
        UserDetails userDetails = UserDetailsImpl.of(account);
        Authentication authentication =
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
}

카카오 로그인 기능 성공!

여기서 만약 구글로그인, 네이버 로그인 등 다른 Oauth2 서비스를 이용한다면 어떻게 해야할까?
매번 새로운 callback API와 새로운 서비스를 만드는 것은 매우 불편한 일이 될것이다.
만약 callback에서 받는 인가 코드가 어느 호스트에서 온지 알 수 있다면? 다형성을 통해 특정 서비스로 위임할 수 있을 것 같다.
또는 spring boot의 spring-boot-starter-oauth2-client 가 도움이 될 수도?


Test

@ExtendWith(MockitoExtension.class)

  • @Mock
    Mockito는 단위 테스트에서 @Mock 애노테이션을 통해 DI 하려는 가짜 객체를 선언해준다.
    주입되는 가짜 객체는 사용자가 when()메서드를 통해 사용 케이스를 정의할 수 있다.

    //given
    when(productRepository.findById(productId))
                    .thenReturn(Optional.of(product));
  • given()
    given-when-then 패턴에 익숙한 사람들은 여기에서 어색함을 느낄 수 있다.
    가짜 객체의 사용 케이스를 정의하는 작업은 보통 given 구간에 작성하는데, 메서드 이름은 when()인 것이다.
    테스트 코드를 좀 더 자연스럽게 하기 위해서 Mockito.when() 대신 BDDMockito.given()을 사용할 수 있다.

    //given
    given(productRepository.findById(productId)).willReturn(Optional.of(product));

@SpringBootTest

스프링은 어플리케이션의 통합테스트를 지원한다.
@SpringBootTest는 테스트에서 스프링이 동작하도록 해준다.(IoC 컨테이너, JPA 사용 가능)

  • @TestMethodOrder
    테스트 순서를 정의한다.

    • @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
      @Order로 테스트 순서를 정의할 수 있다.
  • @TestInstance
    테스트 클래스 라이프 사이클을 정의한다.

    • @TestInstance(TestInstance.Lifecycle.PER_CLASS)
      클래스 단위로 테스트가 인스턴스화 된다.
  • @Transactional
    통합 테스트에서 실행되는 트랜젝션들이 자동으로 롤백되도록 한다.

@WebMvcTest

스프링에서 MVC를 위한 단위 테스트를 제공한다.

  • @Autowired로 WebMvcTest가 생성해주는 MockMvc를 사용할 수 있다.

    @Autowired
        private MockMvc mvc;
  • @MockBean
    가짜 객체를 만들어서 IoC 컨테이너에 있는 기존 빈을 대체한다. 즉, 테스트하는 컨트롤러에 가짜 객체를 주입하게 할 수 있다.

Comments