Auth.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. <?php
  2. namespace app\admin\library;
  3. use Throwable;
  4. use ba\Random;
  5. use think\facade\Db;
  6. use think\facade\Config;
  7. use app\admin\model\Admin;
  8. use app\common\facade\Token;
  9. use app\admin\model\AdminGroup;
  10. /**
  11. * 管理员权限类
  12. * @property int $id 管理员ID
  13. * @property string $username 管理员用户名
  14. * @property string $nickname 管理员昵称
  15. * @property string $email 管理员邮箱
  16. * @property string $mobile 管理员手机号
  17. */
  18. class Auth extends \ba\Auth
  19. {
  20. /**
  21. * 需要登录时/无需登录时的响应状态代码
  22. */
  23. public const LOGIN_RESPONSE_CODE = 303;
  24. /**
  25. * 需要登录标记 - 前台应清理 token、记录当前路由 path、跳转到登录页
  26. */
  27. public const NEED_LOGIN = 'need login';
  28. /**
  29. * 已经登录标记 - 前台应跳转到基础路由
  30. */
  31. public const LOGGED_IN = 'logged in';
  32. /**
  33. * token 入库 type
  34. */
  35. public const TOKEN_TYPE = 'admin';
  36. /**
  37. * 是否登录
  38. * @var bool
  39. */
  40. protected bool $loginEd = false;
  41. /**
  42. * 错误消息
  43. * @var string
  44. */
  45. protected string $error = '';
  46. /**
  47. * Model实例
  48. * @var ?Admin
  49. */
  50. protected ?Admin $model = null;
  51. /**
  52. * 令牌
  53. * @var string
  54. */
  55. protected string $token = '';
  56. /**
  57. * 刷新令牌
  58. * @var string
  59. */
  60. protected string $refreshToken = '';
  61. /**
  62. * 令牌默认有效期
  63. * 可在 config/buildadmin.php 内修改默认值
  64. * @var int
  65. */
  66. protected int $keepTime = 86400;
  67. /**
  68. * 刷新令牌有效期
  69. * @var int
  70. */
  71. protected int $refreshTokenKeepTime = 2592000;
  72. /**
  73. * 允许输出的字段
  74. * @var array
  75. */
  76. protected array $allowFields = ['id', 'username', 'nickname', 'avatar', 'last_login_time'];
  77. public function __construct(array $config = [])
  78. {
  79. parent::__construct($config);
  80. $this->setKeepTime((int)Config::get('buildadmin.admin_token_keep_time'));
  81. }
  82. /**
  83. * 魔术方法-管理员信息字段
  84. * @param $name
  85. * @return mixed 字段信息
  86. */
  87. public function __get($name): mixed
  88. {
  89. return $this->model?->$name;
  90. }
  91. /**
  92. * 初始化
  93. * @access public
  94. * @param array $options 传递到 /ba/Auth 的配置信息
  95. * @return Auth
  96. */
  97. public static function instance(array $options = []): Auth
  98. {
  99. $request = request();
  100. if (!isset($request->adminAuth)) {
  101. $request->adminAuth = new static($options);
  102. }
  103. return $request->adminAuth;
  104. }
  105. /**
  106. * 根据Token初始化管理员登录态
  107. * @param string $token
  108. * @return bool
  109. * @throws Throwable
  110. */
  111. public function init(string $token): bool
  112. {
  113. $tokenData = Token::get($token);
  114. if ($tokenData) {
  115. /**
  116. * 过期检查,过期则抛出 @see TokenExpirationException
  117. */
  118. Token::tokenExpirationCheck($tokenData);
  119. $userId = intval($tokenData['user_id']);
  120. if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) {
  121. $this->model = Admin::where('id', $userId)->find();
  122. if (!$this->model) {
  123. $this->setError('Account not exist');
  124. return false;
  125. }
  126. if ($this->model['status'] != 'enable') {
  127. $this->setError('Account disabled');
  128. return false;
  129. }
  130. $this->token = $token;
  131. $this->loginSuccessful();
  132. return true;
  133. }
  134. }
  135. $this->setError('Token login failed');
  136. $this->reset();
  137. return false;
  138. }
  139. /**
  140. * 管理员登录
  141. * @param string $username 用户名
  142. * @param string $password 密码
  143. * @param bool $keep 是否保持登录
  144. * @return bool
  145. * @throws Throwable
  146. */
  147. public function login(string $username, string $password, bool $keep = false): bool
  148. {
  149. $this->model = Admin::where('username', $username)->find();
  150. if (!$this->model) {
  151. $this->setError('Username is incorrect');
  152. return false;
  153. }
  154. if ($this->model->status == 'disable') {
  155. $this->setError('Account disabled');
  156. return false;
  157. }
  158. // 登录失败重试检查
  159. $lastLoginTime = $this->model->getData('last_login_time');
  160. $adminLoginRetry = Config::get('buildadmin.admin_login_retry');
  161. if ($adminLoginRetry && $lastLoginTime) {
  162. // 重置失败次数
  163. if ($this->model->login_failure > 0 && time() - $lastLoginTime >= 86400) {
  164. $this->model->login_failure = 0;
  165. $this->model->save();
  166. // 重获模型实例,避免单实例多次更新
  167. $this->model = Admin::where('username', $username)->find();
  168. }
  169. if ($this->model->login_failure >= $adminLoginRetry) {
  170. $this->setError('Please try again after 1 day');
  171. return false;
  172. }
  173. }
  174. // 密码检查
  175. if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt])) {
  176. $this->loginFailed();
  177. $this->setError('Password is incorrect');
  178. return false;
  179. }
  180. // 清理 token
  181. if (Config::get('buildadmin.admin_sso')) {
  182. Token::clear(self::TOKEN_TYPE, $this->model->id);
  183. Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
  184. }
  185. if ($keep) {
  186. $this->setRefreshToken($this->refreshTokenKeepTime);
  187. }
  188. $this->loginSuccessful();
  189. return true;
  190. }
  191. /**
  192. * 设置刷新Token
  193. * @param int $keepTime
  194. */
  195. public function setRefreshToken(int $keepTime = 0): void
  196. {
  197. $this->refreshToken = Random::uuid();
  198. Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
  199. }
  200. /**
  201. * 管理员登录成功
  202. * @return bool
  203. */
  204. public function loginSuccessful(): bool
  205. {
  206. if (!$this->model) return false;
  207. $this->model->startTrans();
  208. try {
  209. $this->model->login_failure = 0;
  210. $this->model->last_login_time = time();
  211. $this->model->last_login_ip = request()->ip();
  212. $this->model->save();
  213. $this->loginEd = true;
  214. if (!$this->token) {
  215. $this->token = Random::uuid();
  216. Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
  217. }
  218. $this->model->commit();
  219. } catch (Throwable $e) {
  220. $this->model->rollback();
  221. $this->setError($e->getMessage());
  222. return false;
  223. }
  224. return true;
  225. }
  226. /**
  227. * 管理员登录失败
  228. * @return bool
  229. */
  230. public function loginFailed(): bool
  231. {
  232. if (!$this->model) return false;
  233. $this->model->startTrans();
  234. try {
  235. $this->model->login_failure++;
  236. $this->model->last_login_time = time();
  237. $this->model->last_login_ip = request()->ip();
  238. $this->model->save();
  239. $this->model->commit();
  240. } catch (Throwable $e) {
  241. $this->model->rollback();
  242. $this->setError($e->getMessage());
  243. return false;
  244. }
  245. return $this->reset();
  246. }
  247. /**
  248. * 退出登录
  249. * @return bool
  250. */
  251. public function logout(): bool
  252. {
  253. if (!$this->loginEd) {
  254. $this->setError('You are not logged in');
  255. return false;
  256. }
  257. return $this->reset();
  258. }
  259. /**
  260. * 是否登录
  261. * @return bool
  262. */
  263. public function isLogin(): bool
  264. {
  265. return $this->loginEd;
  266. }
  267. /**
  268. * 获取管理员模型
  269. * @return Admin
  270. */
  271. public function getAdmin(): Admin
  272. {
  273. return $this->model;
  274. }
  275. /**
  276. * 获取管理员Token
  277. * @return string
  278. */
  279. public function getToken(): string
  280. {
  281. return $this->token;
  282. }
  283. /**
  284. * 获取管理员刷新Token
  285. * @return string
  286. */
  287. public function getRefreshToken(): string
  288. {
  289. return $this->refreshToken;
  290. }
  291. /**
  292. * 获取管理员信息 - 只输出允许输出的字段
  293. * @return array
  294. */
  295. public function getInfo(): array
  296. {
  297. if (!$this->model) return [];
  298. $info = $this->model->toArray();
  299. $info = array_intersect_key($info, array_flip($this->getAllowFields()));
  300. $info['token'] = $this->getToken();
  301. $info['refresh_token'] = $this->getRefreshToken();
  302. return $info;
  303. }
  304. /**
  305. * 获取允许输出字段
  306. * @return array
  307. */
  308. public function getAllowFields(): array
  309. {
  310. return $this->allowFields;
  311. }
  312. /**
  313. * 设置允许输出字段
  314. * @param $fields
  315. * @return void
  316. */
  317. public function setAllowFields($fields): void
  318. {
  319. $this->allowFields = $fields;
  320. }
  321. /**
  322. * 设置Token有效期
  323. * @param int $keepTime
  324. * @return void
  325. */
  326. public function setKeepTime(int $keepTime = 0): void
  327. {
  328. $this->keepTime = $keepTime;
  329. }
  330. public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
  331. {
  332. return parent::check($name, $uid ?: $this->id, $relation, $mode);
  333. }
  334. public function getGroups(int $uid = 0): array
  335. {
  336. return parent::getGroups($uid ?: $this->id);
  337. }
  338. public function getRuleList(int $uid = 0): array
  339. {
  340. return parent::getRuleList($uid ?: $this->id);
  341. }
  342. public function getRuleIds(int $uid = 0): array
  343. {
  344. return parent::getRuleIds($uid ?: $this->id);
  345. }
  346. public function getMenus(int $uid = 0): array
  347. {
  348. return parent::getMenus($uid ?: $this->id);
  349. }
  350. /**
  351. * 是否是超级管理员
  352. * @throws Throwable
  353. */
  354. public function isSuperAdmin(): bool
  355. {
  356. return in_array('*', $this->getRuleIds());
  357. }
  358. /**
  359. * 获取管理员所在分组的所有子级分组
  360. * @return array
  361. * @throws Throwable
  362. */
  363. public function getAdminChildGroups(): array
  364. {
  365. $groupIds = Db::name('admin_group_access')
  366. ->where('uid', $this->id)
  367. ->select();
  368. $children = [];
  369. foreach ($groupIds as $group) {
  370. $this->getGroupChildGroups($group['group_id'], $children);
  371. }
  372. return array_unique($children);
  373. }
  374. /**
  375. * 获取一个分组下的子分组
  376. * @param int $groupId 分组ID
  377. * @param array $children 存放子分组的变量
  378. * @return void
  379. * @throws Throwable
  380. */
  381. public function getGroupChildGroups(int $groupId, array &$children): void
  382. {
  383. $childrenTemp = AdminGroup::where('pid', $groupId)
  384. ->where('status', 1)
  385. ->select();
  386. foreach ($childrenTemp as $item) {
  387. $children[] = $item['id'];
  388. $this->getGroupChildGroups($item['id'], $children);
  389. }
  390. }
  391. /**
  392. * 获取分组内的管理员
  393. * @param array $groups
  394. * @return array 管理员数组
  395. */
  396. public function getGroupAdmins(array $groups): array
  397. {
  398. return Db::name('admin_group_access')
  399. ->where('group_id', 'in', $groups)
  400. ->column('uid');
  401. }
  402. /**
  403. * 获取拥有 `所有权限` 的分组
  404. * @param string $dataLimit 数据权限
  405. * @param array $groupQueryWhere 分组查询条件(默认查询启用的分组:[['status','=',1]])
  406. * @return array 分组数组
  407. * @throws Throwable
  408. */
  409. public function getAllAuthGroups(string $dataLimit, array $groupQueryWhere = [['status', '=', 1]]): array
  410. {
  411. // 当前管理员拥有的权限
  412. $rules = $this->getRuleIds();
  413. $allAuthGroups = [];
  414. $groups = AdminGroup::where($groupQueryWhere)->select();
  415. foreach ($groups as $group) {
  416. if ($group['rules'] == '*') {
  417. continue;
  418. }
  419. $groupRules = explode(',', $group['rules']);
  420. // 及时break, array_diff 等没有 in_array 快
  421. $all = true;
  422. foreach ($groupRules as $groupRule) {
  423. if (!in_array($groupRule, $rules)) {
  424. $all = false;
  425. break;
  426. }
  427. }
  428. if ($all) {
  429. if ($dataLimit == 'allAuth' || ($dataLimit == 'allAuthAndOthers' && array_diff($rules, $groupRules))) {
  430. $allAuthGroups[] = $group['id'];
  431. }
  432. }
  433. }
  434. return $allAuthGroups;
  435. }
  436. /**
  437. * 设置错误消息
  438. * @param $error
  439. * @return Auth
  440. */
  441. public function setError($error): Auth
  442. {
  443. $this->error = $error;
  444. return $this;
  445. }
  446. /**
  447. * 获取错误消息
  448. * @return string
  449. */
  450. public function getError(): string
  451. {
  452. return $this->error ? __($this->error) : '';
  453. }
  454. /**
  455. * 属性重置(注销、登录失败、重新初始化等将单例数据销毁)
  456. */
  457. protected function reset(bool $deleteToken = true): bool
  458. {
  459. if ($deleteToken && $this->token) {
  460. Token::delete($this->token);
  461. }
  462. $this->token = '';
  463. $this->loginEd = false;
  464. $this->model = null;
  465. $this->refreshToken = '';
  466. $this->setError('');
  467. $this->setKeepTime((int)Config::get('buildadmin.admin_token_keep_time'));
  468. return true;
  469. }
  470. }