[Spring/SpringBoot] 네이버 로그인 API 구현하기 (New Ver✨)

Dev_ch·2023년 8월 7일
1

📚 이전 포스팅
1. 구글 로그인 구현하기
2. 카카오 로그인 구현하기

이전에 소셜로그인과 관련된 포스팅을 했었는데, 예전에 작성했던 코드이다보니 가독성이 좋지 않고 객체지향 설계원칙에도 부족한면이 있어서 네이버 로그인을 구현하면서 새롭게 리팩터링한 코드를 포스팅 하려 한다.

소셜 로그인을 REST 형식으로 구현하는 경우는 구글, 카카오, 네이버 전부 비슷하기 때문에 하나의 포스팅으로 충분히 숙지하면 여러가지 플랫폼의 소셜 로그인을 구현할 수 있다.

이제 네이버 로그인을 구현해보도록 하자 🙇‍♂️


준비사항

네이버 developers 메인 홈페이지

네이버 devleopers 메인 홈페이지에서 애플리케이션 등록을 해줘야한다.

사용 API에서 네이버 로그인을 선택하면 제공 정보를 선택하는 창이 뜨는데, 여기서 자신이 개발하고 있는 서비스에 맞춰서 선택해주자. 해당 포스팅에서는 회원이름, 연락처 이메일 주소를 선택하였다.

서비스 URL은 로컬 개발이라면 로컬 주소를 넣어주면 되고 ec2에 배포를 위해선 ec2 주소를 넣어주도록 하자.
ex) http://localhost:8080

Callback URL은 인가코드를 발급해서 redirect 해주는 URL을 말하는건데 프로젝트내 Controller에서 인가코드를 받을 수 있도록 매핑해주자.
ex) http://localhost:8080/login/naver

애플리케이션 등록이 완료되었다면 client Id와 client Secret을 발급받는데 이를 활용해서 구현할 것 이다.

처음에는 테스트 애플리케이션으로 개발하게 되는데, 만약 여러가지 네이버 아이디로 테스트 하려면 꼭 멤버 관리에 해당 이메일을 등록해두어야 한다.

- 생성된 네이버 로그인 페이지 URL
https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=SJKHqSCdsjkewSQW2SDAce&state=STATE_STRING&redirect_uri=http://localhost:8080/user/login/naver 

인가코드를 발급하기전 로그인 페이지는 애플리케이션 등록을 마치면 해당 로그인 페이지 URL로 이동 할 수 있다. url parameter에서 redirect_uri 부분을 자신이 Callback URL을 설정한대로 입력해주고 client_id도 마찬가지로 발급받은 client Id를 넣어주도록 하자.


application 설정 파일 세팅

해당 포스팅은 yml 기준으로 작성되었다.

naver:
  request-token-uri: https://nid.naver.com/oauth2.0/token
  client-id: 발급받은 Client Id 코드
  client-secret: 발급받은 Client Secret 코드

토큰을 요청하는 주소와, 발급받은 id, secret 코드를 yml 파일에 작성해주자. 특히 secret 코드는 외부에 노출되지 않도록 조심해주자.

@Data
@Configuration
@ConfigurationProperties(prefix = "naver")
public class NaverProperties {
    private String requestTokenUri;
    private String clientId;
    private String clientSecret;

    public String getRequestURL(String code) {
        return UriComponentsBuilder.fromHttpUrl(requestTokenUri)
                .queryParam("grant_type", "authorization_code")
                .queryParam("client_id", clientId)
                .queryParam("client_secret", clientSecret)
                .queryParam("code", code)
                .toUriString();
    }
}

해당 클래스는 설정파일에 적어둔 데이터를 저장하고, 네이버의 AccessToken을 받기 위한 URL을 만들어내는 메서드를 포함하고 있다.

Controller 구현

    @GetMapping("/login/naver")
    public CustomResponseEntity<UserResponse.Login> loginByNaver(@RequestParam(name = "code") String code) {
        return CustomResponseEntity.success(userService.loginByOAuth(code, NAVER));
    }

위에서 만든 로그인 페이지에서 로그인을 완료하면 해당 주소로 redirect 된다. 로그인 페이지의 URL 파라미터의 response_type을 보면 code 라고 되어있는 부분을 볼 수 있는데 @RequestParam을 통해 인가코드를 요청 받는다고 보면 된다.

필자의 경우 하나의 서비스 메서드로 여러 플랫폼을 관리하기 위해 플랫폼에 맞는 상수값을 함께 매개변수로 넘겨주었다.

Service 구현

1. UserService

	private final List<OAuth2LoginService> oAuth2LoginServices;

    public UserResponse.Login loginByOAuth(String code, Platform platform) {
        Users userEntity = null;

        for (OAuth2LoginService oAuth2LoginService : oAuth2LoginServices) {
            if (oAuth2LoginService.supports().equals(platform)) {
                userEntity = oAuth2LoginService.toEntityUser(code, platform);
                break;
            }
        }

        if (userEntity == null) {
            throw new CustomException(UNEXPECTED_EXCEPTION);
        }

        // 프로젝트에 맞게 이후 로직을 작성

UserService 클래스에서는 프로젝트의 User 도메인에 영향을 주는 로직이기에, 해당 클래스에서 소셜로그인을 구현하면 단일책임의 원칙에서 벗어난다고 생각해 소셜 로그인을 구현하는 부분은 각각의 소셜 로그인에 맞는 Service 클래스에 구현하였다.

또한 객체지향의 OCP 원칙을 통해 if 문으로 해당하는 플랫폼의 Service 클래스의 메서드를 실행시키는 것이 아닌 하나의 인터페이스 구현체를 통해 플랫폼마다의 Service 클래스에서 Override 하여서 구현하였다.

for 문을 통해 구현된 플랫폼을 돌면서 매개변수와 같은 플랫폼일때 해당 플랫폼의 로그인을 메서드를 실행한다. 우리는 네이버 로그인을 구현하고 있었기 때문에 네이버 로그인 API를 구현하자.

2. OAuth2LoginService

public interface OAuth2LoginService {

    Platform supports();

    Users toEntityUser(String code, Platform platform);
}

해당 인터페이스 구현체는 supports()라는 메서드를 통해 해당 플랫폼이 어떤 것 인지 확인할 수 있고
실질적으로 로그인을 진행하는 메서드는 toEntityUser() 이다.

3. NaverLoginService

@Service
@RequiredArgsConstructor
public class NaverLoginService implements OAuth2LoginService {

    private final RestTemplate restTemplate = new RestTemplate();
    private final NaverProperties naverProperties;

    @Override
    public Platform supports() {
        return Platform.NAVER;
    }

    @Override
    public Users toEntityUser(String code, Platform platform) {
        String accessToken = toRequestAccessToken(code);
        NaverUserResponse.NaverUserDetail profile = toRequestProfile(accessToken);

        return Users.builder()
                .email(profile.getEmail())
                .name(profile.getName())
                .build();
    }

OAuth2LoginService를 구현하게끔 implements를 넣어준다. 해당 메서드들을 상속받아 supports() 함수는 그대로 무슨 플랫폼인지 return 해주는 역할이다. UserService에서 for문을 돌면서 이 메서드를 확인하면서 요청받은 플랫폼의 로그인을 진행하는 것 이다.

로그인을 수행하는 메서드는 toEntityUser인데, 기능을 요약하자면 이렇다.

  1. 발급받은 인가코드를 통해 네이버 전용 AccessToken 발급
  2. 발급받은 AccessToken을 통해 네이버 로그인을 한 유저의 프로필 정보를 가져오기
  3. 가져온 프로필 정보를 통해 User 객체 생성 후 return

여기서 1, 2번 로직은 메서드로 분리되어있다. 실질적인 기능을 수행하기 때문에 해당 메서드들을 하나씩 살펴보자.

3-1. AccessToken 요청하기

    private String toRequestAccessToken(String code) {
        ResponseEntity<NaverTokenResponse> response =
                restTemplate.exchange(naverProperties.getRequestURL(code), HttpMethod.GET, null, NaverTokenResponse.class);
                
        // Validate를 만드는 것을 추천

        return response.getBody().getAccessToken();
    
    

NaverLoginService에 restTemplate 이라는 의존성 객체를 생성해두었는데, restTemplate을 이용해 NaverProperties에 만들어둔 URL 메서드를 통해 인가코드를 매개변수로 하여 GET 요청을 보내 AccessToken을 발급받는 과정이다.

AccessToken을 발급받으려면 응답을 받는 DTO 객체를 하나 생성해야한다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class NaverTokenResponse {
    @JsonProperty("access_token")
    private String accessToken;
    @JsonProperty("refresh_token")
    private String refreshToken;
    @JsonProperty("token_type")
    private String tokenType;
    @JsonProperty("expires_in")
    private String expiresIn;
    @JsonProperty("error")
    private String error;
    @JsonProperty("error_description")
    private String errorDescription;
}

위와 같은 DTO 클래스를 생성하여 AccessToken의 응답을 받았다.

3-2 유저 프로필 정보 가져오기

    private NaverUserResponse.NaverUserDetail toRequestProfile(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);

        ResponseEntity<NaverUserResponse> response =
                restTemplate.exchange("https://openapi.naver.com/v1/nid/me", HttpMethod.GET, request, NaverUserResponse.class);
                

        return response.getBody().getNaverUserDetail();
    }

발급받은 AccessToken을 갖고 이번엔 헤더에 해당 토큰을 등록 후 restTemplate을 통해 다시한번 유저 프로필 정보를 GET 요청한다. 이번에도 유저 프로필 정보를 가져오기 위한 response DTO 클래스를 하나 생성해주자.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class NaverUserResponse {
    @JsonProperty("resultcode")
    private String resultCode;
    @JsonProperty("message")
    private String message;
    @JsonProperty("response")
    private NaverUserDetail naverUserDetail;

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class NaverUserDetail {
        private String id;
        private String name;
        private String email;
    }
}

해당 포스팅은 유저 프로필 정보를 회원이름과 연락처 이메일 주소만 받는다고 설정했기 때문에 DTO 클래스가 이렇게 구성되어있다. 만약 더 있다면 여기서 응답받을 객체를 더 생성해주어야 한다.

이런식으로 코드를 구현하면 네이버 로그인 API를 구현할 수 있다.

@Service
@RequiredArgsConstructor
public class NaverLoginService implements OAuth2LoginService{

    private final RestTemplate restTemplate = new RestTemplate();
    private final NaverProperties naverProperties;

    @Override
    public Platform supports() {
        return Platform.NAVER;
    }

    @Override
    public Users toEntityUser(String code, Platform platform) {
        String accessToken = toRequestAccessToken(code);
        NaverUserResponse.NaverUserDetail profile = toRequestProfile(accessToken);

        return Users.builder()
                .email(profile.getEmail())
                .name(profile.getName())
                .build();
    }

    private String toRequestAccessToken(String code) {

        ResponseEntity<NaverTokenResponse> response =
                restTemplate.exchange(naverProperties.getRequestURL(code), HttpMethod.GET, null, NaverTokenResponse.class);

        // Validate를 만드는 것을 추천

        return response.getBody().getAccessToken();
    }

    private NaverUserResponse.NaverUserDetail toRequestProfile(String accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(headers);

        ResponseEntity<NaverUserResponse> response =
                restTemplate.exchange("https://openapi.naver.com/v1/nid/me", HttpMethod.GET, request, NaverUserResponse.class);

		// Validate를 만드는 것을 추천

        return response.getBody().getNaverUserDetail();
    }
}

이렇게 하면 네이버 로그인 API는 구현을 할 수 있다! 해당 프로필 정보나 토큰들을 갖고 자신이 진행하고 있는 프로젝트에는 어떻게 사용할지 설계를 하면 된다. 필자의 경우 JWT와 함께 사용했다.

만약 새로운 플랫폼이 추가된다면, OAuth2LoginService 구현체를 이용해 플랫폼을 추가해나가면 된다. 이렇게 하면 OCP 원칙을 적용하면서 다양한 플랫폼 로그인 서비스들을 지원할 수 있다.

포스팅 초반에도 말했듯이, 소셜 로그인을 Rest 형식으로 구현하는건 웬만해서 비슷하다. 구글이랑 카카오도 위 서비스 구현 방식과 거의 동일하다고 보면 된다.

프로젝트를 진행하면 소셜 로그인을 구현을 계속 해보면서 코드도 리팩터링하고, 객체지향적인 요소들을 고려하다보니 점차 코드가 발전해가는게 약간 보기 좋았다(ㅎ..) 그래서 언젠간 소셜로그인 2편을 만들어보자고 생각했는데 이번에 마음먹고 새로 작성해보았다.

기존에 있던 구글과 카카오도 게시글을 약간 수정할지, 아니면 새로 작성할지 고민해봐야겠다.
근데 진짜 구현방식이 너무 비슷해서 굳이 똑같은 걸 적어야하나...싶긴한데 일단 다음에 생각해보기로 하쟈 🙇‍♂️

여담

틀린 주소 : https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=(어쩌구) &state=STATE_STRING&redirect_uri=http://localhost:8080/user/login/naver

맞는 주소 : https://nid.naver.com/oauth2.0/authorize?response_type=code&client_id=(어쩌구)&state=STATE_STRING&redirect_uri=http://localhost:8080/user/login/naver

로그인창에도 접근은 잘되는데 로그인만 하면 오류가 터지길래 코드도 분명 잘못된게 없는 것 같아서 머리를 끙끙대다가 로그인 URL 에서 클라이언트 ID 넣어주고 한칸 띄어쓰기가 들어가 있는걸 발견해 띄어쓰기를 없애주자마자 잘됐다..

웃긴건 크롬이나 엣지 같은 브라우저에서는 잘돼서 사파리만의 고유 문제인가 했는데 띄어쓰기 문제였다.. 차라리 다른 브라우저도 되지 말지.. ㅠ_ㅠ

profile
내가 몰입하는 과정을 담은 곳

3개의 댓글

comment-user-thumbnail
2024년 6월 11일

혹시 선생님 코드 전체적으로 볼수 있을까요??

1개의 답글
comment-user-thumbnail
2024년 7월 9일

안녕하세요! 글을 정말 잘 봤습니다! 프로젝트로 네이버 로그인을 구현하려고 하고 있는데 질문이 괜찮으시다면 혹시 build.gradle에 라이브러리를 어떤 것을 추가 하는 부분이 있을까요?

답글 달기