2018년 12월 31일 월요일

[django] django-allauth 를 사용하기

핵심흐름
0. 소셜로그인의 배경 이해
1. 필요한 lib설치 setting.py / urls.py / migrate 해주기
2. 소셜 어플리케이션 만들기 (예, 구글 구글 소셜로그인)
3. admin 페이지에서 소셜로그인에 필요한 정보 저장
4. 테스트 로그인 페이지를 만든다 (http://whatisthenext.tistory.com/129 에 있는것이 간단하여 그것을 이용 - 약간변형)
5. allauth의 template 변경
6. email confirmation을 위한 setting.py 설정
7. UserModel의 확장

관련 페이지


0. 소셜로그인 및 allauth의 배경 이해
소셜로그인은 facebook, 구글 아이디 등으로 내사이트의 접근을 가능하게 하는것이다. 
내사이트 입장에서는 1) 소셜 로그인으로 접근하는 사용자와 그냥 2) 내사이트에 가입하는 사용자로 구분이 되게 되는데, 기존에 django에서 디폴트로 제공하는 login 기능 (login/logout/회원가입 등등) 을 제공하는것처럼 allauth를 사용하면 위의 두개를 동시에 지원 하게 된다. 물론 2)은 기존의 django의 디폴트 login 기능을 내부적으로 이용한다고 생각 하면 된다. 
소셜 로그인을 할때, 기본적으로 내사이트에서 관리 하는 아이디와 소셜 로그인 성공후에 내사이트에서 아이디를 하나 만들어 성공한 소셜 로그인 아이디와 연결해 놓는 구성을 한다. 이때 소셜로그인이후의 내사이트에서 관리 하는 아이디는 자동으로 만들어 지게 할수도 있고, 아니면 소셜 로그인 직후에 내사이트에서 관리 할 아이디를 직접 지정 할수도 있다.

allauth를 사용하면 위 모든 것을 간단히 해결 할수 있고, 관리가 쉬워진다.


1. 필요한 lib설치 setting.py / urls.py / migrate 해주기
위 관련 페이지를 참고 해서 위 setting.py 파일을 설정하고, urls.py 설정한다.

setting.py

INSTALLED_APPS
TEMPLATES
AUTHENTICATION_BACKENDS

# 등록하지 않으면,각 요청 시에 host명의 Site 인스턴스를 찾습니다.
SITE_ID =1
# 이메일 확인을 하지 않음.
SOCIALACCOUNT_EMAIL_VERIFICATION = 'none'
LOGIN_REDIRECT_URL = "/"
ACCOUNT_LOGOUT_REDIRECT_URL = "/"



urls.py
url(r'^accounts/', include('allauth.urls')),  include 시에는 url pattern 끝에 $를 붙이면 안됨/ 기존에 django 디폴트 로그인의 accounts가 위처럼 대체 된다.



이렇게 하고 migrate를 반드시 해줘야 한다. migrate를 하게 되면 소셜 관련된
소셜계정
소셜어플리케이션 등에 대한 table 이 추가 된다.

만약 소셜 로그인에 성공하면, 성공한 계정을 내사이트 기준으로 계정을 하나 만들어서 연결 하게 되는데, 그 정보가 소셜 계정 테이블에 저장되고 동시에 django 디폴트 login 테이블에도 저장 된다. sns 연결을 위한 소셜 어플리케이션의 정보는 소셜 어플리케이션 table에 추가 된다.


2. 소셜 어플리케이션 만들기 (예, 구글 구글 소셜로그인)

로 가서 소셜 어플리케이션을 만든다.
소셜 어플리케이션을 만들면 중요한것은 key와 secret 두가지 값이 생긴다.
그리고 중요한것은 승인후에 리다이렉션 될 URL을 정해 주는것이다.

“승인된 리디렉션 URI” 에 다음과 같이  http://localhost:8000/accounts/google/login/callback/

만약 127.0.0.1 로 test서버를 운영 중이면 이참에 localhost로 allow_host를 setting 파일에 설정 해준다. (localhost를 제안하는 이유는 facebook은 127.0.0.1으로 소셜 로그인 테스트가 동작 하지 않기 때문에)



3. admin 페이지에서 소셜로그인에 필요한 정보 저장

django admin에 들어가서 소셜 어플리케이션 버튼을 눌러 key, secret 등의 정보를 입력 해준다.
site는 접속을 시도 하는 site 이므로 127.0.0.1 과 localhost 를 입력 해준다. example.com이 default이므로 총 3개가 된다.


4. 테스트 로그인 페이지를 만든다 
http://whatisthenext.tistory.com/129 을 참고.
login_test.html

{% load socialaccount %}
<body>
<h1>hello world</h1>
{% if user.is_authenticated %}
    <span>{{ user }}님이 로그인중입니다.</span>
    {% for account in user.socialaccount_set.all %}
        {% comment %} show avatar from url {% endcomment %}
        <h2 style="text-transform:capitalize;">{{ account.provider }} account data</h2>
        <p><img width="50" height="50" src="{{ account.get_avatar_url }}"/></p>
        <p>UID: <a href="{{ account.extra_data.link }}">{{ account.uid }}</a></p>
        <p>Username: {{ account.extra_data.name }}</p>
        <p>First Name: {{ account.extra_data.given_name }}</p>
        <p>Last Name: {{ account.extra_data.family_name }}</p>
        <p>Dashboard Link:
            <a href="{{ account.extra_data.link }}">{{ account.extra_data.link }}</a></p>
    {% empty %}
        <p>you haven't any social account please</p>
    {% endfor %}
{% endif %}
<h2><a href="/accounts/login">로그인</a></h2>
<h2><a href="{% provider_login_url 'facebook' method='oauth2' %}">페이스북 로그인</a></h2>
<h2><a href="{% provider_login_url 'google' method='oauth2' %}">구글 로그인</a></h2>
<h2><a href="{% provider_login_url 'naver' method='oauth2' %}">네이버 로그인</a></h2>
<h2><a href="{% provider_login_url 'kakao' method='oauth2' %}">카카오 로그인</a></h2>
<h2><a href="/accounts/logout">로그아웃</a></h2>
<h2><a href="/accounts/signup">회원가입</a></h2>
</body>
cs


5. allauth의 template 변경 
4번까지 하면, 기본적인 소셜 로그인 기능들이 다 되게 된다.  그런데 여기서 이용하였던 테스트 페이지(login_test.html)는 이것은 기본적인 확인 용이고, 실제 accounts/login   accounts/logout 등을 하면 만들지도 않았던 templates, view등 바탕으로 기능이 동작 한다. 특히 accounts/login의 경우 일반 계정 로그인과 소셜계정 로그인을 동시에 가능하도록 화면을 보여준다. 기능은 환상적이다.

기능적으로는 환상적이지만, 화면이 영 별로 므로 accounts/login등의 화면을 바꾸고 싶은 생각이 들것이다.

이유는 allauth의 기본 view와 기본 template를 사용하기 때문에, 이 template를 고치려면 기본 아이디어는 기본 template에 같은 이름으로 필요한 template 파일(html)을 구성해 놓으면 allauth의 template를 읽어서 사용 하기 전에 내가 구성해 놓은 template를 사용 한다는 것이다. 일종의 template 파일 override 이를 위해서,

setting.py를 설정 하고,
TEMPLATES = [
    {
        'BACKEND''django.template.backends.django.DjangoTemplates',
        'DIRS': [ os.path.join(BASE_DIR, 'templates') , os.path.join(BASE_DIR, 'templates''allauth')],  # 추가
        'APP_DIRS': True,
cs


BASE_DIR에

templates/allauth를 경로를 만들고,
/Python/Python37/Lib/site-packages/allauth/templates 에 있는 필요 파일들을 위 경로로 복사한다.  그럼 setting.py 에 설정해 놓은 os.path.join(BASE_DIR, 'templates''allauth')
로 인해 template 파일의 변경을 할수 있다.


참고,



6. email confirmation을 위한 setting.py 설정

allauth를 사용 하면 일반 계정 가입을 하는 경우는 가입자가 정상적인 가입자인지 확인하기 위해 email confirmation을 하고, 또한 소셜 로그인을 처음 하는 경우는 내사이트 기준의 계정이 필요 하게 되는데 기본은 자동생성이고, 아래와 같이 옵션을 정해주면 처음 방문자는 일종의 내사이트용 회원 가입처럼 가입 계정을 입력 하게 하고, 소셜로그인을 할때 입력 된 계정으로 사용자를 관리하게 된다. 안그러면 자동으로 아이디를 만들어서 알아서 입력 한다.

setting.py

ACCOUNT_AUTHENTICATION_METHOD = 'email'
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = 'mandatory'
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 300

SOCIALACCOUNT_AUTO_SIGNUP = False



7. UserModel의 확장
소셜로그인에서 회원 가입시 정보가 부족 하다 싶으면 User 모델의 확장을 할수 있는데 아래의 사이트를 참고 하여 UserModel을 확장하게 되면 사용자의 추가 정보를 받아 들여 사이트를 운영 할수 있다.


  • https://wikidocs.net/6651
  • https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html




[django] 아임포트 결제 사용 하기

핵심흐름
1. 결제 테스트 페이지 작성 (blank 페이지 하나 만든다)
2. 아임포트 가입
3. 아임포트 관리자 화면에서 PG사 선택
4. 결제 테스트 페이지에 아임포트 라이브러리 추가
5. 결제 요청을위한 javascript 추가 (기본동작확인 완료)
6. 결제 결과를 검증 (아임포트로 REST API를 이용하여 다시 호출 하여 확인)

관련 페이지
https://docs.iamport.kr/
https://www.iamport.kr/getstarted
https://admin.iamport.kr/settings
https://github.com/iamport/iamport-manual/blob/master/%EC%9D%B8%EC%A6%9D%EA%B2%B0%EC%A0%9C/README.md (상세메뉴얼)


1. 결제 테스트 페이지 작성

  • blank 페이지 하나 만든다


2. 아임포트 가입
3. 아임포트 관리자 화면에서 PG사 선택
  • 관련페이지(https://www.iamport.kr/getstarted 등) 를 참고 하여 진행한다.


4. 결제 테스트 페이지에 아임포트 라이브러리 추가

간단한 결제 테스트 페이지 를 만들어 놓고, 아임포트 라이브러리 (javascript cdn) 를 추가 한후

<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-x.y.z.js"></script>


5. 결제 요청을위한 javascript 추가 (기본동작확인 완료)

상세메뉴얼(https://github.com/iamport/iamport-manual/blob/master/%EC%9D%B8%EC%A6%9D%EA%B2%B0%EC%A0%9C/README.md)  2.1의

IMP.request_pay({
    pg : 'html5_inicis',
    pay_method : 'card',
    merchant_uid : 'merchant_' + new Date().getTime(),
    name : '주문명:결제테스트',
    amount : 14000,
    buyer_email : 'iamport@siot.do',
    buyer_name : '구매자이름',
    buyer_tel : '010-1234-5678',
    buyer_addr : '서울특별시 강남구 삼성동',
    buyer_postcode : '123-456'
}, function(rsp) {
    if ( rsp.success ) {
        var msg = '결제가 완료되었습니다.';
        msg += '고유ID : ' + rsp.imp_uid;
        msg += '상점 거래ID : ' + rsp.merchant_uid;
        msg += '결제 금액 : ' + rsp.paid_amount;
        msg += '카드 승인번호 : ' + rsp.apply_num;
    } else {
        var msg = '결제에 실패하였습니다.';
        msg += '에러내용 : ' + rsp.error_msg;
    }

    alert(msg);
});

을 결제 테스트 페이지에 추가 하되,  pg를 관리자를,  3. 아임포트 관리자 화면에서 PG사 선택 에서 선택한데로 적당히 잘 셋팅해줘야 한다.

이렇게 해서 연동 여부 바로 확인. 핵심흐름 5번까지 하는데 큰무리 없음.


6. 결제 결과를 검증 (아임포트로 REST API를 이용하여 다시 호출 하여 확인)

핵심흐름 6번이 문제이면서 잘 안되었던 부분 (아임포트 사이트에 너무 대충 나와있는듯)

6번을 하는 이유는 살펴보자.

내사이트(우리), 아임포트, PG사 세가지 서버가 존재 한다.

아임포트는 우리(내사이트)의 요청을 받으면 그것을 실제 PG사로 릴레이 해주고 결과를 받아서 아임포트 서버에 저장 하고, 다시 우리(내사이트)에게 알려주는 역할을 한다.  다시 말해 아임포트는 단지 연결을 쉽게 해주는 역할을 한다.
그런데 이 연결 과정에서 문제가 생겨 마지막에 내사이트(우리)에서 결제하려고 했던 금액과 틀린 금액이 아임포트에 저장 되면 문제가 되므로, 내사이트(우리)에서 아임포트쪽 서버에 REST API를 통해 잘 저장이 된것지를 호출하여 확인하는 검증 과정이 필요하다.

그러므로, 위의 rsp.success 이후에,



IMP.request_pay({
    pg : 'inicis',
    pay_method : 'card',
    merchant_uid : 'merchant_' + new Date().getTime(),
    name : '주문명:결제테스트',
    amount : 14000,
    buyer_email : 'iamport@siot.do',
    buyer_name : '구매자이름',
    buyer_tel : '010-1234-5678',
    buyer_addr : '서울특별시 강남구 삼성동',
    buyer_postcode : '123-456'
}, function(rsp) {
    if ( rsp.success ) {
     //[1] 서버단에서 결제정보 조회를 위해 jQuery ajax로 imp_uid 전달하기
     jQuery.ajax({
      url: "/payments/complete", //cross-domain error가 발생하지 않도록 동일한 도메인으로 전송
      type: 'POST',
      dataType: 'json',
      data: {
       imp_uid : rsp.imp_uid
       //기타 필요한 데이터가 있으면 추가 전달
      }
     }).done(function(data) {
      //[2] 서버에서 REST API로 결제정보확인 및 서비스루틴이 정상적인 경우
      if ( everythings_fine ) {
       var msg = '결제가 완료되었습니다.';
       msg += '\n고유ID : ' + rsp.imp_uid;
       msg += '\n상점 거래ID : ' + rsp.merchant_uid;
       msg += '\결제 금액 : ' + rsp.paid_amount;
       msg += '카드 승인번호 : ' + rsp.apply_num;

       alert(msg);
      } else {
       //[3] 아직 제대로 결제가 되지 않았습니다.
       //[4] 결제된 금액이 요청한 금액과 달라 결제를 자동취소처리하였습니다.
      }
     });
    } else {
        var msg = '결제에 실패하였습니다.';
        msg += '에러내용 : ' + rsp.error_msg;

        alert(msg);
    }
});


와 같이 큰 흐름으로 구성이 되고,  그 실제 검증 과정을 할 페이지/Json response 페이지는 내사이트(우리) 에서 만들어서 제공 하고 그 이름은 /payments/complete가 된다.

결국 결제 연동인 성공한 경우 (rsp.success) ajax로 호출을 하는데, 이것은 내사이트에 /payments/complete  url을 만들고 ajax response를 만들어 주라는 의미이고, 그 response를 만드는 과정에서, 엑세스 토큰을 만들고, 결제정보를 조회하고, 그걸로 db에 필요한 정보를 넣고, 완료가 되면 다시 결제 테스트 페이지 로 돌아 와서  everythings_fine 이후의 것들을 실행 하는 구조이다.

그러므로 우리는 /payments/complete 를 url의 기능(json response)을 만들어 주어야 한다.
이것을 이해하는데 좀 시간이 걸림.

이것을 하는 과정중,
어려웠던점은 django  csrf 검증 때문에,  url: "/payments/complete"  을 

url: "/payments/complete/"
로 해줘야 했고, template view를 못쓰고(해보려고 했는데 잘 안되서)

urls.py
1
url(r'^payment_test/$', TemplateView.as_view(template_name='payment_test.html'), name='payment_test'),url(r'^payments/complete/$', views.payment_complete, name='payment_complete'),
cs

views.py
1
2
@csrf_exempt
def payment_complete(request):
cs

로 함수로 구성 하고 decorator를 넣어 주었다.


그리고, 나머지는 관련페이지중 https://docs.iamport.kr/implementation/payment 를 참고하여,  django형태에 맞게 아래와 같이 구성하였다. (django를 쓰시는 분들에게는 도움이 되길)

@csrf_exempt
def payment_complete(request):
    if request.method == 'POST' and request.is_ajax():
        imp_uid = request.POST.get('imp_uid')
        # // 액세스 토큰(access token) 발급받기
        data = {
            "imp_key""X071XXXXXX52038",
            "imp_secret""MlXXXXXXnhP7plaPGe6NXXaXXXX9ocqekQAuFXXXwXXXXXgrZ5n9GELvNaIdp24ZwJhfvm"
        }
        response = requests.post('https://api.iamport.kr/users/getToken', data=data)
        data = response.json()
        my_token = data['response']['access_token']
        #  // imp_uid로 아임포트 서버에서 결제 정보 조회
        headers = {"Authorization": my_token}
        response = requests.get('https://api.iamport.kr/payments/'+imp_uid, data=data, headers = headers)
        data = response.json()
        # // DB에서 결제되어야 하는 금액 조회 const
        order_amount = 100
        amountToBePaid = data['response']['amount']  # 아임포트에서 결제후 실제 결제라고 인지 된 금액
        status = data['response']['status']  # 아임포트에서의 상태
        if order_amount==amountToBePaid:
            # DB에 결제 정보 저장
            # await Orders.findByIdAndUpdate(merchant_uid, { $set: paymentData}); // DB에
            if status == 'ready':
                # DB에 가상계좌 발급정보 저장
                return HttpResponse(json.dumps({'status'"vbankIssued"'message'"가상계좌 발급 성공"}),
                                    content_type="application/json")
            elif status=='paid':
                return HttpResponse(json.dumps({'status'"success"'message'"일반 결제 성공"}),
                                    content_type="application/json")
            else:
                pass
        else:
            return HttpResponse(json.dumps({'status'"forgery"'message'"위조된 결제시도"}), content_type="application/json")
    else:
        return render_to_response('payment_complete.html', locals())   #수정 필요
cs



그리고, 결제 테스트 페이지에 실제 구성에 done, fail, always등을 같이 구성해서 debugging을 쉽게 하였다.

<script>
    IMP.init('impX2XXXX03');
    IMP.request_pay({
        pg: 'danal_tpay',
        pay_method: 'card',
        merchant_uid: 'merchant_' + new Date().getTime(),
        name'주문명:결제테스트',
        amount: 100,
        buyer_email: 'iamport@siot.do',
        buyer_name: 'yellowdonkey',
        buyer_tel: '010-1234-5678',
        buyer_addr: '서울특별시 강남구 삼성동',
        buyer_postcode: '123-456'
    }, function (rsp) {
        if (rsp.success) {
            //[1] 서버단에서 결제정보 조회를 위해 jQuery ajax로 imp_uid 전달하기
            jQuery.ajax({
                url: "/payments/complete/"//cross-domain error가 발생하지 않도록 동일한 도메인으로 전송
                type: 'POST',
                dataType: 'json',
                data: {
                    imp_uid: rsp.imp_uid
                    //기타 필요한 데이터가 있으면 추가 전달
                },
            }).done(function (data) {
                //[2] 서버에서 REST API로 결제정보확인 및 서비스루틴이 정상적인 경우
                alert("ajax done");               
                console.log("ajax done");
                if (data.status=='success') {
                    var msg = '결제가 완료되었습니다.';
                    msg += '\n고유ID : ' + rsp.imp_uid;
                    msg += '\n상점 거래ID : ' + rsp.merchant_uid;
                    msg += '\결제 금액 : ' + rsp.paid_amount;
                    msg += '카드 승인번호 : ' + rsp.apply_num;
                    alert(msg);
                    console.log(msg);
                } else {
                    //[3] 아직 제대로 결제가 되지 않았습니다.
                    //[4] 결제된 금액이 요청한 금액과 달라 결제를 자동취소처리하였습니다.
                    var msg = '아직 제대로 결제가 되지 않았습니다.';
                    alert(msg);
                    console.log(msg);
                }
            }).fail(function () {
                alert("ajax fail");
                console.log("ajax fail");
            }).always(function () {
                alert("ajax always");
                console.log("ajax always");
            });
        } else {
            var msg = '결제에 실패하였습니다.';
            msg += '에러내용 : ' + rsp.error_msg;
            alert(msg);
        }
    });
</script>
cs