심화 - 네이버 소셜 로그인

변현섭·2023년 9월 23일
0

이전 포스팅에서 다룬 카카오 소셜로그인에 이어 네이버 소셜로그인을 적용하는 방법을 알아보기로 합시다. 처음에는 카카오 로그인만 적용할 생각이었으나, 소셜로그인을 최소 2개 이상은 적용하는 것이 일반적인 것 같아 네이버 소셜 로그인도 적용해보기로 하였습니다.

네이버 로그인도 카카오 로그인과 큰 차이는 없지만, 네이버 소셜로그인의 경우 검수 절차가 있어, 사용 허가를 받는 과정이 다소 까다로울 수 있습니다. 네이버 로그인 API 검수 신청 방법에 대해서는 아래의 링크를 참조해주시기 바랍니다.
>> 네이버 소셜 로그인 설정
이번 포스팅은 네이버 로그인 API 사용에 대해 이미 허가를 받았다는 전제 하에 진행하도록 하겠습니다.

4. 네이버 소셜로그인 구현

1) 백엔드

① jwt 패키지 하위로 naver 패키지를 추가하고, naver 패키지 하위로 NaverController, NaverService, dto > GetNaverUserRes를 추가한다.

② NaverController에 아래의 내용을 입력한다.

@RestController
@RequiredArgsConstructor
public class NaverController {

    private final NaverService naverService;

    /**
     * 네이버 소셜로그인
     */
    @ResponseBody
    @PostMapping("/oauth/naver")
    public BaseResponse<PostNaverLoginRes> naverCallback(@RequestParam("token") String accessToken) {
        try {
            return new BaseResponse<>(naverService.naverCallBack(accessToken));
        } catch (BaseException exception) {
            return new BaseResponse<>(exception.getStatus());
        }
    }
}

③ NaverService에 아래의 내용을 입력한다.

@Service
@RequiredArgsConstructor
public class NaverService {

    private final UserRepository userRepository;
    private final JwtProvider jwtProvider;

    /**
     * 네이버 콜백 메서드
     */
    public PostNaverLoginRes naverCallBack(String accessToken) throws BaseException {
        GetNaverUserRes getNaverUserRes = getUserInfo(accessToken);
        String email = getNaverUserRes.getEmail();
        String nickName = getNaverUserRes.getNickName();
        Optional<User> findUser = userRepository.findByEmail(email);

        JwtResponseDto.TokenInfo tokenInfo;
        if (!findUser.isPresent()) { // 회원가입인 경우
            User naverUser = new User();
            naverUser.createUser(nickName, email, null, null);
            userRepository.save(naverUser);
            tokenInfo = jwtProvider.generateToken(naverUser.getId());
            return new PostNaverLoginRes(naverUser.getId(), naverUser.getEmail(), tokenInfo.getAccessToken(), tokenInfo.getRefreshToken());
        }
        else { // 기존 회원이 로그인하는 경우
            User user = findUser.get();
            tokenInfo = jwtProvider.generateToken(user.getId());
            return new PostNaverLoginRes(user.getId(), user.getEmail(), tokenInfo.getAccessToken(), tokenInfo.getRefreshToken());
        }
    }

    /**
     * 네이버 유저의 정보 가져오기
     */
    public GetNaverUserRes getUserInfo(String accessToken) throws BaseException {
        // HttpHeader 생성
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("Authorization", "Bearer " + accessToken);
        httpHeaders.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");

        // HttpHeader와 HttpBody를 하나의 객체에 담기
        HttpEntity<String> requestEntity = new HttpEntity<>(httpHeaders);

        RestTemplate restTemplate = new RestTemplate();
        // Http 요청을 GET 방식으로 실행하여 멤버 정보를 가져옴
        ResponseEntity<String> responseEntity = restTemplate.exchange(
                "https://openapi.naver.com/v1/nid/me",
                HttpMethod.GET,
                requestEntity,
                String.class
        );

        // 네이버 인증 서버가 반환한 사용자 정보
        String userInfo = responseEntity.getBody();

        // JSON 데이터에서 필요한 정보 추출
        Gson gsonObj = new Gson();
        Map<?, ?> data = gsonObj.fromJson(userInfo, Map.class);
        // 유저의 이메일 정보 가져오기
        String email = (String) ((Map<?, ?>) (data.get("response"))).get("email");
        // 유저의 닉네임 정보 가져오기
        String nickName = (String) ((Map<?, ?>) (data.get("response"))).get("nickname");
        return new GetNaverUserRes(email, nickName);
    }
}

④ GetNaverUserRes에 아래의 내용을 입력한다.

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class GetNaverUserRes {
    private String email;
    private String nickName;
}

⑤ PostNaverLoginRes에 아래의 내용을 입력한다.

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class PostNaverLoginRes {
    private Long userId;
    private String email;
    private String accessToken;
    private String refreshToken;
}

⑥ PostNaverUserReq에 아래의 내용을 입력한다.

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class PostNaverUserReq {
    String uid;
    String deviceToken;
}

2) 프론트엔드

아래의 공식 문서를 참고하여 코드를 작성하면 된다.
>> 네이버 로그인 공식 문서

① Module 수준의 build.gradle 파일에 아래의 의존성을 추가한다.

implementation("com.navercorp.nid:oauth:5.7.0")

② strings.xml에 본인의 Client ID 값과 Client Secret을 입력한다.

<string name="social_login_info_naver_client_id">{Client ID}</string>
<string name="social_login_info_naver_client_secret">{Client Secret}</string>

③ activity_intro.xml에 아래의 내용을 추가한다.

<com.navercorp.nid.oauth.view.NidOAuthLoginButton
    android:id="@+id/buttonOAuthLoginImg"
    android:layout_width="match_parent"
    android:layout_marginHorizontal="15dp"
    android:layout_marginBottom="15dp"
    android:scaleType="fitXY"
    android:layout_height="60dp" />

④ 네이버 로그인에는 data binding이 사용된다. data binding을 사용하기 위해 Module 수준의 build.gradle 파일에 아래의 내용을 추가한다.

android {
	...
    buildFeatures{
        dataBinding = true
    }
}

⑤ activity_intro.xml에 루트 컨테이너에 우클릭한 후 Convert to data binding layout을 클릭한다.

  • 이제 data binding을 사용하기 위한 준비가 끝난다.

⑥ api > dto 패키지 하위로, PostNaverUserReq라는 data class를 추가한다.

  • dto의 이름만 달라졌을 뿐, PostKakaoUserReq와 동일하다.
data class PostNaverUserReq(
    @SerializedName("uid")
    val uid : String,

    @SerializedName("deviceToken")
    val deviceToken : String,
)

⑦ api > dto 패키지 하위로, PostNaverLoginRes도 추가한다.

  • dto의 이름만 달라졌을 뿐, PostKakaoLoginRes와 동일하다.
data class PostNaverLoginRes(
    @SerializedName("userId")
    val userId : Long,

    @SerializedName("email")
    val email : String,

    @SerializedName("accessToken")
    val accessToken : String,

    @SerializedName("refreshToken")
    val refreshToken : String
)

⑧ api 패키지 하위로, NaverApi 인터페이스를 추가한다.

interface NaverApi {
    @POST("/oauth/naver")
    suspend fun naverCallback(
        @Query("token") accessToken : String // 네이버 서버에서 보내준 access token으로, 인증 토큰이다.
    ) : BaseResponse<PostNaverLoginRes>

    @POST("/oauth/device-token")
    suspend fun saveUidAndToken(
        @Header("Authorization") accessToken : String,
        @Body postKakapUserReq: PostNaverUserReq
    ): BaseResponse<String>
}

⑨ RetrofitInstance에 아래의 내용을 추가한다.

val naverApi = retrofit.create(NaverApi::class.java)

⑩ IntroActivity에 아래의 내용을 입력한다.

  • 카카오 소셜로그인과 동일한 방식으로 작성하면 된다.
class IntroActivity : AppCompatActivity() {

    private lateinit var auth: FirebaseAuth
    lateinit var binding : ActivityIntroBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityIntroBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setContentView(R.layout.activity_intro)

        auth = Firebase.auth

        /** Naver Login Module Initialize */
        val naverClientId = getString(R.string.social_login_info_naver_client_id)
        val naverClientSecret = getString(R.string.social_login_info_naver_client_secret)
        NaverIdLoginSDK.initialize(this, naverClientId, naverClientSecret, "U & I TALK")
        ...
        val oauthLoginCallback = object : OAuthLoginCallback {
            override fun onSuccess() {
                // 네이버 로그인 인증이 성공했을 때 수행할 코드 추가
                val token = NaverIdLoginSDK.getAccessToken().toString()
                Log.d("naverToken", token)
                if(token != null) {
                    CoroutineScope(Dispatchers.IO).launch {
                        val response = naverCallback(token)
                        Log.d("IntroActivity", response.toString())
                        if (response.isSuccess) {
                            Log.d("email", response.result!!.email)
                            Log.d("uid", FirebaseAuthUtils.getUid())
                            if(FirebaseAuthUtils.getUid() == null) {
                                auth.createUserWithEmailAndPassword(response.result!!.email, "abc123")
                            }
                            FirebaseMessaging.getInstance().token.addOnCompleteListener(
                                OnCompleteListener { task ->
                                    if (!task.isSuccessful) {
                                        Log.w("MyToken", "Fetching FCM registration token failed", task.exception)
                                        return@OnCompleteListener
                                    }
                                    val uid = FirebaseAuthUtils.getUid()
                                    val deviceToken = task.result
                                    val userInfo = UserInfo(uid, response.result?.userId,
                                        deviceToken, response.result?.accessToken, response.result?.refreshToken)
                                    Log.d("userInfo", userInfo.toString())
                                    FirebaseRef.userInfo.child(uid).setValue(userInfo)

                                    CoroutineScope(Dispatchers.IO).launch {
                                        val postNaverUserReq = PostNaverUserReq(uid, deviceToken)
                                        val saveRes = saveNaverUidAndToken(response.result?.accessToken!!, postNaverUserReq)
                                        Log.d("UidToken", saveRes.toString())
                                        if (saveRes.isSuccess) {
                                            Log.d("UidToken", "UID와 디바이스 토큰 저장 완료")
                                        } else {
                                            Log.d("UidToken", "UID와 디바이스 토큰 저장 실패")
                                        }
                                    }
                                    val intent = Intent(this@IntroActivity, MainActivity::class.java)
                                    startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP))
                                    finish()
                                })
                            Log.d("IntroActivity", "로그인 완료")
                        } else {
                            // 로그인 실패 처리
                            Log.d("IntroActivity", "로그인 실패")
                            val message = response.message
                            Log.d("IntroActivity", message)
                            withContext(Dispatchers.Main) {
                                Toast.makeText(this@IntroActivity, message, Toast.LENGTH_SHORT).show()
                            }
                        }
                    }
                }

                else {
                    Toast.makeText(this@IntroActivity, "접근이 거부 됨", Toast.LENGTH_SHORT).show()
                }
            }
            override fun onFailure(httpStatus: Int, message: String) {
                val errorCode = NaverIdLoginSDK.getLastErrorCode().code
                Log.d("naverToken", errorCode)
                val errorDescription = NaverIdLoginSDK.getLastErrorDescription()
                Log.d("naverToken", errorDescription.toString())
            }
            override fun onError(errorCode: Int, message: String) {
                onFailure(errorCode, message)
            }
        }
        
        binding.buttonOAuthLoginImg.setOAuthLogin(oauthLoginCallback = oauthLoginCallback)
    }
    ...
}

3) 네이버 로그인 API 설정

반드시 아래의 링크에서 알려주는 방법에 따라 앱 등록이 먼저 완료되어야 한다.
>> 네이버 소셜 로그인 설정

① 아래의 링크에 접속한다.
>> 네이버 로그인 Open API

② 본인이 등록한 앱 > 연필 아이콘을 클릭한다.

③ API 설정 탭에 들어간 후, 상단에 보이는 환경 추가에서 안드로이드를 선택한다.

  • 참고로, 다운로드 URL에는 아무런 값이나 넣어도 된다.

④ 수정 버튼을 클릭하면 완료된다.

이제 앱을 실행시켜보자. IntroActivity에서 네이버 아이디로 로그인 버튼을 클릭하면, 정보 제공 동의 화면이 나타나고, 동의하기 버튼을 클릭하면 본인의 네이버 이메일과, 네이버 별명으로 로그인된다.

참고로 동의를 한번 하고난 후, 다시 네이버 아이디로 로그인 버튼을 클릭하면 동의 화면 없이 바로 로그인된다. 만약 동의화면을 다시 테스트해보고 싶을 경우, 아래의 방법을 참고하자.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글