[Spring boot] 카카오 소셜 로그인

다콩이·2022년 6월 3일
9

TIL

목록 보기
1/1
post-thumbnail

Spring boot 소셜 로그인 - 카카오

jwt 토큰 방식을 이용해 카카오 로그인 구현하기

해당 프로젝트에서 회원가입은 Email, Nickname, Password로 하고,
로그인은 Email, Password로 한다.
따라서 RequestDto는 다음과 같이 구성이 된다.

SocialUserInfoDto

@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;
    }
}

컨트롤러는 code를 받아 kakaoUserService로 넘겨준다.

SocailLoginController

@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);
    }


카카오 로그인 과정을 전체적으로 보면 다음과 같다.

KakaoUserService

@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;
    }
}

1. "인가코드"로 "액세스 토큰" 요청

엑세스 토큰을 요청하는 클래스에서는 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 요청을 보내서 돌아온 응답에 있는 액세스 토큰을 파싱한다.

KakaoUserService

	// 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();
    }

2. 토큰으로 카카오 API 호출

위에서 가져온 액세스 토큰으로 카카오 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);
    }

3. 카카오 ID로 회원가입 처리

카카오 정보에 있는 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. 강제 로그인 처리 & response Header에 JWT 토큰 추가
	// 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);
    }

postman에서 카카오 로그인 실행해보기

다음과 같이 정보를 넣는다.

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 형태가 어떻게 되어있는지 볼 수 있다.

1개의 댓글

comment-user-thumbnail
2023년 4월 16일

안녕하세요 잘 보고 갑니다. 궁금한 점이 있는데요 포스트맨에서 Send as Basic Auth header 로 보내면 안돼고 Send client credentials in body 로 보내면 되더라고요 왜 그런지 알 수 있을까요?
그리고 코드를 service부분 controller부분 두 개로 나누어서 작성하셨는데 이러면 실행하면 어떻게 확인 할 수 있을까요? 코드가 잘 작성됐는지? 제가 현재 프론트 부분이 없어서 백엔드로 짜고 테스트 하고 싶은데 postman으로 작성자님이 테스트 한 부분은 굳이 인텔리제이 실행안해도 작동을 하더라고요 . 이부분에 대해서 알려주시면 정말 감사하겠습니다.

답글 달기