员工使用其JS Staff账户登录你的应用。你的应用永远无法获取其密码。
{
"user_id": 3,
"nama": "Sela",
"email": "sela@contoh.com",
"peran": "staff",
"tenant_id": 1,
"tenant_nama": "Jidanshoppu",
"tenant_slug": "jidanshoppu"
}
仅此而已。peran(角色)为 staff、supervisor 或 owner。你的应用无法读取任何人的密码、工资、考勤、预支或银行账户。
向 JS Staff 平台管理员申请(他们在 /super/apps.php 中注册):
| 值 | 示例 |
|---|---|
client_id | app_a1b2c3d4e5f6a7b8 |
client_secret | sec_1a2b3c… ——只显示一次 |
注册时你必须提供你的 redirect URI。规则很严格:
https://<?php // sso-config.php — JANGAN di-commit. Isi dari env var. const SSO_BASE = 'https://staff.jidanshoppu.com'; const SSO_CLIENT_ID = 'app_xxxxxxxxxxxxxxxx'; const SSO_SECRET = 'sec_xxxxxxxxxxxxxxxx'; const SSO_REDIRECT = 'https://order.jidanshoppu.com/sso-callback.php';
<?php
// sso-mulai.php
require 'sso-config.php';
session_start();
/*
* `state` WAJIB. Acak, disimpan di sesi, dicocokkan lagi di callback.
* Tanpa ini, penyerang bisa mengirim link callback berisi kode MILIK DIA ke korban —
* korban jadi login sebagai akun penyerang, lalu mengetik data ke akun itu tanpa sadar.
*/
$_SESSION['sso_state'] = bin2hex(random_bytes(16));
$url = SSO_BASE . '/sso/authorize.php?' . http_build_query([
'client_id' => SSO_CLIENT_ID,
'redirect_uri' => SSO_REDIRECT,
'state' => $_SESSION['sso_state'],
]);
header('Location: ' . $url);
exit;
一个按钮就够了: <a href="/sso-mulai.php">Masuk pakai JS Staff</a>
<?php
// sso-callback.php
require 'sso-config.php';
session_start();
// --- Orangnya menolak di layar persetujuan ---
if (isset($_GET['error'])) {
exit('Login dibatalkan.');
}
// --- Cocokkan state DULU, sebelum menyentuh code ---
$state = (string)($_GET['state'] ?? '');
if ($state === ''
|| !isset($_SESSION['sso_state'])
|| !hash_equals($_SESSION['sso_state'], $state)) {
// hash_equals, bukan ===: perbandingan biasa berhenti di huruf pertama yang beda,
// dan selisih waktunya bisa dipakai menebak nilainya.
exit('State tidak cocok. Ulangi dari awal.');
}
unset($_SESSION['sso_state']); // sekali pakai
$code = (string)($_GET['code'] ?? '');
if ($code === '') {
exit('Kode tidak ada.');
}
// --- Tukar kode jadi identitas (SERVER ke server) ---
$ch = curl_init(SSO_BASE . '/sso/token.php');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_POSTFIELDS => http_build_query([
'client_id' => SSO_CLIENT_ID,
'client_secret' => SSO_SECRET,
'code' => $code,
'redirect_uri' => SSO_REDIRECT, // harus sama dengan langkah 3
]),
]);
$body = curl_exec($ch);
$kode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($kode !== 200) {
// Jangan tampilkan $body apa adanya ke pengguna — itu pesan untuk kamu, bukan dia.
error_log('SSO gagal: ' . $kode . ' ' . $body);
exit('Login gagal. Coba lagi.');
}
$data = json_decode((string)$body, true);
if (!is_array($data) || empty($data['ok'])) {
exit('Login gagal. Coba lagi.');
}
$u = $data['user'];
// --- Mulai dari sini urusan kamu ---
session_regenerate_id(true); // cegah session fixation
$_SESSION['jss_user_id'] = $u['user_id'];
$_SESSION['jss_tenant_id'] = $u['tenant_id'];
$_SESSION['nama'] = $u['nama'];
$_SESSION['peran'] = $u['peran'];
header('Location: /app.php');
exit;
client_secret 经过浏览器,等同于把这个密钥分享给所有人。 Referer 请求头中。 user_id 是全局唯一的,tenant_id 标识所属公司。两者都是稳定的——即使名称改变,它们也永远不会变。
jss_user_id。ALTER TABLE staff ADD COLUMN jss_user_id INTEGER NULL; ALTER TABLE staff ADD COLUMN jss_tenant_id INTEGER NULL; CREATE UNIQUE INDEX uq_staff_jss ON staff (jss_user_id);
首次流程:
jss_user_id = :user_id 对应的账户 → 找到了?登录。tenant_slug 与你这边的店铺匹配,创建员工账户,并保存其 jss_user_id。tenant_slug 无法识别?拒绝。不要悄悄地根据 SSO 数据创建新店铺——任何拥有 JS Staff 账户的人都能触发这种操作。state:随机生成,存于会话中,用 hash_equals 比对,仅用一次。redirect_uri 在第 3 步和第 4 步中必须完全一致。| 现象 | 通常原因 |
|---|---|
| 出现"返回地址不匹配"页面 | redirect_uri 与注册时的不同。检查末尾斜杠、http 与 https、查询字符串。 |
| 出现"未知应用"页面 | client_id 错误,或该应用已被平台管理员撤销。 |
/sso/token.php 返回 401 |
code 已过期(超过 60 秒)、已被使用、client_secret 错误,或 redirect_uri 与第 3 步不一致。 |
返回时带有 ?error=access_denied |
员工点击了拒绝。这不是缺陷。 |
401 有意对所有原因使用相同的信息——区分"code 错误"和"code 已被使用"会给攻击者一种探测哪些 code 曾经存在过的手段,而对集成正确的人也没有任何帮助。请逐一检查上面这四种情况。