| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Tags
- sql
- Commpot
- channels
- 2주차
- 백준
- REDIS
- github
- vscode
- 마스킹
- 채팅
- poetry
- 프로젝트
- WIL
- 정보처리기사
- Class
- 장고
- 정보처리기사실기
- Git
- resnet50
- 1주차
- 미니프로젝트
- 알고리즘
- WHERE절
- re-id
- 가상환경
- 프로그래머스
- 파이썬
- 개발일지
- js
- WebSocket
Archives
- Today
- Total
개발일기
[결제] KG이니시스 결제 구현하기 (feat. 포인트충전) 본문
결제에서 제일 중요한 것은 사전검증과 사후 검증이다.
먼저 결제화면을 만들어보자~
- 결제화면 코드
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="static/css/pointcharge.css" />
<title>ChocoTheCoo</title>
<!-- Bootstrap -->
<!-- jQuery -->
<!-- iamport.payment.js -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.2.0.js"></script>
</head>
<!-- header -->
<header></header>
<!-- body -->
<body>
<div class="title-container">
<div class="image-box"></div>
</div>
<div class="section">
<div class="card text-center" id="card">
<div class="card-header">
포인트 충전하기
</div>
<img class="help-text" src="/static/images/helpbtn.png" data-bs-toggle="tooltip" data-bs-html="true"
data-bs-placement="bottom" data-bs-title="포인트 충전은, 카카오페이로 할 시 24시간 내에 반드시 환불처리됩니다.<br/> 걱정말고 테스트해주세요~">
<div class="card-body">
<h5 class="card-title mb-4">😎😎포인트를 선택하세요😎😎</h5>
<!--테스트목적-->
<div><input value="100" class="btn btn-primary mb-2" id="100"></div>
<!--아래는 실제 사용-->
<div><input value="3000" class="btn btn-primary mb-2" id="3000"></div>
<div><input value="5000" class="btn btn-primary mb-2" id="5000"></div>
<div><input value="10000" class="btn btn-primary mb-2" id="10000"></div>
<div><input value="30000" class="btn btn-primary mb-2" id="30000"></div>
<div><input value="50000" class="btn btn-primary mb-2" id="50000"></div>
<div><input value="100000" class="btn btn-primary mb-2" id="100000"></div>
</div>
</div>
</div>
</body>
<!-- footer -->
<footer></footer>
<!-- script -->
</html>

100원을 만든 이유는 테스트를 하기 위함이다.. 테스트 결제이기 때문에 자정이면 환불이 된다.
- 결제 검증하기
결제금액의 위변조 검증 이유:
- 결제 요청은 클라이언트 환경에서 이루어지기 때문에 별도의 검증을 하지 않으면 클라이언트가 스크립트를 조작해 금액을 위 변조하여 결제를 요청할 수 있다.
- 따라서 결제하고자 하는 상품의 금액과 실제로 결제된 금액을 반드시 검증해야 한다.
- 예를 들어 420,000원짜리 상품을 결제할 때에는 amount: 420000으로 결제요청을 하게 되는데, 공격자가 스크립트를 조작하여 해당 속성을 실제 금액보다 낮은 값(예 amount: 420)으로 변조할 수 있다.
- 클라이언트에서의 스크립트 조작은 원천적으로 막을 수 없는 기술적 특징이 있기 때문에 결제 전후로 서버에서 결제금액의 위변조 여부를 반드시 검증해야 한다.
- 결제 사전검증하기
결제정보 사전 검증은 클라이언트 변조를 원천적으로 차단하기 위한 필수 절차이다.
결제창을 띄우는 프론트엔드를 보여주기 전에 어떤 주문번호로 얼마만큼의 결제가 이루어져야 하는지를 아래의 API를 사용하여 사전에 등록할 수 있다.
https://api.iamport.kr/payments/prepare
내 경우 여기에 더해, 프로젝트 db에도 요청 온 결제정보를 저장하도록 했다.
이유는 사후 검증 때, 사전검증 때 저장한 정보와 비교하여 정상적인 결제인지 아닌지를 판단하기 위해서이다.
또한 사전 검증 단계에서 가맹점 주문번호를 백엔드단에서 커스텀해 프론트로 제공하고 있다. (확실한 보안목적..!!)
# views.py
class PointCheckoutView(APIView):
permission_classes = [IsAuthenticated]
@transaction.atomic
def post(self, request, *args, **kwargs):
user = request.user
amount = request.data.get('amount')
payment_type = request.data.get('payment_type')
try:
trans = PayTransaction.objects.create_new(
user=user,
amount=amount,
payment_type=payment_type
)
except:
trans = None
if trans is not None:
data = {
"works": True,
"merchant_id": trans
}
return JsonResponse(data)
else:
return JsonResponse({}, status=status.HTTP_400_BAD_REQUEST)
# models.py Paytransaction
class PayTransaction(CommonModel):
"""결제 정보가 담기는 모델"""
user = models.ForeignKey(
"users.User", related_name="point_data", on_delete=models.CASCADE
)
transaction_id = models.CharField(verbose_name="imp결제고유번호", max_length=120, null=True, blank=True)
order_id = models.CharField(verbose_name="주문번호", max_length=120, unique=True)
amount = models.PositiveIntegerField(default=0)
# 해외 payment 쓸거면 DecimalField으로 바꿔야함..!!
# amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
success = models.BooleanField(default=False)
transaction_status = models.CharField(max_length=220, null=True, blank=True)
payment_type = models.CharField(max_length=120)
objects = TransactionManager()
def __str__(self):
return self.order_id
class Meta:
ordering = ["-created_at"]
class TransactionManager(models.Manager):
# 새로운 트랜젝션 생성
def create_new(self, user, amount, payment_type, success=None, transaction_status=None):
if not user:
raise ValueError("유저가 확인되지 않습니다.")
# 암호화 => 유니크한 주문번호 생성
short_hash = hashlib.sha1(str(random.random()).encode()).hexdigest()[:2]
time_hash = hashlib.sha1(str(int(time.time())).encode()).hexdigest()[-3:]
base = str(user.email).split("@")[0]
key = hashlib.sha1((short_hash + base + time_hash).encode()).hexdigest()[:10]
new_order_id = str(key) # "%s" % (key)
# 아임포트 결제 사전 검증 단계
# iamport.py에서의 validation_prepare 함수 호출
validation_prepare(new_order_id, amount)
# db에 정보 저장
new_trans = self.model(
user=user,
order_id=new_order_id,
amount=amount,
payment_type=payment_type
)
if success is not None:
new_trans.success = success
new_trans.transaction_status = transaction_status
try:
new_trans.save()
except Exception as e:
print("저장 오류", e)
return new_trans.order_id
# 생성된 트랜잭션 검증
def validation_trans(self, imp_id):
result = get_transaction(imp_id)
if result["status"] == "paid":
return result
else:
return None
def all_for_user(self, user):
return super(TransactionManager, self).filter(user=user)
def get_recent_user(self, user, num):
return super(TransactionManager, self).filter(user=user)[:num]
결제 사전검증 url로 요청이 오면, 유니크한 가맹점주문번호를 생성하고, user, amount, payment_type을 담아서 db에 저장한다. validation_prepare() 함수로 https://api.iamport.kr/payments/prepare로 사전등록을 할 수 있다.
# iamport.py
import requests
import json
from django.conf import settings
# get_access_token: 아임포트 서버에 접근할 수 있는 토큰을 발급
def get_access_token():
access_data = {
'imp_key': settings.IAMPORT_KEY,
'imp_secret': settings.IAMPORT_SECRET
}
url = "https://api.iamport.kr/users/getToken"
req = requests.post(url, data=access_data)
access_res = req.json()
if access_res['code'] == 0:
return access_res['response']['access_token']
else:
return None
# 결제를 검증하는 단계
def validation_prepare(merchant_id, amount, *args, **kwargs):
access_token = get_access_token()
if access_token:
access_data = {
'merchant_uid': merchant_id,
'amount': amount
}
url = "https://api.iamport.kr/payments/prepare"
headers = {
'Authorization': access_token
}
req = requests.post(url, data=access_data, headers=headers)
res = req.json()
if res['code'] != 0:
raise ValueError("API 통신 오류")
else:
raise ValueError("토큰 오류")
- 결제 사후검증하기
# 결제 사후검증
class PointImpAjaxView(APIView):
permission_classes = [IsAuthenticated]
@transaction.atomic
def post(self, request, *args, **kwargs):
user = request.user
merchant_id = request.data.get('merchant_id')
imp_id = request.data.get('imp_id')
amount = request.data.get('amount')
try:
# db에 정보가 존재하는지 확인
trans = PayTransaction.objects.get(
user=user,
order_id=merchant_id,
amount=amount
)
except:
trans = None
if trans is not None:
try:
# 스크립트에서 정보 조작을 할 경우를 대비하여
# 포인트는 db에 저장되어있는 금액으로 생성한다.
Point.objects.create(user=user, point_type_id=5, point=trans.amount)
trans.transaction_id = imp_id
trans.transaction_status = "paid"
trans.success = True
trans.save()
data = {
"works": True
}
return JsonResponse(data)
except:
return JsonResponse({}, status=status.HTTP_400_BAD_REQUEST)
else:
return JsonResponse({}, status=status.HTTP_400_BAD_REQUEST)
이렇게 결제에 대한 백엔드처리과정을 살펴보았다.
결제는 보안에 주의해야하기 때문에 꼭 사전검증과 사후검증 과정을 구현하도록 하자!!
아래 참고자료 중 포트원 결제연동 Docs가 있다.
결제 테스트계정 만드는 법부터 결제 검증까지 가이드가 잘 나와있다.
앞으로 개선할 점:
- 코드 가독성 높이기
- 코드를 작성한 나로서는.. 내 코드를 이해하기가 어렵지 않지만,
코드리뷰를 하며 코드가 많이 복잡하구나 싶었다ㅜㅜ - 아무래도 파일 3곳에 결제코드가 작성되어있다보니 보기 불편한 것 같아서 코드 가독성을 높여야겠다.
(코드를 작성할 때만 해도 확장성 고려 및 사용목적에 따라 분류한 것이었지만, 합체해도 문제없을 것 같다.)
'Project Portfolio' 카테고리의 다른 글
| [channels] 실시간 단체채팅방 구현하기(1) (0) | 2023.06.19 |
|---|---|
| [구독 해지예약 및 취소, 구독 재신청 구현] (0) | 2023.06.10 |
| [Django] Schedule과 crontab 사용하기 (0) | 2023.06.09 |
| [JS] 달력 날짜 누르면 날짜에 해당하는 저축, 소비, 지출 불러오기 (0) | 2023.05.29 |
| [JS] 달력 만들기 (0) | 2023.05.26 |