본문 바로가기
Django

[Django] 장고 google 소셜 로그인

by shur_ 2024. 1. 17.

 

시작하기

 

클라우드 컴퓨팅 서비스 | Google Cloud

데이터 관리, 하이브리드 및 멀티 클라우드, AI와 머신러닝 등 Google의 클라우드 컴퓨팅 서비스로 비즈니스 당면 과제를 해결하세요.

cloud.google.com

 

처음 시작할 때 구글 소셜 로그인 관련된 사이트를 못 찾는 경우가 많다.

구글 클라우드 사이트에 들어간다.

 

 

 

사이트 상단 바를 보면 '콘솔'이라는 문구가 보인다. 클릭.

 

 

구글 소셜 로그인을 이용할 때 'API 및 서비스'을 자주 들어갈 것이다.

초기에 'OAuth 동의 화면' 설정과 같은 내용들은 다른 블로그들에 잘 설명되어 있으니 참고하면 된다.

 

 

 

웹 서버 애플리케이션용 OAuth 2.0 사용  |  Authorization  |  Google for Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 의견 보내기 웹 서버 애플리케이션용 OAuth 2.0 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분

developers.google.com

위 링크는 구글 공식 참고 문서

한 번 쭉 읽어보는걸 추천

 

리디렉션 URI

 

리디렉션 URI, 리다이렉트 URI...

 

초기 설정을 하다 보면 리디렉션 URI를 입력하라는 칸이 있다.

처음에는 구글링하면서 막연하게 따라만 하다보니 리디렉션 URI( 카카오 소셜 로그인에서는 Redirect URI ) 가 뭘 의미하는지 이해하지 못한채 블로그들에 적혀있는대로 무작정 따라하기만 했다.

 

리디렉션 URI는 말 그대로 Google의 인증을 받은 후 다시 연결되는 페이지를 의미한다.

설정으로 입력해 둔 경로로 페이지가 이동되며, 경로 뒤에 GET 방식과 비슷하게 승인 코드가 추가된다.

아래에서 예시로 더 자세하게 설명해보겠다.


 

 

OAuth란 ?

현재 사용중인 서비스가, 서비스를 이용하는 사용자의 타 소셜 정보에 접근하기위해 권한을 타 소셜로 부터 위임받는 것.

구글, 네이버, 카카오와 같은 다양한 소셜 플랫폼에 접근하도록 제 3자 클라이언트(우리의 서비스)가 접근 권한을 위임받을 수 있는 표준 프로토콜이다.

 

이 때 구글, 카카오 등의 소셜 플랫폼이 서비스에게 내 아이디와 비밀번호 계정정보를 그대로 제공하는 것이 아니라, AccessToken의 형태로 발급해준다. 그대로 발급하면 보안상 문제가 발생하니까.

 

OAuth 와 관련한 역할 및 용어

  • Resource Owner:
    내 서비스를 사용할 사용자. 이 서비스 사용자들은 구글과 같은 소셜 플랫폼에서 리소스를 가지고 있는 사용자.
  • Resource Server:
    리소스 제공자(구글, 카카오, 네이버)로, 데이터를 보유하고 있는 서버를 의미.
  • Authorization Server:
    Resource Owner를 인증하고, 내 서비스(클라이언트)한테 토큰을 발급해주는 서버.
  • Client(내가 구현하고 있는 서비스):
    내 서비스, 내가 구현하고 있는 애플리케이션을 의미.
    Resource Server의 리소스를 이용하고자 하는 서비스.
    내가 개발하고 있는 서비스를 클라이언트라고 함.

 

구글 소셜 로그인 흐름

 

간단한 흐름은 다음과 같다

  1. 웹에서 구글 로그인 버튼 클릭
  2. 구글 계정 선택하는 구글 로그인 페이지로 이동
  3. 로그인 후 확인 버튼 클릭
  4. 웹으로 리다이렉션 되고 로그인 성공

 

구체적인 흐름은 다음과 같다.

  1. Authorization Code(인가코드)를 받기 위해 구글 로그인창으로 접속한다.
    클라이언트(Django)가 웹 브라우저를 통해 사용자를 구글 서버(권한 부여 서버)로 넘겨주고 권한 승인 흐름을 시작하는 부분이다.
  2. 구글 서버(권한 부여 서버)는 사용자를 인증하고 사용자의 동의를 구한다. (구글 로그인은 이러한 과정이 없지만 카카오 로그인을 생각해보면 무엇인가를 동의하는 부분이 있다.)
    이때 로그인 시 어떤 정보를 구글 서버로부터 받을 지는 django 서버 내부에서 scope 파라미터로 정해줄 수 있다. (구글 공식 문서 확인). scope를 지정해주면 해당 정보까지 받을 수 있는 접근토큰을 받을 수 있다.
  3. 클라이언트(django)가 인증을 요청하고(-> 계정 버튼을 눌러 로그인 시도) 로그인에 성공 시, 구글 서버는 우리가 지정해준 리디렉션 URI로 접근 토큰을 받을 수 있는 인가 코드를 전송한다.
    인가 코드는 브라우저의 URI 쿼리를 통해 전달되는데, 악의적인 javascript 코드가 인증 코드를 가로챌 수 있으므로 짧은 시간에 걸쳐 단 한 번만 허용된다.
  4. URI에서 추출한 인증 코드를 사용해 구글 서버에 접근 토큰(access token)을 요청한다.(POST)
    이때 GOOGLE_OAUTH2_CLIENT_ID 와 GOOGLE_OAUTH2_CLIENT_SECRET 을 함께 전송한다. (CLIENT_ID, CLIENT_SECRET)
  5. Access Token을 받은 후 유저 정보를 가지고 올 수 있는 URL로 파라미터에 Access Token을 담아서 GET요청을 보낸다.

 

자세히 보면 아래와 같다.

 

 

 

사용자는 소셜 로그인 버튼을 클릭한다.

 

제공된 로그인 페이지에서 로그인을 시도한다.

 

 

로그인 성공 시 설정해놓은 리디렉션 URI로 이동되고 Authorization Code(인가 코드)를 발급받는다.

해당 코드는 설정해놓은 리디렉션 URI 뒤에 'code='으로 붙는다.

이렇게 주어진 인가 코드를 이용하여 구글에 AccessToken을 달라고 요청하면 된다.

 

여기까지가 사용자에게 보여지는 화면들인데 뒷 사정은 조금 복잡할 수 있다.

일단 여기까지 흐름을 이해해보자


 

Authorization Code(인가 코드) 받기

 

위 로그인 창으로 연결되게 하는 URL.

구글 소셜 로그인 버튼에 연결해두면 된다.

구글 로그인창을 호출하는 URL은 https://accounts.google.com/o/oauth2/v2/auth 이다.
필수로 추가해야하는 파라미터는 response_type, client_id, redirect_uri, state 총 4개.

공식 문서를 보면 자세하게 설명되어있다.

  • client_id: 구글 개발자센터에서 발급받은 Key
  • redirect_uri: 구글 개발자센터에서 설정한 리디렉션 URI
  • response_type: code로 고정 (인가코드를 통한 로그인 방식)
  • scope: 토큰 발급 이후 유저 정보에서 어떤 항목을 조회할 것인가를 띄어쓰기를 구분.

scope 관련

https://accounts.google.com/o/oauth2/v2/auth?client_id="클라이언트ID 입력"&redirect_uri="설정해둔 redirect uri 입력"&response_type=code&scope=email profile

 

 

로그인 성공 후 응답받은 URL

'type=google'은 소셜 로그인 플랫폼 별로 개발하기 위해서 내가 설정해놓은 코드(원래는 없음)

http://localhost:5500/signin.html?type=google&code=4%2F0AfJohXmjjibWEnCnAI_HE6N9JY-e2Z0fa7N-mBxRPNVPzluwFYmAWy5Jd_304eoTWbBw&scope=email+profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%1F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+openid&authuser=0&prompt=consent

 

AccessToken 얻기

 

구글 인증 서버에서 로그인 토큰을 가져온다.
요청 URI는 https://oauth2.googleapis.com/token 이다.
로그인 토큰을 가져오기 위해 아래 정보를 Body에 담아 요청한다.

  • code : 클라이언트 페이지에서 얻은 인가 코드를 사용 하기 때문에 "code"로 고정.
  • client_id : 구글 개발자센터에서 발급 받은 Client ID
  • client_secret : 구글 개발자센터에서 발급 받은 Client Secret
  • redirect_uri : 구글 개발자센터에서 등록한 redirect_uri
  • grant_type: 'authorization_code' 로 고정 (인가코드를 통한 로그인 방식)

 

AccessToken을 얻으려고 포스트맨으로 여러번 시도 했는데 

 

{ "error": "invalid_grant", "error_description": "Malformed auth code." } 이런 에러가 떠서 포기하고 vscode로 print 해서 찍어보았다.

 

{
'access_token': 'ya29.a0AfB_byDraH4Lovc3Z9RbG4czQ3NPJxv233MaIW2ykDzvK3TqnxiSzqau0HSSJ_LFtU9YoT0yGhnI1JdPqhaT-NVMocP4lA4gOPFMI10eCbW1XuxgTVdgclXgF1J2NjdqgUgi2GxUUv-R4eQwD3tSmIuqr9XdbbGlb24vaCgYKAfESARESFQHGX2Mitz-7e2R92i3hpqxBlaaDEg0171',
'expires_in': 3599,
'scope': 'openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email',
'token_type': 'Bearer',
'id_token': 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjFmNDBmMGE4ZWYzZDg4MDk3OGRjODJmMjVjM2VjMzE3YzZhNWI3ODEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI0OTIyNTQwNzYzNzctNHZpYWZpdGQwdnFhcjBpbjFmMGFwbDVwbHAyNjdsOGouYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI0OTIyNTQwNzYzNzctNHZpYWZpdGQwdnFhcjBpbjFmMGFwbDVwbHAyNjdsOGouYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMDk4NTMzMjAxNTU5NjQ3Mjc3MjAiLCJlbWFpbCI6InNodXI5MjMwQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoicjhGSDRIMnBSLTlqb3J5YjRqaTN4ZyIsIm5hbWUiOiJTaCIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS9BQ2c4b2NJV1Yydk9SMmlhWGo2WDdjSnJNWS1rdVU2X2lnYm10dGpfWDNPalRNcFEzUT1zOTYtYyIsImdpdmVuX25hbWUiOiJTaCIsImxvY2FsZSI6ImtvIiwiaWF0IjoxNzA1NDgzMDYyLCJleHAiOjE3MDU0ODY2NjJ9.R0UNq5DsbNEmRNRb9UCprMDYQax9WmdYFhHymzyX3iWsJOYIY6apJwFfd0UpuydaHs41fctciWqK0AuCT_Y0_PSV3b-WQIWgAaktERBbVXIn9L71oT7yARu_t0Hd_LLc2MJiMlIP6B370M__nqS-ZmiD_9fK4-ZtMLryJlOJPr9nitEcXSZ9WCe8hvPyEJTNMulTZ52RYoJSFi09y30L6vhYJ07wEit8Ia9CDiLLtNI7v2z7C1436g2lrhHLfm9x00B67PzQDcQyjLSONkCgZTF16FrlxEkINPD5fl5ifWmfwTJFuhk9kqbEzPuzkqAi-g0ObFEOultTYzWM3f8Eog'
}

 

정상적으로 받았을 때 위와 같은 응답이 온다.

  • access_token: AccessToken 값
  • scope: 조회하고자 하는 사용자 정보
  • token_type: 토큰 타입, Bearer로 고정
  • expires_in: Access Token 만료시간(초)

 

유저 정보 가져오기

위 과정에서 발급 받은 Access Token으로 유저 정보를 가져올 수 있다.

요청 URI는 https://www.googleapis.com/oauth2/v1/userinfo

 

 

user_info_response.json() 으로 받아온 사용자 정보를 보면 아래와 같다

{
'id': '786853312155964712345',
'email': 'example@gmail.com',
'verified_email': True,
'name': 'example',
'given_name': 'example',
'picture': 'https://lh3.googleusercontent.com/a/CS8ocIdfavewR2iaXj6X7cJrMY-kuU6_igbmttj_X113OjTMpQ3Q=s96-c',
'locale': 'ko'
}

 

 


 

view

@csrf_exempt
@require_POST
def google_login(request):
    CLIENT_ID = get_env_variable("GOOGLE_CLIENT_ID")
    CLIENT_SECRET = get_env_variable("GOOGLE_CLIENT_SECRET")

    # 클라이언트에서 받은 인가 코드
    json_data = json.loads(request.body.decode("utf-8"))
    authorization_code = json_data.get("code")

    # 로그인 토큰을 얻기 위한 요청 설정
    token_url = "https://oauth2.googleapis.com/token"
    token_payload = {
        "code": authorization_code,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET,
        "redirect_uri": "http://localhost:5500/signin.html?type=google",
        "grant_type": "authorization_code",
    }

    # 로그인 토큰 요청
    token_response = requests.post(token_url, data=token_payload)

    if token_response.status_code == 200:
        access_token = token_response.json().get("access_token")

        # 구글 사용자 정보 요청
        user_info_url = "https://www.googleapis.com/oauth2/v1/userinfo"
        headers = {
            "Authorization": f"Bearer {access_token}",
        }
        user_info_response = requests.get(
            user_info_url, headers=headers
        )
        
        if user_info_response.status_code == 200:
            google_id = user_info_response.json()["id"]

            if not google_id:
                response_data = {"success": False, "message": "구글 계정을 받아오지 못했습니다."}
                return JsonResponse(response_data, status=400)

            if not models.Users.objects.filter(
                google_id=google_id, user_status=0
            ).exists():
                response_data = {
                    "success": True,
                    "message": "not exists",
                    "data": {"id": google_id},
                }
                return JsonResponse(response_data, status=200)

            # jwt 토큰 프론트로 전달
            user = models.Users.objects.get(google_id=google_id)
            token = create_token(user.user_id)
            response_data = {
                "success": True,
                "message": "exists",
                "data": {"token": token},
            }
            return JsonResponse(response_data, status=200)

        else:
            return JsonResponse(
                {"success": False, "message": "Failed to fetch user info from Google"},
                status=500,
            )

    else:
        return JsonResponse(
            {"success": False, "message": "access token을 받아오는데 실패했습니다."},
            status=500,
            json_dumps_params={"ensure_ascii": False},
        )

 

 

js

//calllback으로 받은 인가코드 및 아이디
const code = new URL(window.location.href).searchParams.get('code');
const type = new URL(window.location.href).searchParams.get('type');
const selectId = new URL(window.location.href).searchParams.get('id'); // 아이디 찾기에서 넘어온 경우

// 소셜 로그인
const socialLogin = async(type) => {
    axios({
        url: `http://localhost:8000/account/${type}Login/`,
        method: 'post',
        data: {code: code},
    })
    .then(response => {
        console.log('성공:', response.data); // 로그에 응답 데이터를 찍습니다.

        // 2-1. 로그인 성공 (로그인 후 프롬프트 페이지로 이동)
        if (response.data.message == "exists") {
            // localStroage에 토큰(Token)을 적재한다.
            const accessToken = response.data.data.token;
            setWithExpire('accessToken', accessToken, 12*60*60*1000); //12 시간

            // index.html로 Routing 한다.
            window.location.href = '../index.html';
        } else {
            // 2-2. 로그인 실패 (모달창 띄우기)
            modal.style.display = 'block';
            document.getElementById("socialType").value = type;
            document.getElementById("socialId").value = response.data.data.id;
            document.getElementById("signUpBtn").setAttribute("onclick", `location.href='${signUpURL}?type=${type}&socialId=${response.data.data.id}'`);
        }
    })
    .catch(error => {
        console.log(error);
        errorLogin.style.display = 'inline-block';
        errorLogin.textContent = '로그인에 실패했습니다. 다시 시도해주세요.';
    });
}

 

 

 

 

jwt로 인증하는 방식이라

jwt도 포스팅해서 정리해야겠다.

'Django' 카테고리의 다른 글

아나콘다(Anaconda) 가상환경 만들기  (0) 2024.01.05
Model 생성 관련  (0) 2023.12.20
Django Start  (0) 2023.12.20

댓글