- 거의 지난 프로젝트의 코드를 컨닝한 셈이지만 새로운 프로젝트 컨벤션에 맞게 적용하면서 코드를 다 이해했다면 괜찮지 않을까...?!
<사전 준비>
- 구글에서 API 키를 얻어야 한다
- 이 블로그를 참고해서 얻어보자!
- API 키를 얻었다면 .env 파일에 넣으면 된다
- settings.json 쓰면 거기 넣고, my_settings.py 쓰면 거기 넣으면 된다
- 알아서 각자 잘 넣어보자 ㅋㅋ
<백엔드>
- 유저 모델 필드 확인하기
class User(AbstractBaseUser):
email = models.EmailField(
verbose_name="사용자 이메일",
max_length=255,
unique=True,
)
nickname = models.CharField(max_length=100, unique=True, verbose_name="사용자 닉네임")
password = models.CharField(max_length=255, verbose_name="비밀번호")
GENDER_CHOICES = [
("남성", "남성"),
("여성", "여성"),
("밝히고 싶지 않음", "밝히고 싶지 않음"),
]
gender = models.CharField(
max_length=10,
choices=GENDER_CHOICES,
blank=False,
error_messages="필수 입력 값입니다.",
verbose_name="성별",
)
age = models.PositiveIntegerField(null=True, verbose_name="사용자 나이")
bio = models.TextField(blank=True, null=True, verbose_name="사용자 자기소개")
profile_image = models.ImageField(
upload_to="profile_images/", blank=True, null=True, verbose_name="사용자 프로필이미지"
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="사용자 계정 생성일")
updated_at = models.DateTimeField(auto_now=True, verbose_name="사용자 정보 마지막 수정일")
LOGIN_TYPES = [
("normal", "일반"),
("google", "구글"),
("naver", "네이버"),
("kakao", "카카오"),
]
login_type = models.CharField(
"로그인유형", max_length=10, choices=LOGIN_TYPES, default="normal"
)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = "email" # 이걸로 로그인 하겠다 하는 필드. 들어가는 값은 unique=True 속성.
REQUIRED_FIELDS = ["nickname"] # createsuperuser할때 어떤 필드들을 작성받을 지 적는 필드.
...
- 우리 프로젝트에서는 email을 통해 로그인을 하고 있고
- 이메일이 중복될 수 없기 때문에 로그인 유형이 필요해서 필드를 추가하였다
- url 설정
# users/urls.py
from django.urls import path
from users import views
...
urlpatterns = [
...
path("google/", views.GoogleLogin.as_view(), name="google_login"),
]
- 일단 구글로그인을 수행할 url을 먼저 지정해준다
- 구글로그인 클래스형 뷰 만들기
class GoogleSignin(APIView):
"""구글 소셜 로그인"""
def get(self, request):
GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY")
return Response(GOOGLE_API_KEY, status=status.HTTP_200_OK)
def post(self, request):
access_token = request.data["access_token"]
user_data = requests.get(
"https://www.googleapis.com/oauth2/v2/userinfo",
headers={"Authorization": f"Bearer {access_token}"},
)
user_data = user_data.json()
data = {
"profile_image": user_data.get("picture"),
"email": user_data.get("email"),
"nickname": user_data.get("name"),
"signin_type": "google",
}
return SocialSiginin(**data)
- API키는 각자 설정파일에서 잘 불러오면 된다
- 우선 구글 로그인 버튼을 눌렀을 때 get요청으로 들어간다
- 구글 API키를 반환해서 프론트엔드에서 활용할 수 있도록 한다
- 구글 로그인 창에서 내 계정을 클릭하면 post 요청으로 들어간다
- 계정을 클릭하면 주소창에 access token이 띄워지는데 프론트엔드에서는 그걸 받아서 백엔드로 보내준다
- 그럼 백엔드에서는 그 토큰을 이용해서 구글 사용자 정보 url에 접속해 사용자 정보를 얻고 소셜 로그인을 진행한다
- 소셜로그인 함수 만들기
- 구글 외에도 카카오, 네이버 로그인을 구현할 예정이기 때문에 소셜로그인 기능을 세 소셜 로그인에서 사용할 수 있도록 함수로 따로 정의했다
def SocialSiginin(**kwargs):
"""소셜 로그인, 회원가입"""
# 각각 소셜 로그인에서 유저 정보를 받아오고 None인 값들은 빼줌
data = {k: v for k, v in kwargs.items() if v is not None}
email = data.get("email")
signin_type = data.get("signin_type")
if not email:
# email이 없으면 회원가입이 불가능하므로 프론트에 error메시지와 http status를 보냄
return Response(
{"error": "해당 계정에 email정보가 없습니다."}, status=status.HTTP_400_BAD_REQUEST
)
try:
user = User.objects.get(email=email)
if signin_type == user.signin_type:
# 로그인 타입이 같으면, 토큰 발행해서 프론트로 보내주기
refresh_token = RefreshToken.for_user(user)
access_token = CustomTokenObtainPairSerializer.get_token(user)
return Response(
{
"refresh": str(refresh_token),
"access": str(access_token.access_token),
},
status=status.HTTP_200_OK,
)
else:
# 유저의 다른 소셜계정으로 로그인한 유저라면, 해당 로그인 타입을 보내줌.
# (프론트에서 "{signin_type}으로 로그인한 계정이 있습니다!" alert 띄워주기)
return Response(
{"error": f"{user.signin_type}로 이미 가입된 계정이 있습니다!"},
status=status.HTTP_400_BAD_REQUEST,
)
except User.DoesNotExist:
# 유저가 존재하지 않는다면 회원가입시키기
new_user = User.objects.create(**data)
new_user.set_unusable_password() # pw는 사용불가로 지정
new_user.save()
# 회원가입 후 토큰 발급해서 프론트로 보냄
refresh_token = RefreshToken.for_user(new_user)
access_token = CustomTokenObtainPairSerializer.get_token(new_user)
return Response(
{"refresh": str(refresh_token), "access": str(access_token.access_token)},
status=status.HTTP_200_OK,
)
- 소셜 로그인 시에 이미 소셜로 로그인 해서 비밀번호가 있으므로 데이터베이스에 비밀번호를 사용하지 않음을 명시한다
- 이렇게 소셜 로그인 함수까지 거치고 나면 프론트엔드에 유저의 토큰이 보내진다
<프론트엔드>
- 로그인 버튼 만들기
<-- index.html !-->
<head>
<script type="module" src="../static/js/navbar.js"></script>
<script type="module" src="../static/js/signin.js"></script>
</head>
<body>
<div class="social-login">
<button type="button" id="googleLoginBtn" class="social-btn">
<img class="social-btn-img" src="/static/img/google.png">
</button>
</div>
</body>
- 적당한 곳에 버튼을 넣어준다
- 구글 로고 이미지가 들어간 버튼이다
- button 태그의 id값을 이용해서 해당 id의 button 태그가 클릭되면 함수가 실행되게 할 예정이다
- 버튼 눌렀을 때 실행될 함수 생성하기
// signin.js
import { googleAPI, frontendBaseURL } from "./api.js";
// 구글 로그인
async function googleSignin() {
googleAPI().then((responseJson) => {
const google_id = responseJson
const scope = 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile'
const param = `scope=${scope}&include_granted_scopes=true&response_type=token&state=pass-through value&prompt=consent&client_id=${google_id}&redirect_uri=${frontendBaseURL}`
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${param}`
})
}
// 구글 로그인 function 실행
document.getElementById("googleSigninBtn").addEventListener("click", googleSignin);
- googleAPI 함수는 백엔드에 get요청을 보내서 구글API키를 받고 json 형식으로 바꿔서 반환한다
- 이동할 url에 API키, scope, param을 다 넣는다
- 구글 로그인 버튼이 클릭되면 이 함수가 실행된다
- 이 함수가 실행되면 메인으로 가면서 주소창에 access token이 생긴다
- 백엔드와 통신하는 API 함수 생성하기
// api.js
// 구글 로그인 API
export async function googleAPI(google_token) {
if (google_token == undefined) {
const response = await fetch(`${backendBaseURL}/users/google/`, { method: 'GET' })
const responseJson = await response.json();
return responseJson;
} else {
const response = await fetch(`${backendBaseURL}/users/google/`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ access_token: google_token })
});
const responseJson = await response.json();
return { response, responseJson };
}
}
- 내 구글 계정을 클릭했을 때 실행되는 함수다
- 구글로그인 버튼 클릭 시 get요청을 보내서 구글 API 키를 반환받는다 그걸 json 형식으로 바꿔서 반환하면 siginin.js에서 로그인 수행할 url을 만들어서 이동시킨다=구글 로그인 창으로 이동한다
- 구글로그인 창에서 계정을 클릭하고 나면 주소창에 access token이 생기고 그걸 매개변수로 받아서 백엔드에 post 요청을 보낸다
- 백엔드에서는 이 토큰을 받아서 구글 사용자 정보에 접근해서 사용자 정보를 받아 로그인/회원가입을 진행시킨다
- 토큰을 받아서 로컬저장소에 저장하기
import { googleAPI, frontendBaseURL, payload } from "./api.js";
// 로컬 스토리지에 jwt 토큰 저장하기
function setLocalStorage(responseJson) {
localStorage.setItem("access", responseJson.access);
localStorage.setItem("refresh", responseJson.refresh);
const base64Url = responseJson.access.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
// payload로 저장하기
localStorage.setItem("payload", jsonPayload);
}
// url에서 access_token 얻기
if (payload) {
} else if (location.href.split("=")[1]) {
let hashParams = new URLSearchParams(window.location.hash.substr(1));
let google_token = hashParams.get("access_token");
// 백엔드 통신 함수
googleAPI(google_token).then(({ response, responseJson }) => {
if (response.status == 200) {
alert('로그인 성공');
window.location.replace(`${frontendBaseURL}/`);
} else {
alert(responseJson.error);
}
setLocalStorage(responseJson)
})
}
- 17번째 줄부터 보자면 혹시 로그인이 되어 있으면 그냥 아무 일도 안 일어나고, 안 되어 있는 경우에 코드가 진행된다
- 주소창에서 access token을 따와서 google_token에 넣어준다
- 위에서 만든 백엔드에 post 요청을 하는 googleAPI에 파라미터로 구글토큰을 넣어준다
- response와 json화된 response를 받아서 HTTP 상태코드에 따라 동작이 나뉜다
- 상태코드가 200이면 로그인 성공이라는 알림창과 함께 새로고침된다
- 이외의 코드가 나오면 백엔드에서 지정해준 error 메시지를 알림창으로 띄운다
- 그리고 json화된 response를 로컬저장소에 토큰을 저장하는 함수의 파라미터로 넣어준다
- 지금 보니 29번째 줄에 있는 코드가 24나 25번째 줄에 있어도 되겠네
- 근데 if-else문 아래에 있든 if문 안에 있든 상관 없는 것 같다
- 의미상 로그인이 성공해야 토큰이 로컬저장소에 저장되는 거니까 if문 아래에 있는 게 더 나아보이는 것 같으니 옮겨주겠다..!!
googleAPI(google_token).then(({ response, responseJson }) => {
if (response.status == 200) {
setLocalStorage(responseJson)
alert('로그인 성공');
window.location.replace(`${frontendBaseURL}/`);
} else {
alert(responseJson.error);
}
})
- 로컬저장소에 토큰 저장하는 함수 위위 코드의 24번째줄로 옮겨줌!
- 그리고 로컬저장소에 토큰 저장하는 건 뭐 정해져 있으니까 고대로 해주면 끝..!
<시연>