본문 바로가기

79 Django 소셜로그인 인증활용하여 댓글기능 만들기

79.1 Social login , comment

이전 까지 댓글 등록/수정/삭제 기능과 소셜 로그인 기능을 만들었습니다.
이번에는 소셜로그인을 성공했을때 인증정보를 세션에 저장하고 저장된 인증정보를 가지고 등록/수정/삭제가 가능하도록 작업하겠습니다.

댓글기능에 인증정보가 추가하면서 처음 생각한것보다 소스가 많이 변경되었습니다.

76 Django CBV 댓글 기능 - Ajax 등록
77 Django CBV 댓글기능-Ajax 수정/삭제
78 Django 소셜로그인 - NAVER LOGIN

79.2 설정정보 추가 - settings.py

클라이언트 아이디와 시크릿 키만 있으면 프로바이더를 쉽게 추가할 수 있는게 allauth의 장점이죠.
일단 프로바이더는 네이버, 카카오, 구글로 하였습니다.

DJANGO_BASE_APP = [
   ...
]

AUTHENTICATION_BACKENDS = [
   ...
]

SOCIALACCOUNT_PROVIDERS = {
    'naver': {'APP': { ...  }},
        'kakao': {'APP': {...}},
          'google': {'APP': {... },
                'SCOPE': ['profile','email',
                ],},
}

LOGIN_REDIRECT_URL = '/' # 로그인 후 리디렉션할 페이지
ACCOUNT_USERNAME_REQUIRED = False # 사용자 이름입력 기본 True
ACCOUNT_AUTHENTICATED_LOGIN_REDIRECTS=False #이미 인증된 사용자가 인증시도시

# SocialAccount.extra_data https://dev.twitch.tv/docs/api/reference/#get-users
SOCIALACCOUNT_ADAPTER = "myapp.common.socialAdapter.CustomSocialAccountAdapter" #socialAdapter 상속
SOCIALACCOUNT_LOGIN_ON_GET = True #GET POST 0.47.0 (2021-12-09) 
  • LOGIN_REDIRECT_URL, LOGOUT_REDIRECT_URL|LOGIN_REDIRECT_URL, LOGOUT_REDIRECT_URL 등은 주석으로 적어놓은 그대로 입니다.
  • SOCIALACCOUNT_ADAPTER : 소셜로그인의 인증하는 기능을 하는 어댑터를 상속 받아 일부 과정에 참여할수 있는 기능을 제공합니다.
  • SOCIALACCOUNT_LOGIN_ON_GET :
    1) 소셜 로그인을 시도할때 인증데이터 post, get 방식 설정(기본 False)
    서드파티 계정을 사용해 로그인을 진행하려 합니다. width=
    2) 댓글창에서 바로 인증으로 넘어가야 되는데 로그인버튼을 클릭하면 위 그림처럼 /accouts/login/ 계속 이동되서 찾느라 시간이 좀 걸렸습니다.

79.3 socialAdapter.py 상속 - custom

settings.py 에 SOCIALACCOUNT_ADAPTER 설정을 통해 커스텀 adapter.py를 상속 받아 인증단계에 관여 할수 있다고 했습니다.
소셜로그인 인증 직후 정보를 세션에 담기 위해 socialAdapter.py를 만들고 DefaultSocialAccountAdapter 클래스를 상속 받겠습니다.

#myapp/common/CustomAppSocialAccountAdapter.py
from django.conf import settings
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter

class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
    # 로그인후 사용자가 다음을 통해 성공적으로 인증한 직후에 호출
    def pre_social_login(self, request, sociallogin):                    
        login = sociallogin.serialize()
        account   = login.get('account')
        extra_data = account.get('extra_data')
        self.request.session['user_uid'] = account.get('uid')        
        provider = account.get('provider')
        self.request.session['user_provider'] = provider

        user_name = user_profile_imag = ''
        if provider in  ('naver','google')  :
            user_name         = extra_data.get('email') or extra_data.get('name')            
            user_profile_imag = extra_data.get('profile_image') or extra_data.get('picture')
        elif provider == 'kakao':
            kakao_account     = extra_data.get('kakao_account')
            user_name         = kakao_account.get('email') or kakao_account.get('profile').get('nickname')            
            user_profile_imag = kakao_account.get('profile').get('thumbnail_image_url')

        if user_name != None:
            user_name = f'{user_name[:1]}**{user_name[3:]}'

        self.request.session['user_profile_imag'] = user_profile_imag or 'https://res.cloudinary.com/dtfub5xym/image/upload/v1669638871/pb_comment_logo.png'
        self.request.session['user_name'] = user_name
        return settings.LOGIN_REDIRECT_URL

    # 로그인 전  사용자 인스턴스를 추가로 채우는 데 사용
    def populate_user(self, request, sociallogin, data):         
        return settings.LOGIN_REDIRECT_URL
  • pre_social_login :
    1) 인증 후 로그인 한 사용자의 정보가 sociallogin에 담겨있습니다.
    2) account는 프로바이더 정보가 공통으로 들어있습니다. (uid, provider 등)
    3) extra_data 는 프로바이더별로 사용자의 추가 정보가 담겨있음을 확인할 수 있습니다.
    3) 로그인한 유저의 key로 uid를 사용하고 정보를 세션에 담아 인증 확인시 사용합니다.

79.4 댓글 테이블 수정 - models.py

댓글 models.py 필드 변경 및 추가가 많았습니다.
소셜 정보를 받아보니 추가할 필드(uid)도 생겼고, 비슷한 필드명을 사용하기 위해 변경을 진행했습니다.

변경전 모델 : 76.02 모델 만들기 - models.py
변경 후 : 아래 소스

#myapp/common/common_models.py
from django.db import models
from django.core.exceptions import ValidationError

class PyComment(models.Model):
    id = models.AutoField(primary_key=True)
    app_id =  models.IntegerField( blank=False, null=False)   
    app_name = models.CharField(max_length=15, blank=False, null=False)
    provider_name = models.CharField(max_length=35, blank=False, null=False)
    uid = models.CharField(max_length=200, blank=False, null=False)
    comment_body = models.TextField()
    profile_image = models.CharField(max_length=150, blank=False, null=False)
    use_yn = models.CharField(max_length=1, default='Y', blank=False, null=False)
    regist_nm = models.CharField(max_length=30, blank=False, null=False)    
    regist_dt = models.DateTimeField(auto_now=True, blank=False, null=False)

    class Meta:
        ordering = ['-regist_dt']
        db_table = 'py_comment'

    def to_dict(self):
        return {'comment_id':self.id,
                'profile_image': self.profile_image,
                'comment_body':self.comment_body,
                'regist_nm':self.regist_nm,
                'regist_dt':self.regist_dt.strftime('%Y.%m.%d %H:%M:%S'),
               }
  • to_dict : 댓글을 등록 후 결과 데이터를 JSON으로 시리얼라이즈 하려고 추가하였습니다.

79.5 댓글 폼 수정 - form.py

변경 전에는 단순하게 modelfrom을 상속 받아 사용필드만 정의해서 사용했었습니다.
현재는 save(), init등 view에서 처리하던 로직을 form.py에서 처리하도록 변경했습니다.

변경 전: 76.04 댓글 폼 만들기 - form.py
변경 후: 아래소스

#myapp/common/common_forms.py
from django import forms
from .common_models import PyComment
# https://docs.djangoproject.com/en/4.1/ref/forms/api/
class CommentForm(forms.ModelForm):
    class Meta:
        model = PyComment
        fields = ('comment_body','app_name')
        labels = {"comment_body": "",}
        widgets= {'app_name':      forms.TextInput(attrs={'type':'hidden'}),        
                 'comment_body':  forms.Textarea(attrs={'class':'ml-2 mr-5','style':'width:100%','rows':'2','cols':'90','oninput':'auto_height(this)','placeholder':'Join the discussion and leave a comment!'}),
                 }

    def __init__(self, *args, **kwargs):        
        self.app_id = kwargs.pop('app_id', None)
        self.uid = kwargs.pop('uid', None)
        self.provider_name = kwargs.pop('provider_name', None)
        self.profile_image = kwargs.pop('profile_image', None)
        self.regist_nm = kwargs.pop('regist_nm', None)
        super().__init__(*args, **kwargs)

    def clean_comment_body(self):
        return self.cleaned_data['comment_body'].replace('<','&lt;').replace('>','&gt;')

    def save(self, commit=True, comment_id=None, update_fields=None, *args, **kwargs):        
        instance = super().save(commit=False)        
        instance.comment_body = self.cleaned_data['comment_body']
        instance.app_id = self.app_id
        instance.provider_name = self.provider_name
        instance.uid = self.uid
        instance.profile_image = self.profile_image
        instance.regist_nm = self.regist_nm
        instance.id = comment_id

        if commit:
            instance.save(update_fields=update_fields)
        return instance
  • meta : 기본적으로 form에 사용할 필드 정의합니다.
  • init :
    1) fileds에 정의하지 않아 form에 생성되지 않고 사용자에게 입력받지 않지만 db저장에 필요한 필드를 초기화 합니다.
    2) db 저장시점에 get_form_kwargs(self)을 오버라이딩하여 해당 변수(app_id, uid 등)들에 값을 할당하여 사용합니다.
  • def clean_comment_body(self) :
    1) clean_필드명 으로 함수를 만들어 해당 필드만 지정할 수 있습니다.
    2) 추가적인 유효성 검사나 로직을 추가할 수 있습니다. 여기서는 스크립트 실행 방지를 위해 replace을 추가하였습니다.
    3) clean(self) 로 만들어 모든 필드에 대해 추가적인 유효성 검사도 가능합니다.
  • save : db 저장

79.6 LogoutView, urls.py

DJango auth에 내장되어 있는 LogoutView을 이용하여 따로 logout 기능을 만들지 않아도 됩니다.
해당 기능을 이용하여 간단하게 로그인 세션정보를 삭제할 수 있는 편리한 기능입니다.

from django.contrib.auth.views import LogoutView
app_name = "common"
urlpatterns = [     
       ...
       ...
    path('comment/<int:app_id>/add', views.AddComment.as_view(), name='add_comment'),
    path('comment/<int:comment_id>/edit',views.EditComment.as_view(), name='edit_comment'),
    path('comment/<int:comment_id>/del', views.DelComment.as_view(), name='del_comment'),
    path('comment/<int:comment_id>/<int:app_id>/mod', views.ModComment.as_view(), name='mod_comment'),
    path('logout/', LogoutView.as_view(), name='logout'), 

]

79.7 댓글 등록 / 수정 / 삭제- view.py

소스 가독성을 높이기 위해 기존 불필요한 함수들 제거, 결과 메세지, 인증 체크, 인증 데이터 바인딩 등 변경점이 많았습니다.

변경 전 : 댓글 등록 view.py(/blog/82/#333){target=_blank}
변경 후 : 아래소스

class AddComment(generic.FormView):
    form_class = CommentForm

    def get_form_kwargs(self):
        kwargs = super().get_form_kwargs()        
        kwargs['app_id'] = self.kwargs.get('app_id')    
        kwargs['uid'] =self.request.session.get('user_uid',None)
        kwargs['provider_name'] =self.request.session.get('user_provider',None)
        kwargs['profile_image'] =self.request.session.get('user_profile_imag',None)
        kwargs['regist_nm'] =self.request.session.get('user_name',None)         
        return kwargs

    def post(self, request, *args, **kwargs):                               
        if not self.request.user.is_authenticated:
            return JsonResponse({'isSuccess':False,'error_msg':"로그인이 필요합니다."})

        form = self.get_form()
        if form.is_valid():
            comment = form.save()               
            context = {'isSuccess':True}
            context.update(comment.to_dict())
            return JsonResponse(context)
        return JsonResponse({'isSuccess':False,'error_msg': [(k, v[0]) for k, v in form.errors.items()]}) 
  • get_form_kwargs(self): 댓글 등록이 요청이 들어오면 db저장시 필요한 데이터를 셋팅합니다.

변경 전 : 댓글 수정/삭제
변경 후 : 아래소스

class EditComment(generic.FormView):
    form_class = CommentForm
    model = PyComment

    def get_object(self):   
        return PyComment.objects.get(id=self.kwargs.get('comment_id'), use_yn = 'Y')

    def get_initial(self):      
        PyComment = self.get_object()
        return {'comment_body':self.get_object().comment_body,'app_name':self.get_object().app_name}

    def get(self, request, *args, **kwargs):        
        return JsonResponse({'comment_id':self.get_object().id,'form':self.get_form().as_p()})

class ModComment(generic.FormView):
    form_class = CommentForm

    def post(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return JsonResponse({'isSuccess':False,'error_msg':"로그인이 필요합니다."})      
        form = self.get_form()

        if form.is_valid():         
            comment = form.save(comment_id=self.kwargs.get('comment_id'), update_fields=['comment_body'] )                      
            return JsonResponse({'isSuccess':True,'comment_id':self.kwargs.get('comment_id'),'comment_body':comment.comment_body})
        return JsonResponse({'isSuccess':False,'error_msg': [(k, v[0]) for k, v in form.errors.items()]})

class DelComment(generic.View):
    def post(self, request, *args, **kwargs):
        if not self.request.user.is_authenticated:
            return JsonResponse({'isSuccess':False,'error_msg':"로그인이 필요합니다."})

        comment_queryset = PyComment.objects.filter(id =self.kwargs.get('comment_id'),
                                                    uid = self.request.session.get('user_uid',None),
                                                    provider_name = self.request.session.get('user_provider',None),
                                                    use_yn = 'Y')       
        if comment_queryset.count() == 1:           
            cmt = comment_queryset.update(use_yn='N')               
            return JsonResponse({'isSuccess':True,'error_msg':'','comment_id':self.kwargs.get('comment_id')})                       
        return JsonResponse({'isSuccess':False,'error_msg':'삭제를 실패하였습니다.'})

79.8 화면 상세/등록/수정/삭제- comment.html

Ajax를 이용해 만들려고 하니 동적으로 태그를 컨트롤하는 부분이 많이 추가되었습니다.
그냥 수정이든 삭제든 리다이렉트 하면 편할 텐데 말이죠.

load socialaccount 하고 socialaccount_providers를 for문으로 돌리고 provider_login_url에 프로바이더(settings.py SOCIALACCOUNT_PROVIDERS)를 바인딩하여 소셜로그인 가능한 url을 받습니다.

next= 변수로 로그인 이후 돌아올 페이지를 지정할 수 있습니다.

나머지는 ajax, jquery 를 이용하여 만들었습니다.

<!--myapp>templates/base>comment.html -->
{% if pageInfo.id %}
{% load socialaccount %}
{% get_providers as socialaccount_providers %}                                                 
<div class="row">
    <div class="col-lg-8 mb-1">
        <div class="card bg-light">
            <div id="comment_header" class="card-header h5 ">Comments
                <div class="float-right d-inline-block text-right">
                    {% if user.is_authenticated %}
                    <span class="text-sm ">{{ request.session.user_name }}<br/>
                        <a href="{% url 'common:logout' %}?next={{ request.path }}" id="comment_login_btn" class="">로그아웃</a>
                    </span>
                   {% else %}                  
                    {% for provider in socialaccount_providers %} 
                    <button type="button" class="login-btn-icon ml-1" > 
                    <a href="{% provider_login_url provider.id %}?next={{ request.path }}#commentForm" class="{{provider.id}}-icon width-height-100 dis_block"
                    data-toggle="tooltip" data-placement="top" title="" rel="noopener" aria-label="{{provider.id}}" data-original-title="{{provider.id}} Login"
                    ></a>
                    <span class="toast hide">{{provider.id}} Login</span>
                    {% endfor %}                                      
                    {% endif %}
                </div>
                {% if not user.is_authenticated %}
                <div class="float-right font-weight-bold font-weight-bolder text-sm">Login:</div>
                {% endif %}
            </div>

            <div class="card-body">

                <form id="commentForm" class="form-inline" method="POST">
                    {% csrf_token %}
                    {{form.as_p}}
                    <div class="form-group row">                                
                        <button id="comment_submit_btn" class="btn btn-primary ml-4 " type="button" >등록</button>                                                        
                    </div>                    
                </form>                
                <div id="singleComment">
                    {% for comment in comments %}
                    <div id="comment_box_{{comment.id}}" class="d-flex mb-4">
                        <div class=" flex-sm-shrink-0">
                            <img class="rounded-circle" src="{{comment.profile_image}}" alt="{{comment.regist_nm}} {{comment.provider_name}}" width="50" height="50" style="width:50px !important" />
                        </div>

                        <div class="ms-3">
                            <div class="fw-bold">
                                <span class="text-gray-900 font-weight-bolder text-lg">{{ comment.regist_nm }}</span> <span class="text-xs"> {{comment.regist_dt | date:'Y.m.d H:i:s' }}</span>
                                {% if user.is_authenticated and comment.uid == request.session.user_uid %} 
                                <span class="edit btn text-info btn-ssm" data-comment-id="{{comment.id}}" data-action-edit="{% url "common:edit_comment" comment.id %}">update</span>                            
                                <span class="del btn text-info btn-ssm"  data-comment-id="{{comment.id}}" data-action-del= "{% url "common:del_comment"  comment.id %}" >delete</span>
                                {% endif %}

                            </div>
                            <span id="comment_body_{{comment.id}}">
                            {{ comment.comment_body | linebreaks }}
                            </span>
                        </div>
                    </div>
                    {% endfor %}
                </div>                
            </div>
        </div>
    </div>
</div>
<!--Confirm Modal -->
{% comment %} <div class="modal" id="msg_popup" tabindex="-1" role="dialog">
    <div class="modal-dialog modal-dialog-centered" role="document">
        <div class="modal-content">
            <div class="modal-body">
                <!-- MSG Space-->
            </div>
            <div class="modal-footer" id="btn_confirm">
                <button type="button" id="confirm_yes" class="btn btn-primary" data-dismiss="modal" >YES</button>
                <button type="button" id="confirm_no"class="btn btn-secondary" data-dismiss="modal">NO</button>
            </div>
            <div class="modal-footer" id="btn_alert">
                <button type="button" id="alert_ok"class="btn btn-primary" data-dismiss="modal" >OK</button>
            </div>
        </div>
    </div>
</div> {% endcomment %}

{% block js %}
<script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script>
<script>
$(function() {            
    var header_lenght = $(".card-header").length 
    $("#comment_header").addClass('card-header-idx'+header_lenght) //우측 메뉴에서 사용

    $('#comment_submit_btn').click(function() {   
        if('{{ user.is_authenticated }}' != 'True'){
            noticefyAlert('danger', "댓글등록은 로그인이 필요한 서비스입니다.");
            return;
        }

        if($('#id_comment_body').val() == ''){            
            noticefyAlert('danger', "댓글을 입력하세요.");
            return "";
        }

        $('#comment_submit_btn').attr('disabled',true)
        $.ajax({
            url: '{% url "common:add_comment" pageInfo.id %}',
            method:"POST",
            data:$('#commentForm').serialize()
        }).done(function(data) {        
            if(data != null){
                if(data.isSuccess){
                    new_comment  = '<div id="comment_box_'+data.comment_id+'"class="d-flex mb-4">';
                    new_comment += '    <div class=" flex-sm-shrink-0"><img class="rounded-circle" src="'+data.profile_image+'" alt="'+data.regist_nm+' '+data.provider_name+' Profile Image"  width="50" height="50" style="width:50px !important"  /></div>';
                    new_comment += '    <div class="ms-3">';
                    new_comment += '        <div class="fw-bold">';
                    new_comment += '           <span class="text-gray-900 font-weight-bolder text-lg">'+data.regist_nm+'</span>';
                    new_comment += '           <span class="text-xs"> '+data.regist_dt+'</span>';
                    new_comment += '           <span class="edit btn text-info btn-ssm" data-action-edit="/comment/'+data.comment_id+'/edit">update</span>';
                    new_comment += '           <span class="del btn text-info btn-ssm"  data-action-del="/comment/'+data.comment_id+'/del" >delete</span>';
                    new_comment += '       </div>';
                    new_comment += '        <span id="comment_body_'+data.comment_id+'"> <pre>'+data.comment_body+'</pre></span>';                    
                    new_comment += '    </div>';
                    new_comment += '</div>';

                    $('#id_comment_body').val('')                    
                    $('#singleComment').prepend(new_comment); // new comment 끼워넣기
                    $("#comment_box_"+data.comment_id).fadeOut(100).fadeIn(2000);
                    noticefyAlert("success", "success", 'comment_submit_btn');      

                }else{noticefyAlert('danger', data.error_msg, 'comment_submit_btn');}
            }else{noticefyAlert('danger', "transaction fail", 'comment_submit_btn');}
        });        
    });

    $(document).on("click", ".edit", function() {
        noticefyAlert("info","request waiting for editor");
        $.ajax({             
            url:  $(this).attr('data-action-edit'),         
            type: "GET",             
            beforeSend : function(xhr){xhr.setRequestHeader('csrfmiddlewaretoken','{{ csrf_token }}');},
            success: function(data){                
                tForm = '';
                tForm +='<form id="commentFormMod_'+data.comment_id+'" class="form-inline" method="POST">';
                tForm += data.form
                tForm +='   <div class="form-group row">';
                tForm +='       <button class="mod btn btn-primary ml-4" type="button" data-comment-id="'+data.comment_id+'" data-action-mod="/comment/'+data.comment_id+'/{{pageInfo.id}}/mod" >수정</button>';
                tForm +='   </div>';
                tForm +='</form>';

                $('#comment_body_'+data.comment_id).empty();                                
                $('#comment_body_'+data.comment_id).append(tForm);                
            },
            error: function(request, status, error){noticefyAlert('danger', "transaction fail")},            
        });
    });

    $(document).on("click", ".mod", function() {
        $.ajax({
            url: $(this).attr('data-action-mod'),
            method:"POST",
            beforeSend : function(xhr){xhr.setRequestHeader('X-CSRFToken','{{ csrf_token }}');},
            data:$('#commentFormMod_'+$(this).attr('data-comment-id')).serialize()
        }).done(function(data) {
            if(data.isSuccess){            
                noticefyAlert("success","success");
                $('#comment_body_'+data.comment_id).empty(); 
                $('#comment_body_'+data.comment_id).append(data.comment_body);
            }else{noticefyAlert("danger",data.error_msg);}
        });
        return;
    });

    $(document).on("click", ".del", function() {
        $.ajax({
            url:$(this).attr('data-action-del'),
            method:"POST",
            beforeSend : function(xhr){xhr.setRequestHeader('X-CSRFToken','{{ csrf_token }}');},
        }).done(function(data) {               
            if(data.isSuccess){
                $('#comment_box_'+data.comment_id).remove();
                noticefyAlert("success","success");            
            }else{noticefyAlert("danger",data.error_msg);}     
        });
        return;
    });
});
</script>
{% endblock js %}
{% endif %}

79.9 적용확인

네이버, 카카오, 구글 모두 등록이 잘됩니다.
이젠 소셜로그인에 쉽게쉽게 업체를 추가하면 되겠습니다.

네이버 로그인은 실제 적용할려면 검수 승인 받아야 된다고 하네요...

현재글 : 79 Django 소셜로그인 인증활용하여 댓글기능 만들기
Comments
Login:

p**314@gmail.com google
p**314@gmail.com 2023.08.01 16:03:26

구글 로그인 후 댓글입력

a**sk393939@naver.com naver
a**sk393939@naver.com 2022.12.05 17:14:00

네이버 로그인 후 댓글입력입니다.

Copyright © PythonBlog 2021 - 2022 All rights reserved
Mail : PYTHONBLOG