AdiSON 오퍼월 연동가이드
2.5.0
2.5.0
  • AdiSON 오퍼월 연동가이드
  • 오퍼월 기본 동작
  • 캠페인 API 연동
    • 캠페인 완료
    • HMAC 생성 및 검증
  • 2.5.0 변경사항
  • 안드로이드 SDK 설정
    • 설정 적용
    • 테마 및 색상 변경
    • 적립 가능 금액 표시
    • 성별 연령 타겟팅 적용
    • 딥링크 연동
    • 안드로이드 12 변경사항 대응
  • iOS SDK 설정
    • iOS SDK 릴리즈 정보
    • iOS 14 이상에 대비하기
    • 설정 적용
    • 테마 및 색상 변경
    • 적립 가능 금액 표시
    • 성별 연령 타겟팅 적용
    • 딥링크 연동
  • Unity SDK 설정
    • 라이프 사이클
    • 설정 적용
  • AdiSON 오퍼월 BI
Powered by GitBook
On this page
  • signature 생성 예시
  • 샘플 코드

Was this helpful?

  1. 캠페인 API 연동

HMAC 생성 및 검증

API 호출 간 보안을 위한 검증 방식으로 HMAC을 사용합니다.

언어별로 아래 코드를 참조하여 HMAC 검증 부분을 구현합니다. DEV 환경에서는 "test_secret_key"를 사용하여 테스트 해주시면 됩니다. LIVE 환경의 SECRET_KEY는 연동 시작 시 전달해드립니다.

const moment = require('moment');
const qs = require('qs');
const crypto = require('crypto');

function HmacUtil () {

    this.algorithm = "sha256"
    this.secretKey = "test_secret_key"
    this.expiresIn = 2 * 60 * 1000 // 2minutes
 
    this.hmacDatetime = function() {
      return moment().format("YYYY-MM-DDTHH:mm:ssZ")
    }
    
    this.alphabeticalSort = function(a, b) {
      return a.localeCompare(b);
    }
    
    this.sortedQueryString = function(encodedQueryString) {
      let obj = qs.parse(encodedQueryString);
      return qs.stringify(obj, { sort: this.alphabeticalSort });
    }
    
    this.payloadHash = function(payload) {
      return crypto.createHash(this.algorithm).update(payload,'utf8').digest('hex');
    }
    
    this.stringToSign = function(method, uri, hmacDatetime, queryString, payload) {
      return method + "\n" + uri + "\n" + hmacDatetime + "\n" + this.sortedQueryString(queryString) + "\n" + this.payloadHash(payload)
    }
    
    this.sign = function(stringToSign) {
      rawHmac = crypto.createHmac(this.algorithm, this.secretKey).update(stringToSign).digest('hex');
      return Buffer.from(rawHmac).toString('base64');
    }
    
    this.signature = function(method, uri, hmacDatetime, queryString, payload) {
      return this.sign(this.stringToSign(method, uri, hmacDatetime, queryString, payload));
    }
    
    this.isValid = function(method, uri, hmacDatetime, queryString, payload, signature) {
      let sameSignature = this.signature(method, uri, hmacDatetime, queryString, payload) === signature
      let notExpired = (new Date() - new Date(hmacDatetime)) < this.expiresIn // 2.minutes
      return sameSignature && notExpired
    }
    
}
 
class HmacUtil {

    private $secret_key = 'test_secret_key';
    private $algorithm = 'sha256';

    public function __construct($params = array()) {}

    public function signature($method, $uri, $hmac_datetime, $encoded_query_string, $payload) {
        $sign_str = $this->string_to_sign($method, $uri, $hmac_datetime, $encoded_query_string, $payload);
        $signature = $this->sign($sign_str);
        return $signature;
    }

    public function hmac_datetime() {
        $datetime = date('c');
        return $datetime;
    }
    
    public function valid($method, $uri, $hmac_datetime, $encoded_query_string, $payload, $signature) {
        $new_signature = $this->signature($method, $uri, $hmac_datetime, $encoded_query_string, $payload);
        if($new_signature != $signature) return FALSE;

        $two_minute_ago = date("c", strtotime("-2 minutes", strtotime(date("Y-m-d H:i:s"))));
        if($two_minute_ago > $hmac_datetime) return FALSE;

        return TRUE;
    }

    private function sorted_query_string($encoded_query_string) {
        parse_str($encoded_query_string, $parse_uri_query_string);
        sort($parse_uri_query_string);
        $encoded_uri = str_replace('+', '%20', http_build_query($parse_uri_query_string));
        return $encoded_uri;
    }

    private function sign($string_to_sign) {
        $raw_hmac = hash_hmac($this->algorithm, $string_to_sign, $this->secret_key);
        $signed = base64_encode($raw_hmac);
        return $signed;
    }

    private function payload_hash($payload) {
        $payload = hash('SHA256', $payload);
        return $payload;
    }

    private function string_to_sign($method, $uri, $hmac_datetime, $encoded_query_string, $payload) {
        $sign = $method."\n".$uri."\n".$hmac_datetime."\n".$this->sorted_query_string($encoded_query_string)."\n".$this->payload_hash($payload);
        return $sign;
    }
    
}
import java.util.Date;
import java.util.Formatter;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.security.MessageDigest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;

public final class HmacUtil {

  private static final String ALGORITHM = "HmacSHA256";
  private static final int EXPIRES_IN_SEC = 120;

  // 검증 처리용 함수
  // secretKey : dev 는 'test_secret_key' 고정이며 production 은 별도 발급됩니다.
  public static boolean isValid(String method, String uri, String hmacDatetime, String queryString, String payload, String reqSignature, String secretKey) 
    throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
  {
    // hmacDateTime 기준 2분 경과 시 만료 처리
    Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").parse(hmacDatetime);
    long diff = new Date().getTime() - date.getTime();
    int diffSec = (int) (diff / (1000));
    if (diffSec > EXPIRES_IN_SEC) {
      return false;
    }

    String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
    String genSignature = generate(stringToSign, secretKey);
    return reqSignature.equals(genSignature);
  }
  
  public static String generateSignature(String method, String uri, String hmacDatetime, String queryString, String payload, String secretKey)
    throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
  {
    String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
    return generate(stringToSign, secretKey);
  }

  private static String generate(String plainText, String key)
	throws SignatureException, NoSuchAlgorithmException, InvalidKeyException
  {
    SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), ALGORITHM);
    Mac mac = Mac.getInstance(ALGORITHM);
    mac.init(signingKey);
    return base64encode(toHexString(mac.doFinal(plainText.getBytes())));
  }

  private static String sha256hash(String str) {
    String hashString = "";
    try {
      MessageDigest sh = MessageDigest.getInstance("SHA-256");
      sh.update(str.getBytes());
      byte byteData[] = sh.digest();
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < byteData.length; i++) {
        sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1));
      }
      hashString = sb.toString();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      hashString = null;
    }
    return hashString;
  }

  private static String toHexString(byte[] bytes) {
		Formatter formatter = new Formatter();
		for (byte b : bytes) {
			formatter.format("%02x", b);
		}
		return formatter.toString();
	}

  private static String base64encode(String str) {
    byte[] targetBytes = str.getBytes(); 
    String encoded = DatatypeConverter.printBase64Binary(targetBytes);
    return encoded;
  }
  
  private static String sortedQueryString(String queryString) {
    String[] params = queryString.split("&");
    Map<String, String> map = new HashMap<String, String>();
    for (String param : params) {
      String name = param.split("=")[0];
      String value = param.split("=")[1];
      map.put(name, value);
    }

    List<Map.Entry<String, String>> entries = new LinkedList<>(map.entrySet());
    Collections.sort(entries, (o1, o2) -> o1.getKey().compareTo(o2.getKey()));

    String sortedQueryString = "";
    for (Map.Entry<String, String> entry : entries) {
      if (sortedQueryString != "") sortedQueryString += "&";
      sortedQueryString += entry.getKey() + "=" + entry.getValue();
    }

    return sortedQueryString;
  }
}
import java.util.Date;
import java.util.Formatter;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.text.SimpleDateFormat;
import java.text.ParseException;
import java.security.MessageDigest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public final class HmacUtil {

  private static final String ALGORITHM = "HmacSHA256";
  private static final int EXPIRES_IN_SEC = 120;

  // 검증 처리용 함수
  // secretKey : dev 는 'test_secret_key' 고정이며 production 은 별도 발급됩니다.
  public static boolean isValid(String method, String uri, String hmacDatetime, String queryString, String payload, String reqSignature, String secretKey) 
    throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
  {
    // hmacDateTime 기준 2분 경과 시 만료 처리
    Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").parse(hmacDatetime);
    long diff = new Date().getTime() - date.getTime();
    int diffSec = (int) (diff / (1000));
    if (diffSec > EXPIRES_IN_SEC) {
      return false;
    }

    String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
    String genSignature = generate(stringToSign, secretKey);
    return reqSignature.equals(genSignature);
  }
  
  public static String generateSignature(String method, String uri, String hmacDatetime, String queryString, String payload, String secretKey)
    throws ParseException, SignatureException, NoSuchAlgorithmException, InvalidKeyException
  {
    String stringToSign = method + "\n" + uri + "\n" + hmacDatetime + "\n" + sortedQueryString(queryString) + "\n" + sha256hash(payload);
    return generate(stringToSign, secretKey);
  }

  private static String generate(String plainText, String key)
	throws SignatureException, NoSuchAlgorithmException, InvalidKeyException
  {
    SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), ALGORITHM);
    Mac mac = Mac.getInstance(ALGORITHM);
    mac.init(signingKey);
    return base64encode(toHexString(mac.doFinal(plainText.getBytes())));
  }

  private static String sha256hash(String str) {
    String hashString = "";
    try {
      MessageDigest sh = MessageDigest.getInstance("SHA-256");
      sh.update(str.getBytes());
      byte byteData[] = sh.digest();
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < byteData.length; i++) {
        sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1));
      }
      hashString = sb.toString();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      hashString = null;
    }
    return hashString;
  }

  private static String toHexString(byte[] bytes) {
		Formatter formatter = new Formatter();
		for (byte b : bytes) {
			formatter.format("%02x", b);
		}
		return formatter.toString();
	}

  private static String base64encode(String str) {
    byte[] targetBytes = str.getBytes(); 
    String encoded = Base64.getEncoder().encodeToString(targetBytes);
    return encoded;
  }
  
  private static String sortedQueryString(String queryString) {
    String[] params = queryString.split("&");
    Map<String, String> map = new HashMap<String, String>();
    for (String param : params) {
      String name = param.split("=")[0];
      String value = param.split("=")[1];
      map.put(name, value);
    }

    List<Map.Entry<String, String>> entries = new LinkedList<>(map.entrySet());
    Collections.sort(entries, (o1, o2) -> o1.getKey().compareTo(o2.getKey()));

    String sortedQueryString = "";
    for (Map.Entry<String, String> entry : entries) {
      if (sortedQueryString != "") sortedQueryString += "&";
      sortedQueryString += entry.getKey() + "=" + entry.getValue();
    }

    return sortedQueryString;
  }
}
import hmac
import hashlib
import json
import base64
import urllib.parse
import datetime
from pytz import timezone

class hmac_util:
  def __init__(self, secret):
      self.secret = secret

  def hmac_datetime(self):
      now = datetime.datetime.now(timezone('Asia/Seoul'))
      return now.strftime("%Y-%m-%dT%H:%M:%S%z")

  def signature(self, method, uri, hmac_datetime, query_string, payload):
      string_to_sign = self.__string_to_sign(method, uri, hmac_datetime, query_string, payload)

      print("|- string_to_sign:\n{}".format(string_to_sign))

      return self.__sign(string_to_sign)

  def __sorted_query_string(self, query_string):
      query_dict = urllib.parse.parse_qsl(query_string)
      query_dict = sorted(query_dict)

      return urllib.parse.urlencode(query_dict)

  def __payload_hash(self, payload):
      payload_string = json.dumps(payload, separators=(',', ':'),ensure_ascii=False)

      m = hashlib.sha256()
      m.update(payload_string.encode('utf-8'))
      hash_string = m.hexdigest()

      print("|- payload_hash: {}".format(hash_string))

      return hash_string

  def __string_to_sign(self, method, uri, hmac_datetime, query_string, payload):
      result = ""
      result += method
      result += "\n"
      result += uri
      result += "\n"
      result += hmac_datetime
      result += "\n"
      result += self.__sorted_query_string(query_string)
      result += "\n"
      result += self.__payload_hash(payload)

      return result

  def __sign(self, string_to_sign):
      key = self.secret.encode('utf-8')
      msg = string_to_sign.encode('utf-8')

      hashed = hmac.new(key=key, msg=msg, digestmod=hashlib.sha256)

      signature_computed = base64.b64encode(hashed.hexdigest().encode('utf-8')).decode('utf-8')
  
      return signature_computed
      
require 'cgi'
require 'digest'
require 'openssl'
require 'base64'

class HmacUtil
  attr_accessor :secret_key, :algorithm

  module SupportAlgorithm
    SHA1 = 'sha1'
    SHA256 = 'SHA256'
  end

  def initialize(secret_key, algorithm)
    @secret_key = secret_key
    @algorithm = algorithm
  end

  def hmac_datetime
    Time.now.strftime('%FT%T%:z')
  end

  def signature(method, uri, hmac_datetime, encoded_query_string, payload)
    sign(string_to_sign(method, uri, hmac_datetime, encoded_query_string, payload))
  end
  
  private

  def sorted_query_string(encoded_query_string)
    parse_uri_query_string = CGI.parse(encoded_query_string)
    URI.encode_www_form(parse_uri_query_string.sort.to_h).gsub('+', '%20')
  end

  def payload_hash(payload)
    Digest::SHA256.hexdigest payload
  end

  def string_to_sign(method, uri, hmac_datetime, query_string, payload)
    method + "\n" + uri + "\n" + hmac_datetime + "\n" + sorted_query_string(query_string) + "\n" + payload_hash(payload)
  end
  
  def sign(string_to_sign)
    raw_hmac = OpenSSL::HMAC.hexdigest(@algorithm, @secret_key, string_to_sign)
    Base64.strict_encode64(raw_hmac)
  end
end
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.text.SimpleDateFormat
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

object HmacUtil {
    private const val ALGORITHM = "HmacSHA256"
    private const val EXPIRES_IN_SEC = 120

    fun isValid(
        method: String,
        uri: String,
        hmacDatetime: String,
        queryString: String,
        payload: String,
        reqSignature: String,
        secretKey: String
    ): Boolean {
        // hmacDateTime 기준 2분 경과 시 만료 처리
        val date = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX").parse(hmacDatetime)
        val diff = Date().time - date.time
        val diffSec = (diff / 1000).toInt()
        if (diffSec > EXPIRES_IN_SEC) {
            return false
        }
        val stringToSign = """
             $method
             $uri
             $hmacDatetime
             $queryString
             ${sha256hash(payload)}
             """.trimIndent()
        val genSignature = generate(stringToSign, secretKey)
        return reqSignature == genSignature
    }

    private fun generate(plainText: String, key: String): String {
        val signingKey = SecretKeySpec(key.toByteArray(), ALGORITHM)
        val mac = Mac.getInstance(ALGORITHM)
        mac.init(signingKey)
        return base64encode(toHexString(mac.doFinal(plainText.toByteArray())))
    }

    private fun sha256hash(str: String): String? {
        return try {
            val sh = MessageDigest.getInstance("SHA-256")
            sh.update(str.toByteArray())
            val byteData = sh.digest()
            val sb = StringBuffer()
            for (i in byteData.indices) {
                sb.append(((byteData[i].toInt() and 0xff) + 0x100).toString(16).substring(1))
            }
            sb.toString()
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
            null
        }
    }

    private fun toHexString(bytes: ByteArray): String {
        val formatter = Formatter()
        for (b in bytes) {
            formatter.format("%02x", b)
        }
        return formatter.toString()
    }

    private fun base64encode(str: String): String {
        val targetBytes = str.toByteArray()
        return Base64.getEncoder().encodeToString(targetBytes)
    }
}

API 호출 시 검증에 필요한 값은 요청 헤더로 전달됩니다. 헤더에서 추출한 signature 와 API 호출 정보로 생성한 signature 가 동일한 경우 유효한 요청입니다.

헤더

설명

X-Hmac-Datetime

HMAC 생성 시간 (signature 생성시에도 포함되는 값)

X-Hmac-Signature

HMAC Signature

signature 생성 예시

아래는 signature 생성에 사용 되는 데이터 예시입니다.

데이터

값

method

(대문자)

POST

uri

/api/offerwall/reward

hmacDateTime

2020-06-08T16:56:34+09:00

queryString

(빈값)

payload

(raw body)

{"campaign_id":"1","uid":"test_uid","advertising_id":"d2ca81dc-e3bc-4449-aa8b-f7b0f62b76b8","platform":1,"reward":100,"reward_type":0,"ad_name":"테스트 광고명!","repeat_participate_type":0,"click_key":"MTU5MTYwMzA2OTA4ODo-PDp0ZXN0X3VpZDo-PDp1U0hIaE5wOTZQeGpGaDFOUjlRR2NiU0U"}

위 데이터로 signature 를 생성한 결과입니다. (secret_key 는 test_secret_key 사용) 직접 구현한 모듈로 테스트 시 동일한 signature 가 생성 되는지 확인해주세요.

처리결과

값

SHA256(payload)

04dd512aa6c17b5e1f38cc3c2d9f652ea22878d51e5ea483161852f20e85bde9

StringToSign

POST

/api/offerwall/reward

2020-06-08T16:56:34+09:00

04dd512aa6c17b5e1f38cc3c2d9f652ea22878d51e5ea483161852f20e85bde9

HMAC Signature

MDY4MzYwNzc2MWYxZmViMTcxNDczZmYyNzVjY2ZlODMzYTU2OWVmMmI0MzE0N2RkZDBmZGY1MTJlMmEzMjE0Nw==

StringToSign값의 개행 처리에 주의해서 Signature값을 생성해주세요.

샘플 코드

실제 signature 검증과 관련하여 아래의 샘플을 통해 확인하실 수 있습니다.

Previous캠페인 완료Next2.5.0 변경사항

Last updated 1 year ago

Was this helpful?

payload로 만든 SHA256 해시값은 등의 해시 생성 툴을 통해서도 확인할 수 있습니다.

Node JS 샘플:

Python 3 샘플:

Java 샘플:

https://emn178.github.io/online-tools/sha256.html
https://replit.com/@adison-ads/adison-hmac-sample-nodejs
https://replit.com/@adison-ads/adison-hmac-sample-python
https://replit.com/@adison-ads/adison-hmac-sample-java