일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- annotation processor
- 자바할래
- docker
- 함수형 인터페이스
- 익명 클래스
- System.err
- throwable
- 로컬 클래스
- System.out
- 상속
- 프리미티브 타입
- 브릿지 메소드
- 합병 정렬
- raw 타입
- junit 5
- 람다식
- github api
- System.in
- 스파르타코딩클럽
- 항해99
- 제네릭 타입
- 바운디드 타입
- 제네릭 와일드 카드
- yield
- 정렬
- 접근지시자
- 자바스터디
- Study Halle
- Switch Expressions
- auto.create.topics.enable
- Today
- Total
코딩하는 털보
21.10.04 TIL 본문
오늘 할 일
- 소셜 로그인 기능
- 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로 테스트 순서를 정의할 수 있다.
- @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance
테스트 클래스 라이프 사이클을 정의한다.- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
클래스 단위로 테스트가 인스턴스화 된다.
- @TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Transactional
통합 테스트에서 실행되는 트랜젝션들이 자동으로 롤백되도록 한다.
@WebMvcTest
스프링에서 MVC를 위한 단위 테스트를 제공한다.
@Autowired로 WebMvcTest가 생성해주는 MockMvc를 사용할 수 있다.
@Autowired private MockMvc mvc;
@MockBean
가짜 객체를 만들어서 IoC 컨테이너에 있는 기존 빈을 대체한다. 즉, 테스트하는 컨트롤러에 가짜 객체를 주입하게 할 수 있다.