برای بازدیدکننده
- بدون پازل تصویری
- Cap یک proof-of-work است که مرورگر حل میکند؛ بازدیدکننده فقط یک کادر میبیند
- فارسی
- با
lang: "fa"برچسبهای ویجت فارسی میشوند - بدون کوکی
- ویجت با
credentials: "omit"درخواست میدهد و کوکیای روی سایت شما نمیگذارد
کلید محرمانه فقط در بکاند، بررسی نتیجه با قرارداد سایت
کلید محرمانه فقط در بکاند، بررسی نتیجه با قرارداد سایت
کلید محرمانه فقط در بکاند، بررسی نتیجه با قرارداد سایت
کلید محرمانه فقط در بکاند، بررسی نتیجه با قرارداد سایت
کلید محرمانه فقط در بکاند، بررسی نتیجه با قرارداد سایت
کلید محرمانه فقط در بکاند، بررسی نتیجه با قرارداد سایت
CONNECT / BUILD
نمونههای اتصال HTTP برای HTML، PHP، Python، Node.js و Go با تفکیک کلید مرورگر و اعتبار بکاند
هر طرف فقط همان چیزی را میبیند که لازم دارد
lang: "fa" برچسبهای ویجت فارسی میشوندcredentials: "omit" درخواست میدهد و کوکیای روی سایت شما نمیگذارد409 ALREADY_CONSUMED میگیرد، حتی در درخواستهای همزمانcode، message، request_id و retryable داردمرزها روشناند: کلید عمومی در مرورگر، اعتبار در بکاند و رمز موتور فقط در Captchas.ir
completion_token نشستdck_…request_hashدر پنل یک سایت با مبدأهای مجاز بسازید و آن را به Cap وصل کنید
یک کلید عمومی و یک اعتبار بکاند با intents:create و receipts:verify بسازید
ویجت را در صفحه بگذارید و دو فراخوانی بکاند را از نمونههای پایین بردارید
اینها نمونهٔ اتصال Cap هستند، نه SDK آمادهٔ محصول احراز هویت، CSRF، اعتبارسنجی فرم، هش canonical محتوای نهایی و اجرای یکبارهٔ عملیات تجاری بر عهدهٔ برنامهٔ شماست کلید idempotency تأیید را برای همان intent و همان بدنه در تلاش مجدد ثابت نگه دارید WHMCS و WinSCP فعلاً Soon هستند
در همهٔ نمونهها captcha.example.com را با دامنهٔ Captchas.ir خودتان عوض کنید. اعتبار و شناسهٔ سایت از متغیر محیطی خوانده میشوند
<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>
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
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;
}
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
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
}
}
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 همین صفحه را در قالب یا افزونهٔ اختصاصی خود استفاده کنید
نه. سمت مرورگر فقط /widget/cap.js و ویجت Cap که از سرور Captchas.ir سرو میشوند، و سمت سرور فقط دو درخواست HTTP با JSON
بله. مبدأ سایت خود را در پنل به فهرست مبدأهای مجاز اضافه کنید. Captchas.ir برای همان مبدأها به preflight پاسخ میدهد و هدر Access-Control-Allow-Origin را میفرستد. میتوانید Captchas.ir را با یک مسیر در nginx روی همان مبدأ هم سرو کنید
فقط روی سرور، مثلاً در متغیر محیطی. پنل آن را فقط یک بار نمایش میدهد. اگر گم شد یا لو رفت، از صفحهٔ سایت آن را بچرخانید
درخواست کاربر را رد کنید و از او بخواهید دوباره تلاش کند. اگر میخواهید همان siteverify را تکرار کنید، همان Idempotency-Key را بفرستید تا نتیجهٔ قبلی برگردد و رسید دو بار مصرف نشود
دو دقیقه. intent بین 30 تا 600 ثانیه و نشست حداکثر پنج دقیقه عمر دارد. اگر کاربر فرم را دیر ارسال کند، 410 SESSION_EXPIRED میگیرید و باید یک intent تازه بسازید
برنامهریزی شده ولی تاریخی برای آن اعلام نشده است. این صفحه پس از انتشار بهروز میشود
سایت بسازید، کلیدها را بگیرید و جریان را در playground امتحان کنید