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 cho | Cách gửi | Khoá phía máy chủ O3O | Ghi chú |
|---|---|---|---|
DocBuilder /v1/* | Authorization: Bearer <khoá API> | O3O_DOCBUILDER_API_KEYS | Khai được nhiều khoá. GET /v1/status không cần xác thực. |
DocBuilder /v1/* | Authorization: Bearer <JWT> | O3O_DOCBUILDER_JWT_SECRET | Tuỳ chọn; chỉ bật khi biến này có giá trị. |
Tạo phiên nhúng POST /o3o/embed/session | Trường token trong config | O3O_EMBED_JWT_SECRET | JWT 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úng | Authorization: Bearer <access_token> | Gate tự sinh khi tạo phiên | api.js tự gửi; sai thì 401 invalid_session_token. |
| Callback O3O gửi tới bạn | Header X-O3O-Signature (HMAC SHA-256) | O3O_EMBED_CALLBACK_SECRET, O3O_DOCBUILDER_CALLBACK_SECRET | Chiề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ằngo3o_. - Phần tử chứa
CHANGE_MEbị bỏ qua. Danh sách rỗng thì mọi endpoint cần xác thực trả401 unauthorizedvớidetail.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.
# 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/v1/limitsCách nhanh nhất để thử khoá: trả hạn mức hiệu lực của gói nếu khoá đúng.
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()}");detail.reason là missing, 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ộcGiâ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ọnNhãn ghi vào nhật ký, ví dụ tên ứng dụng gọi.iatnumbertuỳ chọnThời điểm ký. Các thư viện JWT thường tự thêm.
# 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 exp và iat. 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ộcTài liệu cần mở:url,title,fileType,key.editorobjectbắt buộcmode,lang,user,callbackUrl.uiobjecttuỳ chọnTuỳ chọn giao diện mà v1 cài đặt.expnumberbắt buộcGiâ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ọnNên có.
| Cấu hình máy chủ | Có token hợp lệ | Không có token | Token sai hoặc hết hạn |
|---|---|---|---|
Có O3O_EMBED_JWT_SECRET | nhận | 401 token_required | 401 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ần | nhận | bỏ qua token, dùng config trần |
Không có khoá, O3O_EMBED_ALLOW_UNSIGNED=0 | 401 embed_auth_not_configured | 401 embed_auth_not_configured | 401 embed_auth_not_configured |
/o3o/embed/sessionapi.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.
// 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.
// 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 tin | Thời hạn | Hết hạn thì |
|---|---|---|
| JWT gọi DocBuilder | exp bắt buộc, còn lại tối đa 24 giờ | 401 token_expired |
| JWT config nhúng | exp 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úng | O3O_EMBED_SESSION_TTL_MINUTES, mặc định 720 phút | Phiê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úng | O3O_EMBED_RETAIN_HOURS, mặc định 24 giờ | 410 link_expired |
| Tệp kết quả của DocBuilder | result_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.
Khoá API của DocBuilder: không gián đoạn
Thêm khoá mới vàoO3O_DOCBUILDER_API_KEYSvà GIỮ khoá cũ, nạp lạio3o-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.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à ở.envcù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ằngaccess_tokencủa nó.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.