처음부터 차근차근

아임포트 결제 API 사용해서 카카오페이 결제기능 구현하기 본문

Framework/Laravel

아임포트 결제 API 사용해서 카카오페이 결제기능 구현하기

_soyoung 2022. 6. 27. 03:08
반응형

아임포트

https://www.iamport.kr/

 

온라인 비즈니스의 모든 결제를 한곳에서, 아임포트

결제의 시작부터 비즈니스의 성장까지 아임포트와 함께하세요

www.iamport.kr

아임포트(iamport)는 국내 PG결제 연동을 쉽게해주는 결제 API 이다.

여기서 PG란 Payment gateway의 약자인데, 신용카드사와 직접 계약하기 어려운 온라인 쇼핑몰을 대신해 결제와 정산 업무를 대행해 주는 업체이다.

그래서 PG사와 계약을 하면 카드결제, 휴대폰 결제, 계좌이체, 무통장입금 등 다양한 결제 수단을 쇼핑몰 방문 고객에게 제공할 수 있다.

 

아임포트를 사용하기 전과 후 결제 요청 과정의 차이는 아래와 같다.

아임포트 사용하기 전 결제 요청 과정

 

아임포트를 사용하지 않으면 쇼핑몰 서버에서 직접 PG사에 결제 요청을 해야한다.

아임포트 사용한 후 결제 요청 과정

아임포트를 사용하면 아임포트 서버가 쇼핑몰 서버를 대신해서 PG사에 결제 요청을 한다.

쇼핑몰 웹 서버는 나중에 결제 정보 결과만 아임포트를 통해 받는다.

이렇게 아임포트를 사용하면 웹 서버의 부담도 줄이고, PG사에 직접 결제를 요청하는 코드를 작성하지 않아도 돼서 개발하기도 편리하고, 또한 api를 가져다 쓰는 것이기 때문에 유지보수 하기도 편리하다.

 

라라벨에는 결제 기능을 구현하는 데 도움을 주는 Stripe라는 Laravel Cashier 패키지를 제공한다. https://laravel.kr/docs/8.x/billing

 

라라벨 8.x - 캐셔 (Stripe)

라라벨 한글 메뉴얼 8.x - 캐셔 (Stripe)

laravel.kr

하지만 아무래도 외국에서 만들어진 패키지이다 보니까 국내 서비스에 친화적이지 못하다는 단점이 있다.

그래서 비교적 api를 가져다쓰기 편한 아임포트로 결제 기능을 구현해보았다.

 

 

준비

api를 사용하기 전에 해야할 사항들은 아임포트 공식문서에 잘 나와있다.

https://docs.iamport.kr/prepare

 

아임포트 docs

준비하기 이 문서는 아임포트를 사용하여 결제를 연동하기 위한 회원가입과 설정 과정을 설명합니다. STEP1아임포트 회원가입하기 먼저, 결제 연동을 위해서 아임포트에 가입합니다. 아임포트는

docs.iamport.kr

 

api를 사용하려면 먼저 아임포트 사이트에 가입을 해야하고,

그 다음 아임포트 관리자 페이지로 들어가서 PG사 선택하고, 테스트 모드 슬라이드를 on으로 설정하면 끝이다.

관리자 페이지 바로가기 링크 --> https://classic-admin.iamport.kr/ 

 

관리자 페이지 내정보

관리자 페이지 -> 시스템설정 -> 내정보 에 가보면 가맹점 식별코드, REST API 키, secret 값 들이 있는데 이 값들이 api 사용할 때 써야하는 값들이다.

 

 

구현

아임포트의 결제 api는 다양한 언어를 지원한다. 

https://guide.iamport.kr/418a8dd5-6b84-419d-8d1b-ffd311b36404

 

개발자 가이드

지원하는 언어 및 웹빌더

guide.iamport.kr

 

아임포트 일반결제 공식문서(https://docs.iamport.kr/implementation/payment)를 보면 따라하기 쉽게 되어있는건 자바스크립트와 node.js를 이용한 방법밖에 없어서 라라벨로 구현하느라 힘들었다..

 

 

결제 과정

결제 요청(결제 정보 전달)  -> 결제 -> 결제 성공/실패 응답 처리 -> 결제 정보 검증, 저장

만약 결제 정보를 검증을 했는데 올바른 결제가 아니라면 환불 코드를 통해 환불을 하고, 클라이언트에게 올바를 결제가 아니라는 정보를 알린다.

 

 

구현코드

html

<button id="pay_btn" onclick="requestPay({{ auth()->check() }})">결제하기</button>

 

 

javascript

사용할 라이브러리들

<!-- jQuery -->
<script type="text/javascript" src="https://code.jquery.com/jquery-1.12.4.min.js" ></script>
<!-- iamport.payment.js -->
<script type="text/javascript" src="https://cdn.iamport.kr/js/iamport.payment-1.1.5.js"></script>

아임포트는 jQuery 기반이기 때문에 jQuery라이브러리를 추가해줘야 하고, 아임포트 api를 사용하기 위해서는 아임포트 js라이브러리를 추가해야한다.

 

제일 중요한 결제 코드

<script>    
    var IMP = window.IMP; // 생략 가능
    IMP.init("본인가맹점고유번호"); // 예시 : imp00000000
    function requestPay(isLogin) {
        
        // 로그인 체크
        if (!isLogin) {
            alert("로그인 후 이용할 수 있습니다.");
            return;
        }


        // 값 세팅
        getCurrentUserInfo();
        let temp = getMerchantUid_setPrice();
        let merchant_uid = temp.merchant_uid;
        let pay_auth_id = temp.pay_auth_id;
        let amount = temp.price;
        

        // 결제창 호출 코드
        IMP.request_pay({ // 파라미터
            pg: "kakao", // pg사
            pay_method: "kakaopay", // 결제 수단
            merchant_uid: merchant_uid, //주문번호
            name: name,  //결제창에서 보여질 이름
            amount: amount,  //가격 
            buyer_name: buyer_name,// 구매자 이름
            buyer_tel: buyer_tel, // 구매자 전화번호
        }, function (rsp) { 
            if (rsp.success) { // 결제 성공

                $.ajax({
                    headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
                    url: "/pay/complete",
                    method: "POST",
                    dataType : "JSON",
                    data: {
                        imp_uid: rsp.imp_uid,
                        merchant_uid: rsp.merchant_uid,
                        pay_auth_id : pay_auth_id,
                        goods_id : goods_id,
                    },
                    success: function(data) {
                        if(data.result.code!=200){
                            //결제실패(웹서버측 실패)   
                            // 환불 코드(아직 구현 안함)
                            alert("위조된 결제 시도에 의해 결제에 실패했습니다.");  
                            removePayAuth(pay_auth_id);// pay_auth 값 지우기
                        }else{
                            //결제성공(웹서버측 성공)
                            alert("결제에 성공했습니다.");
                        }
                    },
                    error: function(data) {
                        console.log("error" +data);
                    }
                });
            } else {
                removePayAuth(pay_auth_id); // pay_auth 값 지우기
                alert("결제에 실패했습니다. : " +  rsp.error_msg);
            }
        });
    }
    
    // 현재 사용자의 정보를 가져오는 함수
    function getCurrentUserInfo() {
        $.ajax({
            headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
            url: "/users/getCurrentUser",
            type: "get",
            async:false, // 동기방식(전역변수에 값 저장하려면 필요)
            dataType : "json",
            success : function(data) {
                buyer_name = data.name;
                buyer_tel = data.tel;
            },
            error: function(request,status,error){ 
                alert("code = "+ request.status + " message = " + request.responseText + " error = " + error); 
                console.log("code = "+ request.status + " message = " + request.responseText + " error = " + error);
            }
        });
    }
    
	// 주문번호를 가져오는 함수 
    function getMerchantUid_setPrice() {
        var result = "";
        $.ajax({
            headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
            url: "/pay/getMerchantUidAndSetPrice",
            type: "GET",
            async:false, // 동기방식(전역변수에 값 저장하려면 필요)
            dataType: "json",
            data : {
                goods_id : goods_id
            },
            success : function(data) {
                result = data;
            },
            error: function(request,status,error){ 
                alert("code = "+ request.status + " message = " + request.responseText + " error = " + error); 
                console.log("code = "+ request.status + " message = " + request.responseText + " error = " + error);
                result = "error";
            }
        });
        return result;
    }

    function removePayAuth(removePayAuthId) {
        $.ajax({
            headers: {'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')},
            url: "/pay/removePayAuth",
            method: "POST",
            dataType : "text",
            data: {
                removePayAuthId : removePayAuthId
            },
            success: function() {
                
            },
            error: function(request, status, error) {
                console.log("status : " + request.status + ", message : " + request.responseText + ", error : " + error);
            }
        });
    }
</script>

 

 

web.php

url 매핑에 대한 코드

Route::get('users/getCurrentUser', [UsersController::class, 'getCurrentUser']);
Route::get('pay/getMerchantUidAndSetPrice', [PayController::class, 'getMerchantUidAndSetPrice']);
Route::post('pay/complete', [PayController::class, 'complete']);
Route::post('pay/removePayAuth', [PayController::class, 'removePayAuth']);

 

 

 

UsersController.php

사용자의 정보를 가져오는 함수

public function getCurrentUser() {
    $user = User::find(auth()->id());
    $userData['name'] = $user->name;
    $userData['tel'] = $user->tel;

    return response()->json($userData);
}

지금 로그인한 사용자의 이름과 전화번호를 db에서 가져와서 나중에 결제 정보를 입력하는데 사용한다.

 

 

PayController.php - getMerchantUidAndSetPrice()

주문번호를 만들고, 주문번호와 가격을 db에 저장하는 함수

보안을 위해 주문번호와 가격을 payauth라는 테이블에 임시로 저장을 하고 나중에 가격과 주문번호가 일치한지 비교한다. 

가격은 상품 금액의 3.5%를 더한 가격으로 정했다.

public function getMerchantUidAndSetPrice() {
    // 주문번호 규칙 : 연월일(YYMMDD) + 숫자or영어 랜덤 5자리 = 11자리
    $today = date("ymd");
    $merchant_uid = $today.Str::random(5);


    $good = DB::table('goods')->select('price')->where('id', request('goods_id'))->first();
    $price = floor($good->price + ($good->price * (35 / 1000))); // 상품 금액의 3.5% -> 수익!
    $ans = PayAuth::create(['merchant_uid'=>$merchant_uid, 'amount'=>$price]); // db에 임시 저장


    $result['merchant_uid'] = $merchant_uid;
    $result['pay_auth_id'] = $ans->id;
    $result['price'] = $price;

    return response()->json($result);
}

 

 

PayController.php - complete()

결제 검증 함수(결제에 성공하면 호출하는 함수)

코드 참고 : https://kkukkukku.dev/m/189

 

아임포트에서 보안을 위해 추천하는 결제 엑세스 토큰을 발급해서 결제 정보 조회하는 검증 방법을 사용했다.

그리고 그 외에도 payauth 테이블에 임시 저장해놨던 가격과 주문번호가 일치한지 한 번 더 검사했다.

그래서 

1. 결제된 금액과 상품 금액이 일치한지 -> 2번

2. 정상적으로 결제됐는지 -> 1번

3. 주문번호가 동일한지 -> 2번 

총 5번 검사했다.

public function complete(Request $request)
{
    $result = ["code"=>200, "message"=>"success"]; // 결제에 성공했다는 의미의 코드와 메세지를 넣음
    $imp_key = "REST API 키"; // 아임포트 관리자 페이지 시스템설정->내정보->REST API 키 값
    $imp_secret = " REST API Secret"; // 아임포트 관리자 페이지의 시스템설정->내정보->REST API Secret 값
    $imp_uid = request('imp_uid');// 결제 번호
    $merchant_uid = request("merchant_uid");// 주문 번호
    $pay_auth_id = request("pay_auth_id");// 주문 번호 id
    $goods_id = request("goods_id"); // 상품 id
    $sale_user_id = DB::table('goods')->select('user_id')->where('id', $goods_id)->first(); // 판매자 id를 db에서 가져옴
    $sale_user_id = $sale_user_id->user_id; // 상품 판매자


    try{
        // 엑세스 토큰 발급
        $getToken  = Http::withHeaders([
            'Content-Type' => 'application/json'
        ])->post('https://api.iamport.kr/users/getToken', [
            'imp_key' => $imp_key,
            'imp_secret' => $imp_secret,
        ]);
        $getTokenJson = json_decode($getToken, true);
        $access_token = $getTokenJson['response']['access_token'];


        // imp_uid로 아임포트 서버에서 결제 정보 조회
        $getPaymentData = Http::withHeaders([
            'Authorization' => $access_token
        ])->get('https://api.iamport.kr/payments/?imp_uid[]='.$imp_uid);
        $getPaymentDataJson = json_decode($getPaymentData,true);


        // $getPaymentDataJson['response'] : 아임포트에 요청한 실제 결제 정보
        $iamport_status = $getPaymentDataJson['response'][0]['status']; //아임포트 결제 상태 값 (paid : 정상 결제 된 값)
        $iamport_amount = $getPaymentDataJson['response'][0]['amount']; //아임포트 실제 결제 금액           
        $iamport_merchant_uid = $getPaymentDataJson['response'][0]['merchant_uid']; //아임포트 실제 주문번호


        // 결제 검증(XSS 공격 방지)
        $amount = DB::table('goods')->select('price')->where('id', $goods_id)->first();
        $amount = floor($amount->price + ($amount->price * 35/1000)); // 수수료 3.5%
        $real_merchant_uid_val = DB::table('pay_auths')->where('id', $pay_auth_id)->first();
        $real_merchant_uid = $real_merchant_uid_val->merchant_uid;
        $real_amout = $real_merchant_uid_val->amount;


         // 결제 된 금액과 상품 금액이 동일한지 and 정상결제확인 and 주문번호 동일한지
        if($iamport_amount == $amount && $real_amout == $amount && $iamport_status == 'paid' 
            && $real_merchant_uid == $iamport_merchant_uid && $real_merchant_uid == $merchant_uid) {
                if (auth()->check()) {
                    $currentUser = auth()->id();
                }
                else {
                    $currentUser = null;
                    throw new Exception('로그인이 되어있지 않습니다.', 410);
                }

            // db에 값 넣기
            Payment::create([
                'merchant_uid'=>$merchant_uid,
                'imp_uid'=>$imp_uid,
                'amount'=>$iamport_amount,
                'status'=>$iamport_status,
                'buy_user_id'=>$currentUser,
                'sale_user_id'=>$sale_user_id,
                'goods_id'=>$goods_id
            ]);

            // pay_auth값 삭제
            DB::table('pay_auths')->where('id', $pay_auth_id)->delete();

            // goods 판매완료로 전환
            DB::table('goods')->where('id', $goods_id)->update(['sale_state' => 2]);
        }
        else {
            throw new Exception('위조된 결제를 시도했습니다.', 410);
            DB::table('pay_auths')->where('id', $pay_auth_id)->delete(); // pay_auth값 삭제
        }

    }catch(Exception $e){ // 예외처리
        $result = [
            'code' => 410,
            'message' => $e->getMessage()
        ];
    }

    return response()->json([
        'result'=>$result
    ]);
}

 

 

PayController.php - removePayAuth()

임시로 저장해놨던 가격과 주문번호 삭제하는 함수

function removePayAuth() {
    $removePayAuthId = request('removePayAuthId');
    DB::table('pay_auths')->where('id', $removePayAuthId)->delete(); // pay_auth값 삭제
}

 

 

결과

웹 사이트 결제 버튼 클릭 시
결제 알림톡
결제 완료 알림톡

결제를 진행하면 실제처첨 카카오톡으로 결제 알림톡이 온다.

테스트 모드로 구현했기 때문에 실제 돈이 빠져나가진 않고,

아임포트 측 카카오페이 가상 계정에서 빠져나간다.

 

 

 

참고 : https://kkukkukku.dev/m/189

https://velog.io/@poseassome/%EA%B0%9C%EC%9D%B8%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%95%84%EC%9E%84%ED%8F%AC%ED%8A%B8import-%EA%B2%B0%EC%A0%9C-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0

아임 포트 공식문서 : https://docs.iamport.kr/implementation/payment

반응형
Comments