Bỏ qua, tới nội dung
Tài liệu APIv1

Nền tảng

Xác thực: khoá API và JWT

Gọi DocBuilder bằng khoá API hoặc JWT HS256, ký config nhúng trình soạn thảo, kiểm token bằng Node, PHP, Python, C#, thời hạn và xoay khoá.

Trong trang này

O3O dùng ba loại thông tin xác thực cho ba chiều gọi khác nhau. Bảng dưới cho biết loại nào dùng ở đâu; các mục sau đi vào chi tiết từng loại.

Dùng choCách gửiKhoá phía máy chủ O3OGhi chú
DocBuilder /v1/*Authorization: Bearer <khoá API>O3O_DOCBUILDER_API_KEYSKhai được nhiều khoá. GET /v1/status không cần xác thực.
DocBuilder /v1/*Authorization: Bearer <JWT>O3O_DOCBUILDER_JWT_SECRETTuỳ chọn; chỉ bật khi biến này có giá trị.
Tạo phiên nhúng POST /o3o/embed/sessionTrường token trong configO3O_EMBED_JWT_SECRETJWT HS256 chứa chính config của trình soạn thảo.
Các yêu cầu khác của phiên nhúngAuthorization: Bearer <access_token>Gate tự sinh khi tạo phiênapi.js tự gửi; sai thì 401 invalid_session_token.
Callback O3O gửi tới bạnHeader X-O3O-Signature (HMAC SHA-256)O3O_EMBED_CALLBACK_SECRET, O3O_DOCBUILDER_CALLBACK_SECRETChiều ngược lại: bạn kiểm chữ ký của O3O. Xem Callback và webhook.

Khoá API cho DocBuilder#

Khoá API là cách đơn giản nhất để máy chủ của bạn gọi DocBuilder. Quản trị khai khoá trong biến O3O_DOCBUILDER_API_KEYS: danh sách phân tách bằng dấu phẩy, mỗi phần tử dạng ten:khoa hoặc chỉ khoa. Phần ten giúp phân biệt khoá của từng ứng dụng gọi.

  • Khoá phải khớp ^[A-Za-z0-9_-]{24,128}$; nên bắt đầu bằng o3o_.
  • Phần tử chứa CHANGE_ME bị bỏ qua. Danh sách rỗng thì mọi endpoint cần xác thực trả 401 unauthorized với detail.reason = "no_keys_configured".
  • Khoá chỉ gửi trong header Authorization; khoá trên chuỗi truy vấn không được nhận. Máy chủ so khoá bằng hàm so sánh thời gian hằng.
  • Hạn mức tần suất tính chung cho cả instance, không theo từng khoá. Xem Hạn mức theo gói.
BashSinh khoá và khai báo trong .env
# Sinh một khoá ngẫu nhiên 52 ký tự
echo "o3o_$(openssl rand -hex 24)"

# online/.env: nhiều khoá, mỗi ứng dụng một khoá
O3O_DOCBUILDER_API_KEYS=crm:o3o_9f2c61d04b7e3a58c1d2e9f0a7b6c5d4e3f2a1b0c9d8e7f6,ketoan:o3o_1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7081

# Nạp lại cấu hình (chạy trong thư mục online/)
docker compose --env-file .env -f docker/compose.dev.yml up -d o3o-docbuilder
GET/v1/limits

Cách nhanh nhất để thử khoá: trả hạn mức hiệu lực của gói nếu khoá đúng.

Xác thực: BearerCộng đồngDoanh nghiệp
Gọi thử bằng khoá API
curl -s http://localhost:8080/v1/limits \
  -H "Authorization: Bearer O3O_DEMO_KEY"
// Node.js 18+ (tệp .mjs hoặc "type": "module")
const res = await fetch("http://localhost:8080/v1/limits", {
  headers: { Authorization: "Bearer O3O_DEMO_KEY" }
});
console.log(res.status, await res.json());
# pip install requests
import requests

r = requests.get(
    "http://localhost:8080/v1/limits",
    headers={"Authorization": "Bearer O3O_DEMO_KEY"},
    timeout=30,
)
print(r.status_code, r.json())
<?php
$ch = curl_init('http://localhost:8080/v1/limits');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ['Authorization: Bearer O3O_DEMO_KEY'],
    CURLOPT_RETURNTRANSFER => true,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
echo $status, ' ', $body, PHP_EOL;
using System.Net.Http.Headers;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "O3O_DEMO_KEY");
var res = await http.GetAsync("http://localhost:8080/v1/limits");
Console.WriteLine($"{(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
401Thiếu khoá hoặc khoá sai. detail.reasonmissing, invalid hoặc no_keys_configured.
{
  "error": {
    "code": "unauthorized",
    "message": "Thiếu hoặc sai khoá API.",
    "detail": {
      "reason": "missing"
    },
    "request_id": "req_0123456789abcdef"
  }
}

JWT cho DocBuilder#

Khi quản trị đặt O3O_DOCBUILDER_JWT_SECRET (tối thiểu 32 ký tự), header Authorization: Bearer được phép mang JWT HS256 thay cho khoá API. Giá trị có đúng hai dấu chấm được coi là JWT. Dùng JWT khi bạn muốn token tự hết hạn, hoặc cần gắn nhãn ứng dụng gọi vào nhật ký qua sub.

Claim của JWT gọi DocBuilder

  • expnumberbắt buộc
    Giây Unix. Bắt buộc. Thời hạn còn lại không quá 24 giờ; cho phép lệch đồng hồ 60 giây.
  • substringtuỳ chọn
    Nhãn ghi vào nhật ký, ví dụ tên ứng dụng gọi.
  • iatnumbertuỳ chọn
    Thời điểm ký. Các thư viện JWT thường tự thêm.
Ký JWT rồi gọi DocBuilder
# Ký bằng bất kỳ cách nào ở các thẻ bên cạnh rồi gửi trong header
TOKEN="$(python3 sign_docbuilder_jwt.py)"
curl -s http://localhost:8080/v1/limits \
  -H "Authorization: Bearer $TOKEN"
// npm install jsonwebtoken   (tệp .mjs hoặc "type": "module")
import jwt from "jsonwebtoken";

const token = jwt.sign({ sub: "crm" }, process.env.O3O_DOCBUILDER_JWT_SECRET, {
  algorithm: "HS256",
  expiresIn: "15m" // đủ cho một đợt gọi; tối đa 24 giờ
});
const res = await fetch("http://localhost:8080/v1/limits", {
  headers: { Authorization: `Bearer ${token}` }
});
console.log(res.status, await res.json());
# pip install PyJWT requests
import os
import time

import jwt
import requests

now = int(time.time())
token = jwt.encode(
    {"sub": "crm", "iat": now, "exp": now + 15 * 60},  # đủ cho một đợt gọi; tối đa 24 giờ
    os.environ["O3O_DOCBUILDER_JWT_SECRET"],
    algorithm="HS256",
)
r = requests.get(
    "http://localhost:8080/v1/limits",
    headers={"Authorization": f"Bearer {token}"},
    timeout=30,
)
print(r.status_code, r.json())
<?php
// composer require firebase/php-jwt
require __DIR__ . '/vendor/autoload.php';

use Firebase\JWT\JWT;

$now = time();
$token = JWT::encode(
    ['sub' => 'crm', 'iat' => $now, 'exp' => $now + 15 * 60], // đủ cho một đợt gọi; tối đa 24 giờ
    getenv('O3O_DOCBUILDER_JWT_SECRET'),
    'HS256'
);
$ch = curl_init('http://localhost:8080/v1/limits');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER => ["Authorization: Bearer $token"],
    CURLOPT_RETURNTRANSFER => true,
]);
echo curl_exec($ch), PHP_EOL;
using System.Net.Http.Headers;

// Lớp O3OJwt ở mục "Tự kiểm token" bên dưới; không cần thư viện ngoài (.NET 6+)
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string token = O3OJwt.Sign(new { sub = "crm", iat = now, exp = now + 15 * 60 },
    Environment.GetEnvironmentVariable("O3O_DOCBUILDER_JWT_SECRET")!);

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Console.WriteLine(await http.GetStringAsync("http://localhost:8080/v1/limits"));

JWT cho config nhúng trình soạn thảo#

Khi máy chủ đặt O3O_EMBED_JWT_SECRET, mọi yêu cầu tạo phiên nhúng phải kèm token. Payload của token CHỨA CHÍNH config: các khoá document, editor, ui giống hệt config của O3O.Editor, cộng expiat. Khi token hợp lệ, gate dùng config trong token và bỏ qua các trường cùng tên gửi kèm bên ngoài, nên trang chỉ cần gửi {token}. Token do MÁY CHỦ của bạn ký; khoá không bao giờ xuống trình duyệt.

Claim của JWT config nhúng

  • documentobjectbắt buộc
    Tài liệu cần mở: url, title, fileType, key.
  • editorobjectbắt buộc
    mode, lang, user, callbackUrl.
  • uiobjecttuỳ chọn
    Tuỳ chọn giao diện mà v1 cài đặt.
  • expnumberbắt buộc
    Giây Unix. Tối đa 24 giờ kể từ iat (hoặc từ hiện tại khi không có iat); lệch đồng hồ cho phép 60 giây.
  • iatnumbertuỳ chọn
    Nên có.
Cấu hình máy chủCó token hợp lệKhông có tokenToken sai hoặc hết hạn
O3O_EMBED_JWT_SECRETnhận401 token_required401 invalid_token / 401 token_expired
Không có khoá, O3O_EMBED_ALLOW_UNSIGNED=1 (chỉ máy DEV)bỏ qua token, dùng config trầnnhậnbỏ qua token, dùng config trần
Không có khoá, O3O_EMBED_ALLOW_UNSIGNED=0401 embed_auth_not_configured401 embed_auth_not_configured401 embed_auth_not_configured
POST/o3o/embed/session

api.js gọi endpoint này khi bạn tạo O3O.Editor; token nằm trong trường token của thân yêu cầu.

Xác thực: JWTCộng đồngDoanh nghiệp
Ký config nhúng ở máy chủ của bạn
// Máy chủ của bạn — npm install jsonwebtoken   (tệp .mjs hoặc "type": "module")
import jwt from "jsonwebtoken";

const config = {
  document: {
    url: "https://example.com/files/hop-dong.docx",
    title: "Hợp đồng mẫu.docx",
    fileType: "docx",
    key: "hopdong-42-v7"
  },
  editor: {
    mode: "edit",
    lang: "vi",
    user: { id: "u-1001", name: "Nguyễn Văn A" },
    callbackUrl: "https://example.com/o3o/callback"
  },
  ui: { closeButton: true }
};

// Khoá lấy từ biến môi trường của MÁY CHỦ bạn, trùng O3O_EMBED_JWT_SECRET của O3O
const token = jwt.sign(config, process.env.O3O_EMBED_JWT_SECRET, {
  algorithm: "HS256",
  expiresIn: "10m" // chỉ cần đủ cho lúc trang nạp
});

// Trình duyệt: chỉ cần token do máy chủ trả về
// const editor = new O3O.Editor("o3o-editor", {
//   token,
//   events: { onError: (e) => console.error(e.code, e.message) }
// });
# pip install PyJWT
import os
import time

import jwt

now = int(time.time())
payload = {
    "document": {
        "url": "https://example.com/files/hop-dong.docx",
        "title": "Hợp đồng mẫu.docx",
        "fileType": "docx",
        "key": "hopdong-42-v7",
    },
    "editor": {
        "mode": "edit",
        "lang": "vi",
        "user": {"id": "u-1001", "name": "Nguyễn Văn A"},
        "callbackUrl": "https://example.com/o3o/callback",
    },
    "ui": {"closeButton": True},
    "iat": now,
    "exp": now + 10 * 60,  # chỉ cần đủ cho lúc trang nạp
}
# Khoá lấy từ biến môi trường của MÁY CHỦ bạn, trùng O3O_EMBED_JWT_SECRET của O3O
token = jwt.encode(payload, os.environ["O3O_EMBED_JWT_SECRET"], algorithm="HS256")
<?php
// composer require firebase/php-jwt
require __DIR__ . '/vendor/autoload.php';

use Firebase\JWT\JWT;

$now = time();
$payload = [
    'document' => [
        'url' => 'https://example.com/files/hop-dong.docx',
        'title' => 'Hợp đồng mẫu.docx',
        'fileType' => 'docx',
        'key' => 'hopdong-42-v7',
    ],
    'editor' => [
        'mode' => 'edit',
        'lang' => 'vi',
        'user' => ['id' => 'u-1001', 'name' => 'Nguyễn Văn A'],
        'callbackUrl' => 'https://example.com/o3o/callback',
    ],
    'ui' => ['closeButton' => true],
    'iat' => $now,
    'exp' => $now + 10 * 60, // chỉ cần đủ cho lúc trang nạp
];
// Khoá lấy từ biến môi trường của MÁY CHỦ bạn, trùng O3O_EMBED_JWT_SECRET của O3O
$token = JWT::encode($payload, getenv('O3O_EMBED_JWT_SECRET'), 'HS256');
// Lớp O3OJwt ở mục "Tự kiểm token" bên dưới; không cần thư viện ngoài (.NET 6+)
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var payload = new
{
    document = new
    {
        url = "https://example.com/files/hop-dong.docx",
        title = "Hợp đồng mẫu.docx",
        fileType = "docx",
        key = "hopdong-42-v7"
    },
    editor = new
    {
        mode = "edit",
        lang = "vi",
        user = new { id = "u-1001", name = "Nguyễn Văn A" },
        callbackUrl = "https://example.com/o3o/callback"
    },
    ui = new { closeButton = true },
    iat = now,
    exp = now + 10 * 60 // chỉ cần đủ cho lúc trang nạp
};
// Khoá lấy từ biến môi trường của MÁY CHỦ bạn, trùng O3O_EMBED_JWT_SECRET của O3O
string token = O3OJwt.Sign(payload, Environment.GetEnvironmentVariable("O3O_EMBED_JWT_SECRET")!);

Tự kiểm token khi gỡ lỗi#

Máy chủ O3O mới là nơi quyết định token hợp lệ hay không. Các hàm dưới đây áp đúng các quy tắc của O3O (HS256, bắt buộc exp, lệch 60 giây, không quá 24 giờ) để bạn kiểm token trong kiểm thử tự động trước khi gửi đi.

Kiểm JWT HS256
// npm install jsonwebtoken   (tệp .mjs hoặc "type": "module")
import jwt from "jsonwebtoken";

export function checkToken(token, secret) {
  // Chỉ nhận HS256, lệch đồng hồ 60 giây
  const payload = jwt.verify(token, secret, { algorithms: ["HS256"], clockTolerance: 60 });
  if (typeof payload.exp !== "number") throw new Error("thiếu exp");
  if (payload.exp - Date.now() / 1000 > 24 * 3600) throw new Error("exp xa quá 24 giờ");
  return payload;
}
# pip install PyJWT
import time

import jwt


def check_token(token: str, secret: str) -> dict:
    # Chỉ nhận HS256, bắt buộc exp, lệch đồng hồ 60 giây
    payload = jwt.decode(token, secret, algorithms=["HS256"], leeway=60,
                         options={"require": ["exp"]})
    if payload["exp"] - time.time() > 24 * 3600:
        raise ValueError("exp xa quá 24 giờ")
    return payload
<?php
// composer require firebase/php-jwt
require __DIR__ . '/vendor/autoload.php';

use Firebase\JWT\JWT;
use Firebase\JWT\Key;

function check_token(string $token, string $secret): object
{
    // Chỉ nhận HS256, lệch đồng hồ 60 giây
    JWT::$leeway = 60;
    $payload = JWT::decode($token, new Key($secret, 'HS256'));
    if (!isset($payload->exp) || $payload->exp - time() > 24 * 3600) {
        throw new UnexpectedValueException('exp thiếu hoặc xa quá 24 giờ');
    }
    return $payload;
}
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

// Cách dùng: dotnet run -- <token>
string token = args.Length > 0 ? args[0] : "";
JsonElement? payload = O3OJwt.Verify(token, Environment.GetEnvironmentVariable("O3O_EMBED_JWT_SECRET")!);
Console.WriteLine(payload is null ? "token không hợp lệ" : payload.Value.GetProperty("exp").ToString());

// Ký và kiểm JWT HS256, không cần thư viện ngoài (.NET 6+)
public static class O3OJwt
{
    public static string Sign(object payload, string secret)
    {
        string header = B64Url(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
        string body = B64Url(JsonSerializer.SerializeToUtf8Bytes(payload));
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        byte[] sig = hmac.ComputeHash(Encoding.ASCII.GetBytes(header + "." + body));
        return header + "." + body + "." + B64Url(sig);
    }

    // Trả payload nếu hợp lệ, ngược lại null
    public static JsonElement? Verify(string token, string secret, int leewaySeconds = 60)
    {
        try
        {
            string[] parts = token.Split('.');
            if (parts.Length != 3) return null;
            using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
            byte[] expected = hmac.ComputeHash(Encoding.ASCII.GetBytes(parts[0] + "." + parts[1]));
            if (!CryptographicOperations.FixedTimeEquals(expected, B64UrlDecode(parts[2]))) return null;
            using var header = JsonDocument.Parse(B64UrlDecode(parts[0]));
            if (!header.RootElement.TryGetProperty("alg", out JsonElement alg) || alg.GetString() != "HS256") return null;
            using var doc = JsonDocument.Parse(B64UrlDecode(parts[1]));
            JsonElement payload = doc.RootElement.Clone();
            if (!payload.TryGetProperty("exp", out JsonElement exp)) return null;
            long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            if (now > exp.GetInt64() + leewaySeconds) return null;
            if (exp.GetInt64() - now > 24 * 3600) return null;
            return payload;
        }
        catch (FormatException) { return null; }
        catch (JsonException) { return null; }
    }

    static string B64Url(byte[] data) =>
        Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');

    static byte[] B64UrlDecode(string s)
    {
        s = s.Replace('-', '+').Replace('_', '/');
        return Convert.FromBase64String(s.PadRight(s.Length + (4 - s.Length % 4) % 4, '='));
    }
}

Thời hạn#

Thông tinThời hạnHết hạn thì
JWT gọi DocBuilderexp bắt buộc, còn lại tối đa 24 giờ401 token_expired
JWT config nhúngexp bắt buộc, tối đa 24 giờ401 token_expired khi tạo phiên; phiên đã mở không bị ảnh hưởng
access_token của phiên nhúngO3O_EMBED_SESSION_TTL_MINUTES, mặc định 720 phútPhiên chuyển expired; lần lưu cuối vẫn được nhận
URL tải bản lưu của tài liệu nhúngO3O_EMBED_RETAIN_HOURS, mặc định 24 giờ410 link_expired
Tệp kết quả của DocBuilderresult_ttl_minutes của gói: 15 phút (cộng đồng), 1.440 phút (doanh nghiệp)410 gone

Xoay khoá#

Biến môi trường chỉ có hiệu lực khi container được tạo lại. Sau khi sửa .env, chạy docker compose ... up -d: Compose tạo lại đúng những dịch vụ có cấu hình đổi.

  1. Khoá API của DocBuilder: không gián đoạn

    Thêm khoá mới vào O3O_DOCBUILDER_API_KEYS và GIỮ khoá cũ, nạp lại o3o-docbuilder. Chuyển từng ứng dụng gọi sang khoá mới. Khi nhật ký không còn yêu cầu dùng khoá cũ, xoá nó khỏi danh sách và nạp lại lần nữa.
  2. Khoá JWT (O3O_DOCBUILDER_JWT_SECRET, O3O_EMBED_JWT_SECRET)

    v1 nhận MỘT khoá cho mỗi loại, nên token ký bằng khoá cũ bị từ chối ngay khi khoá mới có hiệu lực. Chọn giờ thấp điểm, đổi khoá ở ứng dụng ký và ở .env cùng lúc. Với DocBuilder, có thể tạm gọi bằng khoá API trong lúc chuyển. Với lớp nhúng, chỉ việc TẠO phiên mới bị ảnh hưởng; phiên đang mở tiếp tục bằng access_token của nó.
  3. Khoá ký callback (O3O_EMBED_CALLBACK_SECRET, O3O_DOCBUILDER_CALLBACK_SECRET)

    Cho bộ nhận callback chấp nhận cả khoá cũ lẫn khoá mới, rồi mới đổi khoá trên máy chủ O3O, sau cùng bỏ khoá cũ ở bộ nhận. Cách này không làm rơi callback nào.