Documentation Index
Fetch the complete documentation index at: https://docs.steppay.kr/llms.txt
Use this file to discover all available pages before exploring further.
웹훅 검증을 통해 Steppay에서 전송된 데이터가 실제로 Steppay에서 온 것인지 확인할 수 있습니다. 이 과정은 요청 헤더에 포함된 Steppay-Signature 값과 요청 본문을 비교하여 데이터를 검증하는 방식으로 이루어집니다.
사전 준비
웹훅 - 검증 KEY 준비하기
서명 검증을 위해 필요한 검증 키를 Steppay 포탈에서 확인할 수 있습니다.
- 포탈 > 설정 > 결제 설정 > 웹훅 설정으로 이동합니다.
- 검증하고자 하는 웹훅의 상세 페이지에 들어갑니다.
- 상세 페이지에서 “검증 KEY”를 복사합니다.
검증 단계
1. Steppay-Signature 헤더 구조 이해하기
timestamp=1706002316,key=BMFfPB/HjnZeJrwA4wC1csUDzkINZsaExF99X3/Q9phE=
Steppay-Signature 헤더는 다음과 같은 두 가지 정보를 포함합니다.
timestamp: 웹훅이 발송된 시간 정보
key: 검증을 위해 인코딩된 서명 값
2. timestamp와 요청 본문 결합하기
Steppay-Signature 헤더에서 timestamp 값을 추출합니다.
- 추출한
timestamp 값과 웹훅 요청의 본문 데이터(requestBody)를 "timestamp.requestBody" 형식으로 결합합니다.
3. 결합된 문자열 암호화하기
- 결합한 문자열을 Steppay 포탈에서 받은 검증 키와 함께
HmacSHA256 함수를 사용하여 암호화합니다.
- 이 과정에서 결합된 문자열은
"timestamp.requestBody" 형식이어야 합니다.
- 검증 키는 비밀 키로 사용됩니다.
- 암호화된 결과값을 Base64 형식으로 인코딩합니다.
4. 헤더의 서명 값과 암호화 결과 비교하기
- 인코딩된 결과값을
Steppay-Signature 헤더의 key 값과 비교합니다.
- 헤더의
key 값이 여러 개일 경우 ; 로 구분되며, 모든 key 값 중 하나라도 인코딩된 값과 일치하면 검증이 성공합니다.
헤더의 key 값이 여러 개일 경우 예시
timestamp=1706002316,key=BMFfPB/HjnZeJrwA4wC1csUDzkINZsaExF99X3/Q9phE=;H3uZhieE19k/eF3ARNwjeQhdrJErf8Z8THV108mnC9w=
검증 코드
검증 과정을 바로 사용하실 수 있도록 코드를 제공합니다.
-
Javascript
const crypto = require("crypto");
const SteppayWebhookUtil = {
/**
* Steppay-Signature 헤더 값과 요청 데이터, 비밀 키를 사용하여 서명을 검증합니다.
*
* @param {string} signature 수신받은 'Steppay-Signature' header 값
* @param {string} payload 수신받은 request data 전문
* @param {string} secret 포탈에서 발급받은 secret key
* @throws {Error} 서명이 유효하지 않을 경우 에러를 발생시킵니다.
*/
verifySignature: function (signature, payload, secret) {
// timestamp, payload로 encodeKeys 생성
const timestamp = signature
.split(",", 2)
.find((part) => part.startsWith("timestamp="))
.split("=")[1];
const encodeKey = this.encodeHmacSha256(
`${timestamp}.${payload}`,
secret
);
// header의 encodeKeys 추출
const headerEncodeKeys = signature
.split(",")
.find((part) => part.startsWith("key="))
.split("key=")[1]
.split(";");
// header의 encodeKeys와 생성한 encodeKeys 비교
if (
!headerEncodeKeys.some((headerEncodeKey) =>
headerEncodeKey.includes(encodeKey)
)
) {
throw new Error("Invalid signature");
}
},
/**
* HmacSha256로 인코딩합니다.
*
* @param {string} payload
* @param {string} secret
* @return {string}
*/
encodeHmacSha256: function (payload, secret) {
const hmac = crypto.createHmac("sha256", secret);
return hmac.update(payload).digest("base64");
},
};
module.exports = SteppayWebhookUtil;
-
Kotlin
검증에 지속적으로 실패할 경우
만약 요청을 클래스로 받았을 때, 클래스를 문자열로 변환하는 방식에 따라 의도한대로 문자열 변환이 되지 않을 수 있습니다. 만약 검증 과정에서 지속적으로 문제가 생긴다면, 처음부터 데이터를 String 형태로 받는 것이 더 나을 수 있습니다.
object SteppayWebhookUtil {
/**
* signature 검증합니다.
*
* @param signature 수신받은 'Steppay-Signature' header 값
* @param payload 수신받은 request data 전문
* @param secret 포탈에서 발급받은 secret key
* @throws SteppaySignatureException Steppay 에서 발생하는 예외에 대한 custom exception을 재정의하는 것을 권장 드립니다.
*/
fun verifySignature(
signature: String,
payload: String,
secret: String,
) {
// timestamp, payload로 encodeKeys 생성
val timestamp = signature.split(",", limit = 2)[0].split("=")[1].toLong()
val encodeKey = encodeHmacSha256("$timestamp.$payload", secret)
// header의 encodeKeys 추출
val headerEncodeKeys = signature.split(",")
.map { it.split("=", limit = 2) }
.filter { it[0] != "timestamp" }
.map { it[1] }
// header의 encodeKeys와 생성한 encodeKeys 비교
if (headerEncodeKeys.none { headerEncodeKey -> headerEncodeKey.contains(encodeKey) }) {
throw SteppaySignatureException("Invalid signature")
}
}
/**
* HmacSha256로 인코딩 합니다.
*
* @param payload String
* @param secret String
* @return String
*/
private fun encodeHmacSha256(
payload: String,
secret: String,
): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secret.toByteArray(Charsets.UTF_8), "HmacSHA256")
sha256Hmac.init(secretKey)
return Base64.getEncoder().encodeToString(sha256Hmac.doFinal(payload.toByteArray(Charsets.UTF_8)))
}
}