Rest Api 기반 카카오, 네이버 JWT 로그인 구현

서규범·2023년 1월 24일
1

이번에 진행하는 팀프로젝트에서 소셜 로그인 기능을 구현해야 했다. 로그인 방식은 JWT 기반 로그인 방식으로, 프론트 측에서 인가코드를 가지고 백엔드 서버로 요청을 보내는 방식으로 구현하였다.

전체적인 로직 흐름

그림 출처: https://velog.io/@padd60/%EB%84%A4%EC%9D%B4%EB%B2%84-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%BF%8C%EC%88%98%EA%B8%B0

로직 흐름을 나타낸 그림 중 가장 깔끔하게 정리되어있어, 해당 그림을 참조하며 구현하였다.
간단하게 설명하면, 프론트 측에서는 직접 카카오 및 네이버에 인가코드를 요청하여 얻어온 뒤에, 해당 인가코드를 가지고 백엔드 측에 로그인 요청을 날리는 방식이다. 백엔드는 이후에 자체적으로 JWT 토큰을 생성해 프론트 측에 넘겨준다.

기본적인 애플리케이션 설정(카카오 디벨로퍼, 네이버 디벨로퍼) 내용은 생략했다.

프론트 측 인가코드 생성

//카카오 인가코드 생성

https://kauth.kakao.com/oauth/authorize?client_id={클라이언트아이디}&redirect_uri={리다이렉트URI}&response_type=code

//네이버 인가코드 생성

https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id={클라이언트아이디}&state={state값}&redirect_uri={리다이렉트URI}

각각 카카오와 네이버 인가코드 생성 코드이다. 해당 코드는 프론트 측에서 이루어져야 하며, 요청을 보내면 카카오, 네이버 서버측으로부터 URL을 통해 인가코드를 받는다. 해당 인가코드를 추후에 백엔드 서버측에 보내주어야 한다.



http://localhost:3000/auth/kakao/callback?code=O0Lmr93wcaMdRqhpVavS0GaLCHnDlNLXbUqyPkKSecQ73cpXi7NULvD2sOtoCUudBxVfogo9dJkAAAGF4_IpqA

위와 같이 요청시에 URL에 코드 값이 담겨오는 것을 확인할 수 있다.

참고) 네이버 디벨로퍼에서 애플리케이션 설정 시 리다이렉트 URI는 프론트 서버의 url로 지정해야한다.

개인 프로젝트시에는 서버사이드 렌더링을 통해 구현했기 때문에, 고려하지 않았던 부분들이, 팀프로젝트시에 문제가되어 꽤나 고생했다.


백엔드 컨트롤러 단 코드

OAuth2KaKaoController.java

@RestController
@RequiredArgsConstructor
public class OAuth2KakaoController {

    private final KakaoService kakaoService;

    @GetMapping("/oauth2/kakao")
    public ResponseEntity<?> kakaoCallback(@RequestParam("code") String code) throws IOException {

        KakaoTokenDto kakaoTokenDto = kakaoService.getKakaoToken(code);
        TokenDto tokenDto = kakaoService.loginWithKakao(kakaoTokenDto);

        TokenResponseDto tokenResponseDto = TokenResponseDto.builder()
                .accessToken(tokenDto.getAccessToken())
                .refreshToken(tokenDto.getRefreshToken())
                .roles(tokenDto.getGrantType())
                .build();
        return ResponseEntity.status(HttpStatus.OK).body(tokenResponseDto);
    }
}

OAuth2NaverController.java

@RestController
@RequiredArgsConstructor
public class OAuth2NaverController {

    private final NaverService naverService;

    @GetMapping("/oauth2/naver")
    public ResponseEntity<?> naverCallback(@RequestParam("code") String code, @RequestParam("state") String state) throws IOException {

        NaverTokenDto naverTokenDto = naverService.getNaverToken(code, state);
        TokenDto tokenDto = naverService.loginWithNaver(naverTokenDto);

        TokenResponseDto tokenResponseDto = TokenResponseDto.builder()
                .accessToken(tokenDto.getAccessToken())
                .refreshToken(tokenDto.getRefreshToken())
                .roles(tokenDto.getGrantType())
                .build();

        return ResponseEntity.status(HttpStatus.OK).body(tokenResponseDto);
    }
}
  • 네이버, 카카오 로그인을 담당하는 컨트롤러이다. 두 컨트롤러의 동작 방식은 동일하다.
    1. getNaverToken 메서드를 통해, 프론트 측에서 넘어온 인가코드로 카카오, 네이버 서버에 토큰(유효성 검증용)을 발급 받는다.
    1. loginWithNaver 메서드를 통해, 발급받은 토큰을 바탕으로 실제 유저 정보를 얻어올 수 있다.
    1. 유저 정보를 얻어온 이후에는, 서버 자체적으로 JWT 토큰을 생성해 최종적으로 반환한다.

백엔드 서비스 단 코드

NaverService.java 중 일부

    public NaverTokenDto getNaverToken(String code, String state) {

        String access_Token = "";
        String refresh_Token = "";
        String reqURL = "https://nid.naver.com/oauth2.0/token";

        String result = null;

        try{
            URL url = new URL(reqURL);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();

            //POST 요청을 위해 기본값이 false인 setDoOutput을 true로
            conn.setRequestMethod("POST");
            conn.setDoOutput(true);

            //POST 요청에 필요로 요구하는 파라미터 스트림을 통해 전송
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
            StringBuilder sb = new StringBuilder();
            sb.append("grant_type=authorization_code");
            sb.append("&client_id=" + client_id);
            sb.append("&client_secret=" + client_secret);
            System.out.println("code = " + code);
            sb.append("&code=" + code);
            sb.append("&state=" + state);
            bw.write(sb.toString());
            bw.flush();

            //응답 코드가 200이면 성공
            int responseCode = conn.getResponseCode();
            log.info("responseCode={}", responseCode);

            //요청을 통해 얻은 JSON타입의 Response 메세지 Read
            BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = "";
            result = "";

            while ((line = br.readLine()) != null) {
                result += line;
            }

            log.info("response body={}", result);

            // Gson 라이브러리에 포함된 클래스로 JSON파싱
            JsonParser parser = new JsonParser();
            JsonElement element = parser.parse(result);

            access_Token = element.getAsJsonObject().get("access_token").getAsString();
            refresh_Token = element.getAsJsonObject().get("refresh_token").getAsString();

            log.info("네이버 access token={}", access_Token);
            log.info("네이버 refresh token={}", refresh_Token);

            br.close();
            bw.close();
            conn.disconnect();

        }catch (Exception e){
            e.printStackTrace();
        }

        NaverTokenDto naverTokenDto = NaverTokenDto.builder()
                .accessToken(access_Token)
                .refreshToken(refresh_Token)
                .build();

        return naverTokenDto;
    }

NaverService.java 중 일부

public TokenDto loginWithNaver(NaverTokenDto naverTokenDto) throws IOException {

        //회원 정보 요청 url
        String reqURL = "https://openapi.naver.com/v1/nid/me";

        //accessToken 통한 사용자 정보 조회
        URL url = new URL(reqURL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();

        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setRequestProperty("Authorization", "Bearer " + naverTokenDto.getAccessToken()); //전송할 header 작성, access_token전송

        //결과 코드가 200이면 성공
        int responseCode = conn.getResponseCode();
        log.info("responseCode={}", responseCode);

        //요청을 통해 얻은 JSON타입의 Response 메세지 Read
        BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line = "";
        String result = "";

        while ((line = br.readLine()) != null) {
            result += line;
        }

        log.info("response body={}", result);

        //Gson 라이브러리로 JSON파싱
        JsonElement element = JsonParser.parseString(result);

        String email = element.getAsJsonObject().get("response").getAsJsonObject().get("email").getAsString();

        //DB에 해당 이메일 없을 경우 회원 가입 로직 실행
        if (!memberRepository.existsByEmail(email)) {

            Member member = Member.builder()
                    .email(email)
                    .password("")
                    .roles(Collections.singletonList("ROLE_USER"))
                    .build();

            memberRepository.save(member);

            TokenDto tokenDto = jwtTokenProvider.createToken(email, member.getRoles());
            tokenDto.setGrantType(member.getRoles().get(0));
            jwtService.saveRefreshToken(tokenDto);

            return tokenDto;

        } else {

            Optional<Member> member = memberRepository.findByEmail(email);

            //DB에 해당 이메일 회원 정보 있을 경우 jwt token 생성해서 리턴
            TokenDto tokenDto = jwtTokenProvider.createToken(email, member.get().getRoles());
            tokenDto.setGrantType(member.get().getRoles().get(0));
            jwtService.saveRefreshToken(tokenDto);

            return tokenDto;

        }

    }
  • 이전에 getNaverToken 메서드를 통해 반환된 토큰을 가지고 네이버 서버에 유저 정보를 요청하는 로직이다.
  • 유저 정보를 반환 받은 이후에는, 해당 유저 정보를 가지고 서버 자체에서 JWT 토큰을 발행한다. 이 때, 추가적으로 기존에 가입된 회원이 아닌 경우에 자동으로 회원가입하는 로직까지 추가해주었다.


카카오 로그인 같은 경우에도 전체적인 흐름과 로직은 똑같이 구현하였다. 전체 소스코드는 아래 깃허브에서 확인할 수 있다.

https://github.com/Att-ies/backend

profile
하려 하자

0개의 댓글