카카오 소셜로그인 - access token 받은 후 유저 정보 DB 저장

gnoesnooj·2022년 5월 30일
0

배경 + 미리쓰는 느낀점

  • 더모티 프로젝트의 로그인 기능을 소셜 로그인 기능으로 대체하기로 했다.
  • 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 라는 책이 있는데, 이를 보고 OAuth2를 이용해서 구현해주는 방법도 있지만, 이걸 사용하진 않았다. (이상하게 리다이렉션 횟수가 너무 많습니다.. 라는 Error가 발생했다. 지금와서 생각해보니 아래의 문제상황에서 코드 작성을 시도했던지라 해결 방법 또한 제대로 떠올리지 못했던 것 같은데, 어쨌든 도저히 문제해결이 되질 않아서 다른 방법을 찾아 나섰다.)
  • 초기 흐름 이해를 제대로 안하고, 다른 사람들이 구현해놓은 코드들을 보고 어떻게 끼워맞춰서 코드를 빨리 작성해야 겠다는 마음만 앞서서 시간을 너무 낭비했다. 천천히 kakao developers 에서 코드 흐름을 이해하고, 필요한 request, response 들을 하나씩 뜯어서 이해하다보니, 그제서야 다른 사람들의 코드들도 제대로 눈에 들어오고, 내가 어떤걸 가져다쓰고 버려야할지 정리가 되기 시작했다.
  • 내가 짰던 흐름은
    1. 사용자가 로그인 시도 -> 카카오로부터 redirect uri로 인가 코드 받기
    2. 받은 코드로 access token 얻기
    3. access token 으로 kakao 에서 사용자 정보 가져오기
    4. 가져온 정보로 로그인 + 토큰 발급 진행 (아직 JWT가 미구현되어있어서, 토큰발급은 X)
    (+) 코드를 작성하던 중, 프론트로부터 access token 을 줄 수 있다는 내용이 나왔지만, 이미 개발을 하고 있기도 했고, access token 없이는 어차피 postman 등 test가 진행이 되질 않아서 access token 얻는 것도 구현을 했다.

코드

    1. build.gradle
implementation group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.3'
implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1'
    1. User : 내가 필요한 정보는 email, nickname, role 이다.

import com.daily.themoti.user.constant.UserRole;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@NoArgsConstructor
@Getter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String email;

    @Column
    private String username;

    @Enumerated(EnumType.STRING)
    private UserRole role;

    @Builder
    public User(String username, String email, UserRole role){
        this.username = username;
        this.email = email;
        this.role = role;
    }
}
    1. UserRole
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum UserRole {

    USER("USER", "사용자"),
    GUEST("GUEST", "손님");
    private final String key;
    private final String title;

}
    1. UserController

import com.daily.themoti.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/login/{token}")
    public void login(@PathVariable String token){
        userService.loginWithAccessToken(token);
    }

    @GetMapping("/user/kakao/oauth") // 카카오 어플리케이션에서 설정해준 redirect uri이다. code를 가져온 후 access_token 을 리턴
    public String getCode(@RequestParam String code){
        return userService.getAccessToken(code);
    }
}
    1. UserRepository : email 를 이용해서 user 가 존재하는지 여부를 판단하기 위해 findByEmail 을 작성했다.

import com.daily.themoti.user.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email);
}
    1. UserService, UserServiceImpl

UserService


public interface UserService {

    void loginWithAccessToken(String token);

    String getAccessToken(String code);
}

UserServiceImpl


import com.daily.themoti.smokearea.exception.ParseFailedException;
import com.daily.themoti.user.dto.KakaoUserInfo;
import com.daily.themoti.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService{

    private final UserRepository userRepository;

    @Value("${apikey.kakao.rest.api.key}")
    private String KAKAO_REST_API_KEY;

    @Override
    public String getAccessToken(String code){
        String kakaoURL = "https://kauth.kakao.com/oauth/token";
        String access_token = "";
        String refresh_token = "";
        try{
            URL url = new URL(kakaoURL);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("POST");
            connection.setDoOutput(true);

            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream()));
            StringBuilder sb = new StringBuilder();
            sb.append("grant_type=authorization_code");
            sb.append("&client_id=" + KAKAO_REST_API_KEY);
            sb.append("&redirect_uri=http://localhost:8080/user/kakao/oauth");
            sb.append("&code=" + code);
            bw.write(sb.toString());
            bw.flush();

            int responseCode = connection.getResponseCode();
            System.out.println("responseCode : " + responseCode);

            BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            String line = "";
            String result = "";

            while((line = br.readLine()) != null){
                result += line;
            }
            System.out.println("response body = " + result);

            JSONObject object = parseJSON(result);
            access_token = (String) object.get("access_token");
            refresh_token = (String) object.get("refresh_token");

        } catch(IOException e){
            e.printStackTrace();
        }
        return access_token;
    }

    @Override
    public void loginWithAccessToken(String token) {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + token);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        RestTemplate rt = new RestTemplate();
        HttpEntity<MultiValueMap<String, String>> kakaoProfileRequest = new HttpEntity<>(headers);
        rt.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
        rt.setErrorHandler(new DefaultResponseErrorHandler() {
            public boolean hasError(ClientHttpResponse response) throws IOException {
                HttpStatus statusCode = response.getStatusCode();
                return statusCode.series() == HttpStatus.Series.SERVER_ERROR;
            }
        });

        ResponseEntity<String> response = rt.exchange(
                "https://kapi.kakao.com/v2/user/me",
                HttpMethod.POST,
                kakaoProfileRequest,
                String.class
        );

        String str = response.getBody();
        JSONObject kakao_response = parseJSON(str);
        JSONObject kakao_account = (JSONObject) kakao_response.get("kakao_account");
        JSONObject profile = (JSONObject) kakao_account.get("profile");
        String email = (String) kakao_account.get("email");
        String nickname = (String) profile.get("nickname");

        KakaoUserInfo kakaoUserInfo = new KakaoUserInfo(email, nickname);
        if(!userRepository.findByEmail(email).isPresent()){
            userRepository.save(kakaoUserInfo.toEntity());
        }
    }

    private JSONObject parseJSON(String result){
        try {
            JSONParser jsonParser = new JSONParser();
            JSONObject jsonObject = (JSONObject) jsonParser.parse(result);

            return jsonObject;
        } catch(ParseException e){
            throw new ParseFailedException();
        }
    }
}

참조

UserService - access token을 통해서 유저 정보를 얻어오는 부분) https://velog.io/@kimujin99/Spring-React-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EC%86%8C%EC%85%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-JWT-2

profile
누구나 믿을 수 있는 개발자가 되자 !

0개의 댓글