본문 바로가기
Spring/Spring Boot

[Spring boot - OAuth2] Naver Login 구현 #2

by seoyamin 2023. 3. 4.

오늘은 인가 코드를 이용해 사용자 정보를 받아오는 코드를 작성해볼 것이다.

 

[참고] Naver Developers

https://developers.naver.com/docs/login/devguide/devguide.md#3-1-3-%ED%95%84%EC%88%98-%ED%95%AD%EB%AA%A9-%ED%99%95%EC%9D%B8

 

네이버 로그인 개발가이드 - LOGIN

네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디

developers.naver.com

 

 

1. 환경설정

(1) build.gradle

 

(2) SecurityConfig

 

(3) application.yml

해당 정보는 노출되면 안된다. 나는 별도의 application-secret.yml 파일에 작성했다.

spring:
  security:
    oauth2:
      client:
        registration:
          naver:
            client-id: 내 어플리케이션 CLIENT ID
            client-secret: 내 어플리케이션 CLIENT SECRET
            redirect-uri: http://localhost:8080/oauth/naver
            authorization-grant-type: authorization_code
            scope:
              - name
              - email
        provider:
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response

 

 

2. OAuthResponseDTO

우리 서비스는 회원가입 이후 추가 정보 입력이 필수여서 OAuth 이후 임시 DTO를 만들어서 리턴하기로 했다.

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor
public class OAuthResponseDTO {
    private Long userId;
    private String accessToken;
    private String refreshToken;
    private Boolean isMember;

    public OAuthResponseDTO(Long userId, String accessToken, String refreshToken, Boolean isMember) {
        this.userId = userId;
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        this.isMember = isMember;
    }
}

 

 

3. OAuthService

카카오와 네이버 로그인 모두 Controller에서 동일한 signup 메소드를 호출한다.

OAuth2User를 이용해서 한번에 처리하는 방식이 아닌, 직접 end point마다 response를 받고, request를 보내는 코드이다. HTTP 관련 처리는 RestTemplate를 이용했다.

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zonebug.debugging.config.jwt.TokenProvider;
import com.zonebug.debugging.domain.user.User;
import com.zonebug.debugging.domain.user.UserRepository;
import com.zonebug.debugging.dto.response.OAuthResponseDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class OAuthService {

    private final UserRepository userRepository;
    private final CustomUserDetailsService customUserDetailsService;
    private final TokenProvider tokenProvider;
    private final PasswordEncoder passwordEncoder;

    @Value("${spring.security.oauth2.client.registration.kakao.client-id}")
    private String KAKAO_CLIENT_ID;

    @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
    private String KAKAO_REDIRECT_URI;

    @Value("${spring.security.oauth2.client.registration.naver.client-id}")
    private String NAVER_CLIENT_ID;

    @Value("${spring.security.oauth2.client.registration.naver.client-secret}")
    private String NAVER_CLIENT_SECRET;

    @Value("${spring.security.oauth2.client.registration.naver.redirect-uri")
    private String NAVER_REDIRECT_URI;

    @Value("${spring.security.oauth2.client.provider.naver.user-info-uri")
    private String NAVER_USER_INFO_URI;


    private String TYPE;
    private String CLIENT_ID;
    private String REDIRECT_URI;


    public OAuthResponseDTO signUp(String code, String type) {
        TYPE = type;

        if(type == "kakao") {
            CLIENT_ID = KAKAO_CLIENT_ID;
            REDIRECT_URI = KAKAO_REDIRECT_URI;
        } else {
            CLIENT_ID = NAVER_CLIENT_ID;
            REDIRECT_URI = NAVER_REDIRECT_URI;
        }

        // "인가 코드"로 "accessToken" 요청
        String kakaoAccessToken = getAccessToken(code);

        // 토큰으로 카카오 API 호출 (이메일 정보 가져오기)
        String email = getUserInfo(kakaoAccessToken);

        // DB정보 확인 -> 없으면 DB에 저장
        User user = registerUserIfNeed(email);

        // JWT 토큰 리턴 & 로그인 처리
        String jwtToken = usersAuthorizationInput(user);

        // 회원여부 닉네임으로 확인
        Boolean isMember = checkIsMember(user);

        return new OAuthResponseDTO(user.getId(), jwtToken, user.getRefreshToken(), isMember);
    }


    private String getAccessToken(String code) {

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

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "authorization_code");
        body.add("client_id", CLIENT_ID);
        if(TYPE == "naver") body.add("client_secret", NAVER_CLIENT_SECRET);
        body.add("redirect_uri", REDIRECT_URI);
        body.add("code", code);


        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
        RestTemplate rt = new RestTemplate();
        ResponseEntity<String> response;

        if(TYPE == "kakao") {
            response = rt.exchange(
                    "https://kauth.kakao.com/oauth/token",
                    HttpMethod.POST,
                    request,
                    String.class
            );
        } else {
            response = rt.exchange(
                    "https://nid.naver.com/oauth2.0/token",
                    HttpMethod.POST,
                    request,
                    String.class
            );
        }

        // HTTP 응답 (JSON) -> 액세스 토큰 파싱
        String responseBody = response.getBody();
        try{
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode jsonNode = objectMapper.readTree(responseBody);
            System.out.println(jsonNode);
            return jsonNode.get("access_token").asText();
        } catch (Exception e) {
            System.out.println("in exception");
            return e.toString();
        }
    }

    private String getUserInfo(String accessToken) {
        // HTTP Header 생성
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer " + accessToken);
        headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HTTP 요청 보내기 - Post 방식
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);
        RestTemplate rt = new RestTemplate();
        ResponseEntity<String> response;

        if(TYPE == "kakao") {
            response = rt.exchange(
                    "https://kapi.kakao.com/v2/user/me",
                    HttpMethod.POST,
                    request,
                    String.class
            );
        } else {
            response = rt.exchange(
                    "https://openapi.naver.com/v1/nid/me",
                    HttpMethod.POST,
                    request,
                    String.class
            );
        }

        // responseBody 정보 꺼내기
        String responseBody = response.getBody();
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode jsonNode = objectMapper.readTree(responseBody);

            if(TYPE == "kakao") {
                return jsonNode.get("kakao_account").get("email").asText();
            } else {
                System.out.println(jsonNode.get("response"));
                return jsonNode.get("response").get("email").asText();
            }


        } catch (Exception e) {
            return e.toString();
        }
    }

    // DB정보 확인 -> 없으면 DB에 저장
    private User registerUserIfNeed(String email) {
        // DB에 중복된 이메일 있는지 확인
        Optional<User> user = userRepository.findByEmail(email);

        if (user.isEmpty()) {
            // DB에 정보 등록
            User newUser = User.builder()
                    .email(email)
                    .password(passwordEncoder.encode(TYPE))
                    .type(TYPE)
                    .build();
            userRepository.save(newUser);
        }

        return userRepository.findByEmail(email).get();
    }

    private String usersAuthorizationInput(User user) {

        UserDetails userDetails = customUserDetailsService.loadUserByUsername(user.getEmail());
        Authentication authentication = new UsernamePasswordAuthenticationToken(
                userDetails,
                "",
                userDetails.getAuthorities()
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String accessToken = tokenProvider.createAccessToken(authentication);
        String refreshToken = tokenProvider.createRefreshToken(authentication);

        user.setRefreshToken(refreshToken);
        userRepository.save(user);
        return accessToken;
    }

    private Boolean checkIsMember(User user) {
        return user.getNickname() != null;
    }
}

 

 

4. OAuthController

카카오, 네이버 모두 OAuthService의 signUp 메소드를 호출하는데, 그 타입을 함께 담아준다.

* 타입 : 카카오인지 네이버인지

import com.zonebug.debugging.dto.response.OAuthResponseDTO;
import com.zonebug.debugging.service.OAuthService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/oauth")
public class OAuthController {

    private final OAuthService oauthService;

    public OAuthController(OAuthService oauthService) {
        this.oauthService = oauthService;
    }

    @GetMapping("/kakao")
    public ResponseEntity<OAuthResponseDTO> signUp(@Valid @RequestParam(name = "code") String code) {
        return ResponseEntity.ok(oauthService.signUp(code, "kakao"));
    }

    @GetMapping("/naver")
    public ResponseEntity<OAuthResponseDTO> signup(@Valid @RequestParam String code, @RequestParam String state) {
        return ResponseEntity.ok(oauthService.signUp(code, "naver"));
    }
}