نمونه‌های اتصال بک‌اند

کلید محرمانه فقط در بک‌اند، بررسی نتیجه با قرارداد سایت

کلید محرمانه فقط در بک‌اند، بررسی نتیجه با قرارداد سایت

کلید محرمانه فقط در بک‌اند، بررسی نتیجه با قرارداد سایت

کلید محرمانه فقط در بک‌اند، بررسی نتیجه با قرارداد سایت

کلید محرمانه فقط در بک‌اند، بررسی نتیجه با قرارداد سایت

کلید محرمانه فقط در بک‌اند، بررسی نتیجه با قرارداد سایت

Captchas.irمرکز توسعه‌دهندگان

نمونه‌های اتصال HTTP برای HTML، PHP، Python، Node.js و Go با تفکیک کلید مرورگر و اعتبار بک‌اند

چرا

یکپارچه‌سازی ساده، بدون سازش امنیتی

هر طرف فقط همان چیزی را می‌بیند که لازم دارد

برای بازدیدکننده

بدون پازل تصویری
Cap یک proof-of-work است که مرورگر حل می‌کند؛ بازدیدکننده فقط یک کادر می‌بیند
فارسی
با lang: "fa" برچسب‌های ویجت فارسی می‌شوند
بدون کوکی
ویجت با credentials: "omit" درخواست می‌دهد و کوکی‌ای روی سایت شما نمی‌گذارد

تأیید

رسید گره‌خورده
رسید فقط برای همان سایت، action، intent و hash درخواست معتبر است
یک بار مصرف
مصرف دوم 409 ALREADY_CONSUMED می‌گیرد، حتی در درخواست‌های هم‌زمان
Fail-closed
نتیجهٔ نامعلوم هرگز موفقیت نیست؛ بک‌اند شما هم نباید آن را قبول کند

یکپارچه‌سازی

HTTP ساده
نه SDK اختصاصی لازم است و نه وابستگی بیرونی؛ همهٔ نمونه‌ها فقط کتابخانهٔ استاندارد زبان را استفاده می‌کنند
دامنهٔ جدا یا همان مبدأ
Captchas.ir می‌تواند روی زیردامنهٔ خودش باشد؛ CORS فقط برای مبدأهای ثبت‌شده باز است
خطای قابل فهم
هر خطا code، message، request_id و retryable دارد
مسئولیت‌ها

چه کسی چه چیزی را نگه می‌دارد

مرزها روشن‌اند: کلید عمومی در مرورگر، اعتبار در بک‌اند و رمز موتور فقط در Captchas.ir

  • بک‌اند intent را می‌سازد و رسید را مصرف می‌کند
  • مرورگر نشست را باز می‌کند و چالش را حل می‌کند
  • Captchas.ir با Cap حرف می‌زند و رسید صادر می‌کند

اسرار و کلیدها

مرورگر

  • کلید عمومی سایت
  • completion_token نشست

بک‌اند شما

  • اعتبار dck_…
  • request_hash

فقط Captchas.ir

  • رمز سایت Cap
  • کلید مدیریت Cap
  • کلید داده
مراحل

سه قدم تا فرم محافظت‌شده

  1. سایت را بسازید

    در پنل یک سایت با مبدأهای مجاز بسازید و آن را به Cap وصل کنید

  2. کلیدها را بگیرید

    یک کلید عمومی و یک اعتبار بک‌اند با intents:create و receipts:verify بسازید

  3. کد را اضافه کنید

    ویجت را در صفحه بگذارید و دو فراخوانی بک‌اند را از نمونه‌های پایین بردارید

این‌ها نمونهٔ اتصال Cap هستند، نه SDK آمادهٔ محصول احراز هویت، CSRF، اعتبارسنجی فرم، هش canonical محتوای نهایی و اجرای یک‌بارهٔ عملیات تجاری بر عهدهٔ برنامهٔ شماست کلید idempotency تأیید را برای همان intent و همان بدنه در تلاش مجدد ثابت نگه دارید WHMCS و WinSCP فعلاً Soon هستند

نمونه‌کد

نمونه‌کد برای هر زبان

در همهٔ نمونه‌ها captcha.example.com را با دامنهٔ Captchas.ir خودتان عوض کنید. اعتبار و شناسهٔ سایت از متغیر محیطی خوانده می‌شوند

HTML و fetch

contact.html
<form id="contact" method="post" action="/contact">
  <textarea name="message" required></textarea>
  <div id="captcha"></div>
  <input type="hidden" name="dc_receipt">
  <button>ارسال</button>
</form>

<script src="https://captcha.example.com/widget/cap.js"></script>
<script src="https://captcha.example.com/widget/vendor/cap.min.js"></script>
<script src="/js/contact.js"></script>
contact.js
const API = "https://captcha.example.com";
const SITE_KEY = "PUBLIC_SITE_KEY_FROM_PANEL";

(async () => {
  // Your backend creates the intent and keeps request_hash in the server session.
  const { intent_id } = await fetch("/captcha/intent", { method: "POST" }).then((r) => r.json());

  const res = await fetch(API + "/v1/sessions", {
    method: "POST",
    credentials: "omit",
    headers: {
      "content-type": "application/json",
      "x-digicaptcha-site-key": SITE_KEY,
      "idempotency-key": "sess-" + crypto.randomUUID(),
    },
    body: JSON.stringify({ intent_id, locale: "fa" }),
  });
  const session = await res.json();
  if (!res.ok) throw new Error(session.error.code);

  const done = await DigiCaptcha.mountCap({
    apiBase: API,
    siteKey: SITE_KEY,
    sessionId: session.session_id,
    completionToken: session.completion_token,
    container: document.getElementById("captcha"),
    lang: "fa",
  });
  document.querySelector("#contact [name=dc_receipt]").value = done.receipt;
})();

CSP لازم برای صفحهٔ میزبان در مستندات ویجت آمده است

PHP

captcha.php
<?php
const DC_API = 'https://captcha.example.com';

function dc_post(string $path, array $body, string $idemKey): array {
    $ch = curl_init(DC_API . $path);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . getenv('DIGICAPTCHA_CREDENTIAL'),
            'Idempotency-Key: ' . $idemKey,
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS     => json_encode($body),
    ]);
    $raw = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);
    return [$status, $raw === false ? null : json_decode($raw, true)];
}

function dc_hash(array $fields): string {
    ksort($fields);
    return 'sha256:' . hash('sha256', json_encode($fields, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
}

// POST /captcha/intent
function dc_create_intent(string $action): string {
    $hash = dc_hash(['action' => $action, 'nonce' => bin2hex(random_bytes(16))]);
    [$status, $res] = dc_post('/v1/intents', [
        'site_id'            => getenv('DIGICAPTCHA_SITE_ID'),
        'action'             => $action,
        'request_hash'       => $hash,
        'binding_version'    => 'jcs-v1',
        'expires_in_seconds' => 300,
    ], 'intent-' . bin2hex(random_bytes(16)));
    if ($status !== 201) {
        throw new RuntimeException($res['error']['code'] ?? 'DIGICAPTCHA_UNAVAILABLE');
    }
    $_SESSION['dc'] = ['intent_id' => $res['intent_id'], 'action' => $action, 'hash' => $hash];
    return $res['intent_id'];
}

// On form submit. Anything but 200 with success:true is a rejection.
function dc_verify(string $receipt): bool {
    $dc = $_SESSION['dc'] ?? null;
    if (!$dc || $receipt === '') {
        return false;
    }
    [$status, $res] = dc_post('/v1/siteverify', [
        'receipt'      => $receipt,
        'intent_id'    => $dc['intent_id'],
        'action'       => $dc['action'],
        'request_hash' => $dc['hash'],
    ], 'verify-' . $dc['intent_id']);
    $verified = $status === 200 && ($res['success'] ?? false) === true;
    if ($verified) unset($_SESSION['dc']);
    return $verified;
}

Python

captcha.py
import hashlib, json, os, secrets, urllib.error, urllib.request

API = "https://captcha.example.com"


def dc_post(path, body, idem_key):
    req = urllib.request.Request(
        API + path,
        data=json.dumps(body).encode(),
        method="POST",
        headers={
            "Authorization": "Bearer " + os.environ["DIGICAPTCHA_CREDENTIAL"],
            "Idempotency-Key": idem_key,
            "Content-Type": "application/json",
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=5) as res:
            return res.status, json.load(res)
    except urllib.error.HTTPError as err:
        return err.code, json.load(err)


def request_hash(fields):
    canonical = json.dumps(fields, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    return "sha256:" + hashlib.sha256(canonical.encode()).hexdigest()


def create_intent(action):
    """Returns (intent_id, request_hash); keep both in the server session."""
    rh = request_hash({"action": action, "nonce": secrets.token_hex(16)})
    status, body = dc_post("/v1/intents", {
        "site_id": os.environ["DIGICAPTCHA_SITE_ID"],
        "action": action,
        "request_hash": rh,
        "binding_version": "jcs-v1",
        "expires_in_seconds": 300,
    }, "intent-" + secrets.token_hex(16))
    if status != 201:
        raise RuntimeError(body["error"]["code"])
    return body["intent_id"], rh


def verify(receipt, intent_id, action, rh):
    try:
        status, body = dc_post("/v1/siteverify", {
            "receipt": receipt,
            "intent_id": intent_id,
            "action": action,
            "request_hash": rh,
        }, "verify-" + intent_id)
    except (OSError, ValueError):
        return False  # network failure is a rejection
    return status == 200 and body.get("success") is True

Node.js

captcha.mjs · Node 18+
import { createHash, randomUUID } from "node:crypto";

const API = "https://captcha.example.com";

async function dcPost(path, body, idemKey) {
  const res = await fetch(API + path, {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.DIGICAPTCHA_CREDENTIAL}`,
      "idempotency-key": idemKey,
      "content-type": "application/json",
    },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(5000),
  });
  return { status: res.status, body: await res.json() };
}

// Flat objects only: keys sorted, compact JSON.
function requestHash(fields) {
  const sorted = Object.fromEntries(Object.entries(fields).sort(([a], [b]) => (a < b ? -1 : 1)));
  return "sha256:" + createHash("sha256").update(JSON.stringify(sorted)).digest("hex");
}

// Keep requestHash in the server session, never in the page.
export async function createIntent(action) {
  const rh = requestHash({ action, nonce: randomUUID() });
  const { status, body } = await dcPost("/v1/intents", {
    site_id: process.env.DIGICAPTCHA_SITE_ID,
    action,
    request_hash: rh,
    binding_version: "jcs-v1",
    expires_in_seconds: 300,
  }, `intent-${randomUUID()}`);
  if (status !== 201) throw new Error(body.error.code);
  return { intentId: body.intent_id, requestHash: rh };
}

export async function verify(receipt, intentId, action, rh) {
  try {
    const { status, body } = await dcPost("/v1/siteverify", {
      receipt,
      intent_id: intentId,
      action,
      request_hash: rh,
    }, `verify-${intentId}`);
    return status === 200 && body.success === true;
  } catch {
    return false; // timeout or network error is a rejection
  }
}

Go

captcha.go
package captcha

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"
)

const api = "https://captcha.example.com"

var client = &http.Client{Timeout: 5 * time.Second}

func randomHex() string {
	var b [16]byte
	_, _ = rand.Read(b[:])
	return hex.EncodeToString(b[:])
}

func post(ctx context.Context, path, idemKey string, body, out any) (int, error) {
	raw, err := json.Marshal(body)
	if err != nil {
		return 0, err
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, api+path, bytes.NewReader(raw))
	if err != nil {
		return 0, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("DIGICAPTCHA_CREDENTIAL"))
	req.Header.Set("Idempotency-Key", idemKey)
	req.Header.Set("Content-Type", "application/json")
	res, err := client.Do(req)
	if err != nil {
		return 0, err
	}
	defer res.Body.Close()
	return res.StatusCode, json.NewDecoder(res.Body).Decode(out)
}

// RequestHash relies on encoding/json sorting map keys.
func RequestHash(fields map[string]string) string {
	raw, _ := json.Marshal(fields)
	sum := sha256.Sum256(raw)
	return "sha256:" + hex.EncodeToString(sum[:])
}

// CreateIntent returns the intent ID and the hash to keep in the server session.
func CreateIntent(ctx context.Context, action string) (string, string, error) {
	hash := RequestHash(map[string]string{"action": action, "nonce": randomHex()})
	var out struct {
		IntentID string `json:"intent_id"`
	}
	status, err := post(ctx, "/v1/intents", "intent-"+randomHex(), map[string]any{
		"site_id":            os.Getenv("DIGICAPTCHA_SITE_ID"),
		"action":             action,
		"request_hash":       hash,
		"binding_version":    "jcs-v1",
		"expires_in_seconds": 300,
	}, &out)
	if err != nil || status != http.StatusCreated {
		return "", "", fmt.Errorf("digicaptcha intent: status %d: %v", status, err)
	}
	return out.IntentID, hash, nil
}

// Verify is true only for HTTP 200 with success:true.
func Verify(ctx context.Context, receipt, intentID, action, hash string) bool {
	var out struct {
		Success bool `json:"success"`
	}
	status, err := post(ctx, "/v1/siteverify", "verify-"+intentID, map[string]string{
		"receipt":      receipt,
		"intent_id":    intentID,
		"action":       action,
		"request_hash": hash,
	}, &out)
	return err == nil && status == http.StatusOK && out.Success
}

دربارهٔ request_hash در این نمونه‌ها hash از action و یک nonce تصادفی ساخته و در نشست سرور نگه داشته می‌شود. اگر می‌خواهید رسید به محتوای خود فرم گره بخورد، intent را هنگام ارسال فرم بسازید و hash را از همان فیلدها حساب کنید

وردپرس

افزونهٔ وردپرس در راه است

افزونهٔ رسمی وردپرس هنوز منتشر نشده و برنامه‌ریزی شده است. تا آن زمان می‌توانید نمونهٔ PHP همین صفحه را در قالب یا افزونهٔ اختصاصی خود استفاده کنید

وضعیت یکپارچه‌سازی‌ها

آماده

  • HTML و fetch آماده
  • PHP آماده
  • Python آماده
  • Node.js آماده
  • Go آماده

برنامه‌ریزی‌شده

  • افزونهٔ وردپرس به‌زودی
پرسش‌ها

پرسش‌های یکپارچه‌سازی

آیا SDK لازم است؟

نه. سمت مرورگر فقط /widget/cap.js و ویجت Cap که از سرور Captchas.ir سرو می‌شوند، و سمت سرور فقط دو درخواست HTTP با JSON

Captchas.ir می‌تواند روی دامنهٔ دیگری باشد؟

بله. مبدأ سایت خود را در پنل به فهرست مبدأهای مجاز اضافه کنید. Captchas.ir برای همان مبدأها به preflight پاسخ می‌دهد و هدر Access-Control-Allow-Origin را می‌فرستد. می‌توانید Captchas.ir را با یک مسیر در nginx روی همان مبدأ هم سرو کنید

اعتبار بک‌اند کجا نگه داشته شود؟

فقط روی سرور، مثلاً در متغیر محیطی. پنل آن را فقط یک بار نمایش می‌دهد. اگر گم شد یا لو رفت، از صفحهٔ سایت آن را بچرخانید

اگر siteverify پاسخ ندهد چه کنم؟

درخواست کاربر را رد کنید و از او بخواهید دوباره تلاش کند. اگر می‌خواهید همان siteverify را تکرار کنید، همان Idempotency-Key را بفرستید تا نتیجهٔ قبلی برگردد و رسید دو بار مصرف نشود

رسید چقدر اعتبار دارد؟

دو دقیقه. intent بین 30 تا 600 ثانیه و نشست حداکثر پنج دقیقه عمر دارد. اگر کاربر فرم را دیر ارسال کند، 410 SESSION_EXPIRED می‌گیرید و باید یک intent تازه بسازید

افزونهٔ وردپرس کی منتشر می‌شود؟

برنامه‌ریزی شده ولی تاریخی برای آن اعلام نشده است. این صفحه پس از انتشار به‌روز می‌شود

آماده برای اولین سایت

سایت بسازید، کلیدها را بگیرید و جریان را در playground امتحان کنید