해당 프로젝트에서 회원가입은 Email, Nickname, Password로 하고,
로그인은 Email, Password로 한다.
따라서 RequestDto는 다음과 같이 구성이 된다.
@Getter
@NoArgsConstructor
public class SocialUserInfoDto {
private Long id;
private String nickname;
private String email;
public SocialUserInfoDto(Long id, String nickname, String email) {
this.id = id;
this.nickname = nickname;
this.email = email;
}
}
@RestController
@RequiredArgsConstructor
public class SocialLoginController {
private final KakaoUserService kakaoUserService;
// 카카오 로그인
@GetMapping("/user/kakao/callback")
public SocialUserInfoDto kakaoLogin(@RequestParam String code, HttpServletResponse response) throws JsonProcessingException {
return kakaoUserService.kakaoLogin(code, response);
}
@Service
@RequiredArgsConstructor
public class KakaoUserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public SocialUserInfoDto kakaoLogin(String code, HttpServletResponse response) throws JsonProcessingException {
// 1. "인가 코드"로 "액세스 토큰" 요청
String accessToken = getAccessToken(code);
// 2. 토큰으로 카카오 API 호출
SocialUserInfoDto kakaoUserInfo = getKakaoUserInfo(accessToken);
// 3. 카카오ID로 회원가입 처리
User kakaoUser = registerKakaoUserIfNeed(kakaoUserInfo);
// 4. 강제 로그인 처리
Authentication authentication = forceLogin(kakaoUser);
// 5. response Header에 JWT 토큰 추가
kakaoUsersAuthorizationInput(authentication, response);
return kakaoUserInfo;
}
}
엑세스 토큰을 요청하는 클래스에서는 HTTP Header, Body에 필요한 정보들을 담아준다.
client_id에는 kakao developers에서 제공된 REST API키를 넣는다.
redirect_uri에는 redirect할 callback uri를 넣어준다.
지금은 http://localhost:8080 에서 실행해 볼 것이기 때문에 http://localhost:8080/user/kakao/callback 을 넣어주고, 이외에 다른 곳에서 실행할 때에는 코드를 바꿔서 넣어줘야 한다.
물론 해당 callback url을 kakao developers에 등록해 놓아야 한다.
HTTP 요청을 보내서 돌아온 응답에 있는 액세스 토큰을 파싱한다.
// 1. "인가 코드"로 "액세스 토큰" 요청
private String getAccessToken(String code) throws JsonProcessingException {
// 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", CLIENT_ID);
body.add("redirect_uri", "http://localhost:8080/user/kakao/callback");
body.add("code", code);
// HTTP 요청 보내기
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(body, headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kauth.kakao.com/oauth/token",
HttpMethod.POST,
kakaoTokenRequest,
String.class
);
// HTTP 응답 (JSON) -> 액세스 토큰 파싱
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);
return jsonNode.get("access_token").asText();
}
위에서 가져온 액세스 토큰으로 카카오 API를 호출한다.
responseBody에 있는 정보를 꺼내 jsonNode에서 id, nickname, email을 꺼낸다.
// 2. 토큰으로 카카오 API 호출
private SocialUserInfoDto getKakaoUserInfo(String accessToken) throws JsonProcessingException {
// HTTP Header 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HTTP 요청 보내기
HttpEntity<MultiValueMap<String, String>> kakaoUserInfoRequest = new HttpEntity<>(headers);
RestTemplate rt = new RestTemplate();
ResponseEntity<String> response = rt.exchange(
"https://kapi.kakao.com/v2/user/me",
HttpMethod.POST,
kakaoUserInfoRequest,
String.class
);
// responseBody에 있는 정보를 꺼냄
String responseBody = response.getBody();
ObjectMapper objectMapper = new ObjectMapper();
JsonNode jsonNode = objectMapper.readTree(responseBody);
Long id = jsonNode.get("id").asLong();
String email = jsonNode.get("kakao_account").get("email").asText();
String nickname = jsonNode.get("properties")
.get("nickname").asText();
return new SocialUserInfoDto(id, nickname, email);
}
카카오 정보에 있는 email, nickname을 가져오고,
만약 동일한 email을 가진 user가 없다면 회원가입,
있다면 유저정보를 그대로 리턴한다.
// 3. 카카오ID로 회원가입 처리
private User registerKakaoUserIfNeed (SocialUserInfoDto kakaoUserInfo) {
// DB 에 중복된 email이 있는지 확인
String kakaoEmail = kakaoUserInfo.getEmail();
String nickname = kakaoUserInfo.getNickname();
User kakaoUser = userRepository.findByUserEmail(kakaoEmail)
.orElse(null);
if (kakaoUser == null) {
// 회원가입
// password: random UUID
String password = UUID.randomUUID().toString();
String encodedPassword = passwordEncoder.encode(password);
String profile = "https://ossack.s3.ap-northeast-2.amazonaws.com/basicprofile.png";
kakaoUser = new User(kakaoEmail, nickname, profile, encodedPassword);
userRepository.save(kakaoUser);
}
return kakaoUser;
}
// 4. 강제 로그인 처리
private Authentication forceLogin(User kakaoUser) {
UserDetails userDetails = new UserDetailsImpl(kakaoUser);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
// 5. response Header에 JWT 토큰 추가
private void kakaoUsersAuthorizationInput(Authentication authentication, HttpServletResponse response) {
// response header에 token 추가
UserDetailsImpl userDetailsImpl = ((UserDetailsImpl) authentication.getPrincipal());
String token = JwtTokenUtils.generateJwtToken(userDetailsImpl);
response.addHeader("Authorization", "BEARER" + " " + token);
}
다음과 같이 정보를 넣는다.
Type : OAuth 2.0
Header: Bearer
Token Name : 아무 이름이나 설정
Callback URL : http://localhost:8080/kakao/user/callback
Auth URL : https://kauth.kakao.com/oauth/authorize
Access Token URL : https://kauth.kakao.com/oauth/token
Client ID : kakao developers에서 발급 받은 REST API 키
그리고 Get New Access Token을 클릭해 카카오 로그인을 하면 Token을 발급받을 수 있다.
발급받은 토큰을 Use Token을 통해 넣은 후, https://kapi.kakao.com/v2/user/me 로 요청하면 다음과 같이 json 형태가 어떻게 되어있는지 볼 수 있다.
안녕하세요 잘 보고 갑니다. 궁금한 점이 있는데요 포스트맨에서 Send as Basic Auth header 로 보내면 안돼고 Send client credentials in body 로 보내면 되더라고요 왜 그런지 알 수 있을까요?
그리고 코드를 service부분 controller부분 두 개로 나누어서 작성하셨는데 이러면 실행하면 어떻게 확인 할 수 있을까요? 코드가 잘 작성됐는지? 제가 현재 프론트 부분이 없어서 백엔드로 짜고 테스트 하고 싶은데 postman으로 작성자님이 테스트 한 부분은 굳이 인텔리제이 실행안해도 작동을 하더라고요 . 이부분에 대해서 알려주시면 정말 감사하겠습니다.