소셜로그인(카카오)

철준·2022년 12월 26일
0

소셜로그인(Oauth2)

목록 보기
1/2

spring-boot 기반의 gradle 빌드를 사용한 프로젝트이다.
1 oauth2 프레임워크를 사용하지 않고, api 통신으로 구현했다.
(oauth2 프레임워크를 사용하면 더 쉬울지도?)
2 카카오 로그인
3 구글 로그인

1. 소셜로그인 Oauth2

일단 나는 oauth2 프레임워크를 사용하지 않았다.(하던 도중에 알게 되었다...)
전체적인 흐름은 다음과 같다.
사용자가 프론트 서버에서 로그인 -> 카카오 서버에서 인가코드를 줌
-> 프론트 서버가 인가코드를 백엔드 서버로 넘겨 줌 -> 백엔드 서버에서 카카오 서버와 통신해서 엑세스 토큰을 가져오고 -> 엑세스 토큰으로 카카오 사용자 정보를 가져옴 -> 토큰 발급(jwt)

2 카카오 로그인

2-1 카카오 디벨로퍼 아이디 만들고, 내 앱만들기
카카오 디벨롭퍼에 들어가서 애플리케이션 추가를 선택하고, 사업자명은 애플리케이션 아이디와 동일하게 하면 된다.
나중에 사용하게 될 앱키를 여기서 보면 된다.
플랫폼 설정하기에서 나는 웹서비스를 만들기 때문에 Web서비스 등록을 선택하고 도메인 설정을 해준다.(테스트 용도로 localhost:8080도 추가했다.)
그리고 중요한 redirect url을 설정해야하는데 프론트 서버, 프론트 로컬 서버(localhost:3000)를 추가해줬다.

이제 공식문서를 보면서 하나하나 작성해가면 된다.
그전에 다시 흐름을 생각해보고 가면 좋을거 같다.

백엔드 서버 입장에서 프론트에서 인가코드를 주면 받아서 액세스 토큰 요청
-> 사용자정보 가져오고 -> jwt 토큰발급

2-2 공식문서를 보면서 하나하나 작성하기

카카오 디벨로퍼에 가면 많은 오픈 api를 지원하는데 나는 로그인을 하니깐 문서보기를 들어가서 이해하기 부터 다 읽어보면 좋을 것 같다.
왼쪽 바에서 REST API를 선택하면 그냥 다 나와있다...
중요한 코드로 넘어간다.

2-3 일단 컨트롤러에서 url 정하고 서비스로 넘어감
컨트롤러와 서비스가 필요하겠다.

public class SocialUserController {

    private final GoogleUserService googleUserService;
    private final KakaoUserService kakaoUserService;
    
    @ApiOperation(value = "카카오 로그인")
    @GetMapping("/users/login/kakao")
    public ResponseDto<LoginDto> kakaoLogin(@RequestParam String code, HttpServletResponse response) throws IOException {
        return kakaoUserService.kakaoLogin(code, response);
    }
    
    @ApiOperation(value = "구글 로그인")
    @GetMapping("/users/login/google")
    public ResponseDto<LoginDto> googleLogin(@RequestParam String code, HttpServletResponse response) throws IOException {
        return googleUserService.googleLogin(code, response);
    }
}

컨트롤러는 어렵지 않게 만들 수 있었다.
컨트롤러는 하나로 묶고, 서비스부분은 길어질 것을 고려해 두개로 나누었다.
먼저 GetMapping, url 정해주고
리스폰스는 username, email 등을 보여주기 위해 하나 만들었다.
코드는 requestParam으로 받고, httpServletResponse 객체에 Content Type, 응답코드, 메세지 등을 담아서 보내준다

public class KakaoUserService {

    @Value("${kakao.login.admin-key}")
    private String APP_ADMIN_KEY;

    @Value("${kakao.login.client-id}")
    private String CLIENT_ID;

    @Value("${kakao.login.redirect-uri}")
    private String REDIRECT_URI;


    private final UserRepository userRepository;
    private final BCryptPasswordEncoder encoder;

    //카카오로그인
    public ResponseDto<LoginDto> kakaoLogin(String code, HttpServletResponse response) throws IOException {

        // 1. "인가 코드"로 "액세스 토큰" 요청
        String accessToken = getAccessToken(code);

        // 2. "액세스 토큰"으로 "카카오 사용자 정보" 가져오기
        KakaoSocialDto kakaoSocialDto = getKakaoUserInfo(accessToken);

        // 3. 필요시에 회원가입
        User kakaoUser = registerKakaoUser(kakaoSocialDto);

        // 4. 토큰 발급
        kakaoLoginAccess(kakaoUser, response);


        return ResponseDto.success(
                LoginDto.builder()
                        .username(kakaoUser.getUsername())
                        .email(kakaoUser.getEmail())
                        .profileImg(kakaoUser.getProfileImg())
                        .build()
        );

    }

    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", REDIRECT_URI);
        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
        );


        String responseBody = response.getBody();

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode jsonNode = objectMapper.readTree(responseBody);

        return jsonNode.get("access_token").asText();
    }

    private KakaoSocialDto getKakaoUserInfo(String accessToken) throws IOException {
        // 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.GET,
                kakaoUserInfoRequest,
                String.class
        );
        // rt.exchange하면 response에 밑에 것들이 들어간다.

        String responseBody = response.getBody();

        ObjectMapper objectMapper = new ObjectMapper(); // 객체로 만들어줌

        JsonNode jsonNode = objectMapper
                .readTree(responseBody);

        Long kakaoId = jsonNode.get("id").asLong();

        String username = jsonNode
                .get("properties")
                .get("nickname").asText();

        String profileImg = jsonNode
                .get("properties")
                .get("profile_image").asText();

        String email = jsonNode
                .get("kakao_account")
                .get("email").asText();

        return KakaoSocialDto.builder()
                .kakaoId(kakaoId)
                .email(email)
                .username(username)
                .profileImg(profileImg)
                .build();
    }

    private User registerKakaoUser(KakaoSocialDto kakaoSocialDto) {

        // User 정보있는지 확인
        User kakaoUser = userRepository.findByEmail(kakaoSocialDto.getEmail()).orElse(null);

        // User 정보가 없으면 회원가입 시키기
        if (kakaoUser == null) {
            String password = UUID.randomUUID().toString();

            kakaoUser = User.builder()
                    .socialId(kakaoSocialDto.getKakaoId().toString())
                    .username(kakaoSocialDto.getUsername())
                    .password(encoder.encode(password))
                    .email(kakaoSocialDto.getEmail())
                    .social(UserSocialEnum.KAKAO)
                    .profileImg(kakaoSocialDto.getProfileImg())
                    .build();

            userRepository.save(kakaoUser);
        }

        return kakaoUser;

    }

    private void kakaoLoginAccess(User kakaoUser, HttpServletResponse response) {

        UserDetailsImpl userDetails = new UserDetailsImpl(kakaoUser);
        String token = JwtTokenUtils.generateJwtToken(userDetails);
        response.addHeader(AUTH_HEADER, TOKEN_TYPE + " " + token);

    }


    //카카오 회원일 경우, application 연결 끊기 과정 진행
    public void signOutKakaoUser(User user) {

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "KakaoAK " + APP_ADMIN_KEY);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("target_id_type", "user_id");
        body.add("target_id", user.getSocialId().toString());

        HttpEntity<MultiValueMap<String, String>> kakaoUserInfoRequest = new HttpEntity<>(body, headers);
        RestTemplate rt = new RestTemplate();

        ResponseEntity<String> response = rt.exchange(
                "https://kapi.kakao.com/v1/user/unlink",
                HttpMethod.POST,
                kakaoUserInfoRequest,
                String.class
        );
        log.info("회원탈퇴 한 유저의 kakaoId : {}", response.getBody());
    }

}

(너무 길어져서 import, 어노테이션은 다 지워버렸다.)
뚝배기 아프다 그죠~
차근차근 하나씩 보면 이해할 수 있다
4가지로 나누어서 하나씩 메서드를 만들어서 처리했다.
순서대로 인가코드로 액세스 토큰 요청
-> 프론트에서 요청을 쿼리스트링으로 준것을 받아서 카카오 서버로 요청을 보낸다.
여기서 redirect uri가 필요하다 redirect uri는 인증에서 보안 수단으로 사용된다.
uri가 다르면 에러가 뜬다.
보통 도메인 뒤에 /oauth/kakao/callback 등을 사용한다고 한다.(프론트에서 저렇게 요청해주심)

그리고 카카오에 요청을 보내고 받은 값들을 제이슨 형태로 바꿔주고 User가 없다면 DB에 넣어주면 끝

이때 비밀번호는 그냥 랜덤하게 설정했다.

이제 포스트맨으로 테스트를 해보겠다.

https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=클라이언트 아이디&redirect_uri=http://localhost:3000/oauth/kakao/callback

위의 주소로 요청을 보내는것이 인가코드를 받는 과정이다.

요청을 보내면 위와같은 일회성 인가코드가 부여된다 code=~~~

코드 다음 부분을 포스트맨에서

이렇게 넣어주면 끝

CORS 설정도 할 수 있는데 CORS는

헤더에 origin을 추가하고 요청을 보내는 주소를 넣으면 확인할 수 있다.

테스트 확인 끝!

DB에도 잘 들어왔는지 확인해주면 진짜 끝!

구글도 똑같은데 너무 길어서 다음 글로

0개의 댓글