들어가는 말

내가 쓴 글 중 구글 인앱 결제 검증 웹 서비스 만들기가 가장 인기 있다. 꾸준히 찾는 사람이 있다.

이세계에 진입한 서버 개발 연재가 시작된 후로도 최고 조회수 3위 안에 든다.

그런데 해당 글에는 자원을 다루는 내용과 연계되지 못해 반쪽짜리였다.

인앱 결제 영수증 검증을 했어도 자원을 클라이언트에서 관리하게되면 메모리 조작 등으로 방지해도 방지한게 아닌 상태가 된다.

이 시간에는 인앱 결제 영수증 검증과 자원 지급을 묶어서 처리하도록 한다.

인앱 결제 영수증 검증

인앱 결제 후 영수증 검증 과정은 다음과 같다.

인앱영수증검증

서버 사이드에서 처리하는 일은 인앱 영수증 검증을 요청하고, 자원을 지급하는 것이다.

모델 추가

models 폴더에 파일을 추가하고 각각 아래 내용을 적용한다.

LogReceipt는 검증 요청한 영수증을 저장한다. DefineShop은 상점을 관리할 때 사용한다. 여기서는 인앱 결제로 사용되는 상품을 나타내도록 한다.

라우터 추가

routes폴더에 receipt.js 파일을 추가하고 아래 내용을 적용한다.

app.js에 등록

app.js 파일을 수정하여 receipt 라우터를 추가해보자.

  1. app.js에서 아래 내용을 찾아서 그 아래쪽에 코드를 추가한다.
    • 찾아야하는 내용
        const routes = require('./routes/index');
      
    • 추가할 코드
        const receipt = require('./routes/receipt');
      
  2. app.js에서 아래 내용을 찾아서 그 아래쪽에 코드를 추가한다.
    • 찾아야하는 내용
        app.use('/', routes);
      
    • 추가할 코드
        app.use('/receipt', receipt);
      

로직 추가

logics 폴더에 validationReceipt.js 파일을 추가하고 아래 내용을 적용한다.

애플, 구글, 원스토어의 영수증을 검정할 때 사용하는 로직을 모아두었다.

각 로직은 마켓 서버에 HTTP로 검증 요청을 보내게되므로 request 모듈을 새로 추가해야한다.

프로젝트 폴더에서 아래 명령을 입력한다.

$ yarn add request

에러 추가

에러코드를 추가한다. utils/error.js에 아래 클래스를 추가한다.


/** 80701 */
class UnsupportedReceiptType extends CustomError {
    constructor() {
        let message = '지원하지 않거나 존재하지 않는 영수증타입(ReceipType)';
        let code = 80701;
        super(message, code);
    }
}

/** 80702 */
class InitializationFirst extends CustomError {
    constructor() {
        let message = '초기화를 먼저 진행해야한다.';
        let code = 80702;
        super(message, code);
    }
}

utils/error.jserrorMap 오브젝트에 추가할 클래스를 등록한다.

추가할 때 이전의 마지막 부분 – 여기서는 NoLongerUpgrade – 에 콤마(,)를 추가하여야 한다.

"UnsupportedReceiptType":UnsupportedReceiptType,
"InitializationFirst":InitializationFirst

utils/commonFunc.js에 메서드 추가

오브젝트에 원하는 프로퍼티가 하나라도 있는지 체크하는 메서드를 추가한다.

/**
 * object에 key가 적어도 하나라도 포함되어있는지 체크.
 */
exports.ObjectIncludeOneKey = (obj, keyArr)=>{
    for(let argValue of keyArr) {
        if(obj.hasOwnProperty(argValue) === true)
            return true;
    }
    return false;
}

데이터 입력

DefineShop 테이블에 데이터를 입력한다.

ShopID ProductName PriceType Price RewardSetGroupID RewardGoodsGroupID
NULL test 20 0.99 6001 NULL

영수증 검증 테스트

  • 패스 : POST
  • URL : localhost:3000/receipt/validation/apple
  • Headers : Authorization을 추가하고 토큰 내용을 value 부분에 넣는다.
  • body : 아래 내용을 넣는다.

samplereceipt.json

정상적으로 요청되면 아래와 같은 형식의 결과를 확인할 수 있다.

{
  "result": 0,
  "reward": {
    "item": [],
    "currency": [
      {
        "TotalQNTY": 900000,
        "OwnCurrencyUID": 7,
        "CurrencyID": 103,
        "CurrentQNTY": 1217,
        "NowMaxQNTY": 900000,
        "AddMaxQNTY": 0,
        "UpdateTimeStamp": "2017-02-21T00:00:00.000Z",
        "GameUserID": 1
      }
    ]
  }
}

구글 권한 처리

검증과 관련된 부분은 완료되었다. 그러나 문제가 하나 있다.

애플과 원스토어는 별도의 권한없이 영수증 확인이 가능하다. 구글은 권한이 필요하다.

구글 인앱 결제 검증 웹 서비스 만들기에서도 많은 부분이 이 내용이었다.

길고 지루하겠지만 다음 과정을 진행해보자.

필요사항 준비

구글 개발자 콘솔에서 OAuth로 사용자를 인증하는 아이디를 등록해서 CLIENT_ID, CLIENT_SECRET를 얻는 과정을 알아보자.

  1. 구글 플레이 개발자 콘솔에 접속한 후 API 액세스 페이지로 이동한다.
  2. 처음 사용 시 신규 프로젝트를 생성하거나 기존에 등록된 것이 있다면 원하는 것을 선택하고 링크를 클릭한다.
  3. 2번 과정을 통해 구글 개발자 콘솔에 접속한 후 API 관리자에 도달했다.
  4. 사용자 인증 정보 - 새 사용자 인증 정보 버튼을 클릭하고 OAuth 클라이언트 ID를 선택한다.
    OAuth 클라이언트 ID 생성
  5. 웹 어플리케이션을 선택하고 이름을 입력한 후 생성버튼을 클릭한다.
    클라이언트 ID 만들기
  6. 생성이 완료되면 클라이언트 ID클라이언트 보안 비밀이 출력된다.
    클라이언트 ID 확인

이로써 CLIENT_ID, CLIENT_SECRET는 확보되었다. REDIRECT_URL은 이후 과정에서 추가하기로 하고 넘어가자.

모델 추가

models 폴더에 파일을 추가하고 각각 아래 내용을 적용한다.

관리자 계정을 등록할 수 있도록 AdminUser를 추가했다. 구글 권한 획득 내용을 기록할 수 있도록 AuthGoogle을 추가했다.

라우터 추가

routes폴더에 auth.js 파일을 추가하고 아래 내용을 적용한다.

app.js에 등록

app.js 파일을 수정하여 auth 라우터를 추가해보자.

  1. app.js에서 아래 내용을 찾아서 그 아래쪽에 코드를 추가한다.
    • 찾아야하는 내용
        const routes = require('./routes/index');
      
    • 추가할 코드
        const auth = require('./routes/auth');
      
  2. app.js에서 아래 내용을 찾아서 그 아래쪽에 코드를 추가한다.
    • 찾아야하는 내용
        app.use('/', routes);
      
    • 추가할 코드
        app.use('/auth', auth);
      

utils/auth.js 내용 추가

utils/auth.js에 다음 내용을 추가한다.

const crypto = require('crypto');
const cryptoPassword = 
    process.env.cryptoPassword || 'wendy';

exports.encryptPassword = (password)=>{
    let hash = crypto.createHash('sha256')
        .update(cryptoPassword).digest('base64');
    return hash;
}

exports.isAdminAuthenticated = (req, res, next)=>{
    jwt.verify(req.headers.authorization, SECRET, (err, decoded)=>{
        if(err || decoded.grade < 10) {
            let error = wendyError('CredentialFailure');
            res.status(401).send({result:error.code, message:error.message});
        }

에러 추가

utils/error.js에 아래 클래스를 추가한다.

/** 99201 */
class WrongEmailOrPassword extends CustomError {
    constructor() {
        let message = 'email 이나 password가 없거나 틀렸다.';
        let code = 99201;
        super(message, code);
    }
}

/** 99202 */
class UsedEmail extends CustomError {
    constructor() {
        let message = '이미 사용중인 email';
        let code = 99202;
        super(message, code);
    }
}

/** 99203 */
class WrongEmail extends CustomError {
    constructor() {
        let message = 'email 형식이 아니다';
        let code = 99203;
        super(message, code);
    }
}

/** 99204 */
class WrongPassword extends CustomError {
    constructor() {
        let message = 'password는 최소 1개의 숫자 혹은 특수문자를 포함한 8~16자리여야 한다';
        let code = 99204;
        super(message, code);
    }
}

/** 99205 */
class LockdownUserAccess extends CustomError {
    constructor() {
        let message = 'password 입력 실패 5회로 권한 박탈, 관리자에게 문의!';
        let code = 99205;
        super(message, code);
    }
}

utils/error.jserrorMap 오브젝트에 추가할 클래스를 등록한다.

추가할 때 이전의 마지막 부분 – 여기서는 NoLongerUpgrade – 에 콤마(,)를 추가하여야 한다.

"WrongEmailOrPassword":WrongEmailOrPassword,
"UsedEmail":UsedEmail,
"WrongEmail":WrongEmail,
"WrongPassword":WrongPassword,
"LockdownUserAccess":LockdownUserAccess,

REDIRECT_URI 추가

필요사항을 준비할 때 빼놓은 REDIRECT_URI을 등록해보자.

Azure 웹앱의 hostname을 기억한다면 /auth/google/return 패스를 더해 아래와 같은 형태가 된다.

hostname/auth/google/return

로컬 개발 환경에서는 ngrok을 활용할 수 있다.

ngrok은 외부에서 내 컴퓨터에 접근할 수 있도록 터널을 뚫어준다.

ngrok 개념

ngrok으로 로컬 네트워크의 터널 열기에 자세히 설명되어있음.

ngrok을 설치한 후 포트를 열도록 한다.

    $ ./ngrok http 3000

osx나 linux 환경에서는 path에 등록되지 않았을 때 ngrok이 위치한 폴더에서 위와 같이 입력한다. path에 등록되어있다면 ./는 제거한다.

포트가 열리면 아래처럼 주소가 나온다. 해당 주소를 hostname으로 사용하면 된다.

Azure 웹앱에서 사용할 때는 hostname이 웹앱의 URL로 대치되어야한다.

ngrok 포트 개방 예시

redirect_uri에 입력한 주소를 구글 개발자 콘솔 API 관리자에 접속하여 등록해야한다.

redirect uri 업데이트

테스트

테스트 관리자 계정 추가

  • 패스 : POST
  • URL : localhost:3000/auth/add
  • body : 아래 내용을 넣는다.
{
	"email":"[email protected]",
	"password":"test1234"
}

테스트용으로 추가한 것이다. 운영할 때 복잡한 패스워드를 사용하고 절대 공개하면 안된다.

별도의 관리툴이 없으므로 DBeaver로 AdminUser테이블로 접근한 뒤 grade를 10으로 조정한다.

vs code 개발 환경 설정

.vscode/launch.json에서 env노드를 찾아서 아래 3가지 환경변수를 추가한다.

여기서 hostname은 ngrok을 통해 얻은 hostname을 입력해야한다.

                "authAOSClientID":"{클라이언트 id}",
                "authAOSClientSecret":"{클라이언트 secret}",
                "authAOSRedirectURI":"{hostname}/auth/google/return"

콤마(,)추가를 잊지 말자!

눈치가 빠른 사람이라면 알겠지만 Azure 웹앱에도 환경 변수 등록을 해야한다. 단 hostname은 웹앱의 URL를 넣어야한다.

브라우저로 권한 요청

vs code에서 실행한 뒤 브라우저로 아래 주소로 접속한다.

localhost:3000/auth/google/start

오프라인액세스권한

허용 버튼을 클릭하면 아래처럼 토큰을 얻을 수 있다.

{
    "access_token": "ya29.Glhaha",
    "expires_in": 3600,
    "refresh_token": "1/o8-U9haha",
    "token_type": "Bearer"
}

웹 작업(WebJob) 등록

Google 권한 요청으로 얻은 토큰은 1시간 동안만 유효하다. 실제로 서비스가 이뤄지고 있다면 지속적인 갱신이 필요하다.

Azure 웹앱은 이러한 작업을 웹 작업(WebJob)으로 해결하도록 도와준다.

소스코드 편집

  • 아래 링크로 웹 작업용 소스코드를 다운받는다.

Wendy-webjob

  • 소스코드에서 server.js 파일의 3가지 변수를 수정한다.
let email = '';
let password = '';
let hostname = '';

앞선 예시를 반영하면 아래와 같이 변경한다.

let email = [email protected]';
let password = 'test1234';
let hostname = 'Azure 웹앱 URL';
  • 모듈을 설치한다.
$ yarn install
  • 전체 폴더를 압축한다.

웹 작업 추가

  1. Azure 포털(https://portal.azure.com)로 접속한다.
  2. 이전에 생성한 웹앱을 선택한다.
  3. 왼쪽 설정 메뉴 하단에 웹 작업을 클릭한다.

    웹작업

  4. 상단의 추가 버튼을 클릭한다.

    웹작업추가

  5. 이름을 입력하고 앞서 생성한 압축 파일을 선택한다. 형식트리거됨으로 선택하고 20분마다 실행될 수 있도록 CRON 식을 아래 처럼 입력한다. 확인 버튼을 클릭하여 등록을 마친다.

    웹작업

     0 0/20 * * * *
    

이렇게 등록된 웹 작업은 20분마다 Google 권한을 갱신 요청을 한다.

맺음말

인앱 영수증을 검증하는 방법에 대해 알아봤다. Google은 다소 복잡한 권한 획득 과정이 필요하다. 실제 운영을 고려한다면 결제 전에 웹 서버로부터 develperPayload를 발급받아 결제를 진행해서 영수증에 함께 나오도록하여 검증 단계를 강화하는 것도 고려해야한다.

인앱 결제를 크랙하려는 시도는 다양하므로 자물쇠를 더 단단히 걸어둘 필요가 있다.

1차 – 혹은 1기 – 완료 목표였던 7강이 완료되었다.

2016년 12월 말부터 2017년 3월 초까지 약 2달 조금 넘는 시간 동안 새벽을 허락해준 아내, 좋아요공유, 댓글로 응원해준 모든 분에게 감사의 인사를 전한다.

2차 – 혹은 2기 – 는 도적같이 찾아오도록 하겠다.


참고자료

완성된 소스코드는 아래 링크에서 다운로드받으면 된다.

Wendy 7강 완료 버전