Compare commits
10
Commits
9ef6eb8d67
...
ziong-8-28
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1f7287331 | ||
|
|
9d91ff90ae | ||
|
|
1b8e166b33 | ||
|
|
0cea43b027 | ||
|
|
d0e2422155 | ||
|
|
9206d5ea62 | ||
|
|
bd0a3d3b96 | ||
|
|
a5f4c4472d | ||
|
|
5e22d423d4 | ||
|
|
7f1ed49cc8 |
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 企业微信客服桌面端管理员认证接口
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\adminapi\controller;
|
||||
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\common\enum\AdminTerminalEnum;
|
||||
use app\common\enum\YesNoEnum;
|
||||
use app\common\model\auth\Admin;
|
||||
use app\common\model\auth\AdminRole;
|
||||
|
||||
/**
|
||||
* 用最小身份信息把 ZYT 管理员账号映射到客服软件租户。
|
||||
*
|
||||
* session 标为免登录是为了跳过管理后台的菜单权限和登录 IP 限制;它并非匿名
|
||||
* 接口,方法内部仍会严格校验 token 以及 token 的桌面终端类型。客服后台从另一
|
||||
* 台服务器验证 token 时,请求 IP 本来就不会等于桌面用户登录时的 IP。
|
||||
*/
|
||||
class DesktopController extends BaseAdminController
|
||||
{
|
||||
public array $notNeedLogin = ['session'];
|
||||
|
||||
public function session()
|
||||
{
|
||||
$identity = (array)($this->request->adminInfo ?? []);
|
||||
if (empty($identity) || empty($identity['admin_id'])) {
|
||||
return $this->fail('登录已失效,请重新登录');
|
||||
}
|
||||
if ((int)($identity['terminal'] ?? 0) !== AdminTerminalEnum::WECOM_RPA) {
|
||||
return $this->fail('当前登录令牌不属于企业微信客服桌面端');
|
||||
}
|
||||
|
||||
$admin = Admin::where(['id' => (int)$identity['admin_id']])
|
||||
->field('id,account,name,avatar,disable,root')
|
||||
->findOrEmpty();
|
||||
if ($admin->isEmpty() || (int)$admin['disable'] === YesNoEnum::YES) {
|
||||
return $this->fail('管理员不存在或已禁用');
|
||||
}
|
||||
|
||||
// 身份和权限在专用接口一次返回,客服后台无需跨 IP 调用网页端 mySelf。
|
||||
$roleIds = array_values(array_unique(array_map('intval',
|
||||
AdminRole::where('admin_id', (int)$admin['id'])->column('role_id')
|
||||
)));
|
||||
sort($roleIds);
|
||||
$permissions = AuthLogic::getBtnAuthByRoleId([
|
||||
'id' => (int)$admin['id'],
|
||||
'root' => (int)$admin['root'],
|
||||
'role_id' => $roleIds,
|
||||
]);
|
||||
|
||||
$account = trim((string)$admin['account']);
|
||||
$name = trim((string)$admin['name']);
|
||||
return $this->data([
|
||||
// 加类型前缀,避免管理员 id 与普通用户 id 相同时落到同一个客服租户。
|
||||
'user_id' => 'admin:' . (int)$admin['id'],
|
||||
'sn' => $account,
|
||||
'nickname' => $name !== '' ? $name : $account,
|
||||
'mobile' => '',
|
||||
'avatar' => (string)$admin['avatar'],
|
||||
'terminal' => AdminTerminalEnum::WECOM_RPA,
|
||||
'expire_time' => (int)($identity['expire_time'] ?? 0),
|
||||
'status' => 'active',
|
||||
'account_type' => 'admin',
|
||||
'root' => (int)$admin['root'] === YesNoEnum::YES,
|
||||
'role_ids' => $roleIds,
|
||||
'permissions' => array_values($permissions),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,8 @@ use think\facade\Log;
|
||||
class LoginValidate extends BaseValidate
|
||||
{
|
||||
protected $rule = [
|
||||
'terminal' => 'require|in:' . AdminTerminalEnum::PC . ',' . AdminTerminalEnum::MOBILE,
|
||||
'terminal' => 'require|in:' . AdminTerminalEnum::PC . ',' . AdminTerminalEnum::MOBILE
|
||||
. ',' . AdminTerminalEnum::WECOM_RPA,
|
||||
'account' => 'require',
|
||||
'password' => 'require|password',
|
||||
];
|
||||
@@ -143,4 +144,4 @@ class LoginValidate extends BaseValidate
|
||||
return in_array($flag, ['1', 'true', 'yes', 'on'], true);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | 企业微信客服桌面端认证接口
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\api\controller;
|
||||
|
||||
use app\common\enum\user\UserTerminalEnum;
|
||||
use app\common\enum\YesNoEnum;
|
||||
use app\common\model\user\User;
|
||||
|
||||
/**
|
||||
* 桌面端只通过这里确认一个 token 是否属于企业微信客服终端。
|
||||
*
|
||||
* 登录仍复用 LoginController::account;这个接口只返回建立外部账号映射所需的
|
||||
* 最小身份信息,避免客服后台依赖面向普通 PC 页面的完整个人资料结构。
|
||||
*/
|
||||
class DesktopController extends BaseApiController
|
||||
{
|
||||
public function session()
|
||||
{
|
||||
if ((int)($this->userInfo['terminal'] ?? 0) !== UserTerminalEnum::WECOM_RPA) {
|
||||
return $this->fail('当前登录令牌不属于企业微信客服桌面端');
|
||||
}
|
||||
|
||||
$user = User::where(['id' => $this->userId])
|
||||
->field('id,sn,nickname,mobile,avatar,is_disable')
|
||||
->findOrEmpty();
|
||||
if ($user->isEmpty() || (int)$user['is_disable'] === YesNoEnum::YES) {
|
||||
return $this->fail('用户不存在或已禁用');
|
||||
}
|
||||
|
||||
return $this->data([
|
||||
'user_id' => (int)$user['id'],
|
||||
'sn' => (string)$user['sn'],
|
||||
'nickname' => (string)$user['nickname'],
|
||||
'mobile' => (string)$user['mobile'],
|
||||
'avatar' => (string)$user['avatar'],
|
||||
'terminal' => UserTerminalEnum::WECOM_RPA,
|
||||
'expire_time' => (int)($this->userInfo['expire_time'] ?? 0),
|
||||
'status' => 'active',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -35,8 +35,8 @@ class LoginAccountValidate extends BaseValidate
|
||||
|
||||
protected $rule = [
|
||||
'terminal' => 'require|in:' . UserTerminalEnum::WECHAT_MMP . ',' . UserTerminalEnum::WECHAT_OA . ','
|
||||
. UserTerminalEnum::H5 . ',' . UserTerminalEnum::PC . ',' . UserTerminalEnum::IOS .
|
||||
',' . UserTerminalEnum::ANDROID,
|
||||
. UserTerminalEnum::H5 . ',' . UserTerminalEnum::PC . ',' . UserTerminalEnum::IOS
|
||||
. ',' . UserTerminalEnum::ANDROID . ',' . UserTerminalEnum::WECOM_RPA,
|
||||
'scene' => 'require|in:' . LoginEnum::ACCOUNT_PASSWORD . ',' . LoginEnum::MOBILE_CAPTCHA . '|checkConfig',
|
||||
'account' => 'require',
|
||||
];
|
||||
@@ -158,4 +158,4 @@ class LoginAccountValidate extends BaseValidate
|
||||
}
|
||||
return '验证码错误';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +25,6 @@ class AdminTerminalEnum
|
||||
{
|
||||
const PC = 1;
|
||||
const MOBILE = 2;
|
||||
}
|
||||
// 企业微信客服桌面软件。与网页端分开保存会话,桌面登录不会顶掉浏览器登录。
|
||||
const WECOM_RPA = 7;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ class UserTerminalEnum
|
||||
const PC = 4;//电脑PC
|
||||
const IOS = 5;//苹果app
|
||||
const ANDROID = 6;//安卓app
|
||||
const WECOM_RPA = 7;//企业微信客服桌面端
|
||||
|
||||
|
||||
const ALL_TERMINAL = [
|
||||
@@ -37,6 +38,7 @@ class UserTerminalEnum
|
||||
self::PC,
|
||||
self::IOS,
|
||||
self::ANDROID,
|
||||
self::WECOM_RPA,
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -55,10 +57,11 @@ class UserTerminalEnum
|
||||
self::PC => '电脑PC',
|
||||
self::IOS => '苹果APP',
|
||||
self::ANDROID => '安卓APP',
|
||||
self::WECOM_RPA => '企业微信客服桌面端',
|
||||
];
|
||||
if(true === $from){
|
||||
return $desc;
|
||||
}
|
||||
return $desc[$from] ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use app\common\enum\AdminTerminalEnum;
|
||||
|
||||
function adminDesktopExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
fwrite(STDERR, "FAIL: {$message}\n");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
adminDesktopExpect(AdminTerminalEnum::WECOM_RPA === 7, 'desktop admin terminal remains 7');
|
||||
|
||||
$validator = file_get_contents(dirname(__DIR__) . '/app/adminapi/validate/LoginValidate.php');
|
||||
$controller = file_get_contents(dirname(__DIR__) . '/app/adminapi/controller/DesktopController.php');
|
||||
adminDesktopExpect(is_string($validator) && is_string($controller), 'desktop auth sources are readable');
|
||||
adminDesktopExpect(
|
||||
str_contains($validator, 'AdminTerminalEnum::WECOM_RPA'),
|
||||
'admin login accepts the dedicated desktop terminal'
|
||||
);
|
||||
adminDesktopExpect(
|
||||
str_contains($controller, "public array \$notNeedLogin = ['session'];")
|
||||
&& str_contains($controller, "'user_id' => 'admin:'")
|
||||
&& str_contains($controller, "'account_type' => 'admin'"),
|
||||
'desktop session verifies an admin token and namespaces its tenant identity'
|
||||
);
|
||||
|
||||
echo "OK\n";
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\model\auth {
|
||||
// 所有账号/角色数据在内存中,测试不连接业务数据库。
|
||||
class Admin extends \ArrayObject
|
||||
{
|
||||
public static array $row = [];
|
||||
public static function where(array $filter): self
|
||||
{
|
||||
if ($filter !== ['id' => 9]) { throw new \RuntimeException('wrong identity lookup'); }
|
||||
return new self(self::$row);
|
||||
}
|
||||
public function field(string $fields): self { return $this; }
|
||||
public function findOrEmpty(): self { return $this; }
|
||||
public function isEmpty(): bool { return $this->count() === 0; }
|
||||
}
|
||||
class AdminRole
|
||||
{
|
||||
public static function where(string $field, int $id): self
|
||||
{
|
||||
if ($field !== 'admin_id' || $id !== 9) { throw new \RuntimeException('wrong role lookup'); }
|
||||
return new self();
|
||||
}
|
||||
public function column(string $field): array { return ['3', '2', '3']; }
|
||||
}
|
||||
}
|
||||
namespace app\adminapi\logic\auth {
|
||||
class AuthLogic
|
||||
{
|
||||
public static array $permissions = ['tcm.diagnosis/lists'];
|
||||
public static function getBtnAuthByRoleId(array $admin): array
|
||||
{
|
||||
if ($admin['id'] !== 9 || $admin['role_id'] !== [2, 3]) {
|
||||
throw new \RuntimeException('wrong permission identity');
|
||||
}
|
||||
return $admin['root'] ? ['*'] : self::$permissions;
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace {
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
require dirname(__DIR__) . '/vendor/topthink/framework/src/helper.php';
|
||||
use app\adminapi\controller\DesktopController;
|
||||
use app\adminapi\http\middleware\AuthMiddleware;
|
||||
use app\adminapi\logic\auth\AuthLogic;
|
||||
use app\common\model\auth\Admin;
|
||||
|
||||
$app = new think\App();
|
||||
final class DesktopSessionFixture extends DesktopController
|
||||
{
|
||||
public function __construct(think\Request $request) { $this->request = $request; }
|
||||
protected function data($data) { return ['code' => 1, 'data' => $data]; }
|
||||
protected function fail(string $msg = 'fail', array $data = [], int $code = 0, int $show = 1)
|
||||
{
|
||||
return ['code' => $code, 'msg' => $msg];
|
||||
}
|
||||
}
|
||||
function desktopCheck(bool $ok, string $message): void
|
||||
{
|
||||
if (!$ok) { throw new RuntimeException($message); }
|
||||
}
|
||||
$valid = ['admin_id' => 9, 'terminal' => 7, 'expire_time' => time() + 3600,
|
||||
'root' => 0, 'login_ip' => '198.51.100.10'];
|
||||
$invoke = function (array $identity, string $action = 'session') use ($app) {
|
||||
$request = (new think\Request())->withServer(['REMOTE_ADDR' => '203.0.113.20']);
|
||||
$request->setAction($action);
|
||||
$request->setController('Desktop');
|
||||
$request->adminInfo = $identity;
|
||||
$controller = new DesktopSessionFixture($request);
|
||||
$request->controllerObject = $controller;
|
||||
$app->instance('request', $request);
|
||||
return (new AuthMiddleware())->handle($request, static fn() => $controller->session());
|
||||
};
|
||||
Admin::$row = ['id' => 9, 'account' => 'doctor9', 'name' => '医助',
|
||||
'avatar' => '', 'disable' => 0, 'root' => 0];
|
||||
$result = $invoke($valid);
|
||||
desktopCheck($result['code'] === 1, 'valid desktop session works across different IPs');
|
||||
desktopCheck($result['data']['user_id'] === 'admin:9', 'account is namespaced');
|
||||
desktopCheck($result['data']['role_ids'] === [2, 3], 'current roles are normalized');
|
||||
desktopCheck($result['data']['permissions'] === ['tcm.diagnosis/lists'], 'current permissions returned');
|
||||
desktopCheck($result['data']['root'] === false, 'ordinary account stays ordinary');
|
||||
desktopCheck(!isset($result['data']['token']) && !isset($result['data']['login_ip']), 'minimal response');
|
||||
|
||||
AuthLogic::$permissions = [];
|
||||
desktopCheck($invoke($valid)['data']['permissions'] === [], 'empty permissions remain empty');
|
||||
Admin::$row['root'] = 1;
|
||||
desktopCheck($invoke($valid)['data']['permissions'] === ['*'], 'current root privilege returned');
|
||||
desktopCheck($invoke($valid)['data']['root'] === true, 'root comes from current account');
|
||||
Admin::$row['root'] = 0;
|
||||
|
||||
desktopCheck($invoke([])['code'] === 0, 'missing or expired cached identity rejected');
|
||||
desktopCheck($invoke(array_replace($valid, ['terminal' => 1]))['code'] === 0, 'web token rejected');
|
||||
Admin::$row['disable'] = 1;
|
||||
desktopCheck($invoke($valid)['code'] === 0, 'disabled account rejected');
|
||||
Admin::$row = [];
|
||||
desktopCheck($invoke($valid)['code'] === 0, 'deleted account rejected');
|
||||
|
||||
$webResult = $invoke($valid, 'mySelf')->getData();
|
||||
desktopCheck($webResult['code'] === -1 && str_contains($webResult['msg'], 'ip地址'),
|
||||
'ordinary web requests retain IP validation');
|
||||
echo "AdminDesktopSessionBehaviorTest passed\n";
|
||||
}
|
||||
Reference in New Issue
Block a user