Auth.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. <?php
  2. namespace app\common\library;
  3. use Throwable;
  4. use ba\Random;
  5. use think\facade\Db;
  6. use think\facade\Event;
  7. use think\facade\Config;
  8. use app\common\model\User;
  9. use think\facade\Validate;
  10. use app\common\facade\Token;
  11. /**
  12. * 公共权限类(会员权限类)
  13. * @property int $id 会员ID
  14. * @property string $username 会员用户名
  15. * @property string $nickname 会员昵称
  16. * @property string $email 会员邮箱
  17. * @property string $mobile 会员手机号
  18. * @property string $password 密码密文
  19. * @property string $salt 密码盐
  20. */
  21. class Auth extends \ba\Auth
  22. {
  23. /**
  24. * 需要登录时/无需登录时的响应状态代码
  25. */
  26. public const LOGIN_RESPONSE_CODE = 303;
  27. /**
  28. * 需要登录标记 - 前台应清理 token、记录当前路由 path、跳转到登录页
  29. */
  30. public const NEED_LOGIN = 'need login';
  31. /**
  32. * 已经登录标记 - 前台应跳转到基础路由
  33. */
  34. public const LOGGED_IN = 'logged in';
  35. /**
  36. * token 入库 type
  37. */
  38. public const TOKEN_TYPE = 'user';
  39. /**
  40. * 是否登录
  41. * @var bool
  42. */
  43. protected bool $loginEd = false;
  44. /**
  45. * 错误消息
  46. * @var string
  47. */
  48. protected string $error = '';
  49. /**
  50. * Model实例
  51. * @var ?User
  52. */
  53. protected ?User $model = null;
  54. /**
  55. * 令牌
  56. * @var string
  57. */
  58. protected string $token = '';
  59. /**
  60. * 刷新令牌
  61. * @var string
  62. */
  63. protected string $refreshToken = '';
  64. /**
  65. * 令牌默认有效期
  66. * 可在 config/buildadmin.php 内修改默认值
  67. * @var int
  68. */
  69. protected int $keepTime = 86400;
  70. /**
  71. * 刷新令牌有效期
  72. * @var int
  73. */
  74. protected int $refreshTokenKeepTime = 2592000;
  75. /**
  76. * 允许输出的字段
  77. * @var array
  78. */
  79. protected array $allowFields = ['id', 'username', 'nickname', 'email', 'mobile', 'avatar', 'gender', 'birthday', 'money', 'score', 'join_time', 'motto', 'last_login_time', 'last_login_ip'];
  80. public function __construct(array $config = [])
  81. {
  82. parent::__construct(array_merge([
  83. 'auth_group' => 'user_group', // 用户组数据表名
  84. 'auth_group_access' => '', // 用户-用户组关系表(关系字段)
  85. 'auth_rule' => 'user_rule', // 权限规则表
  86. ], $config));
  87. $this->setKeepTime((int)Config::get('buildadmin.user_token_keep_time'));
  88. }
  89. /**
  90. * 魔术方法-会员信息字段
  91. * @param $name
  92. * @return mixed 字段信息
  93. */
  94. public function __get($name): mixed
  95. {
  96. return $this->model?->$name;
  97. }
  98. /**
  99. * 初始化
  100. * @access public
  101. * @param array $options 传递给 /ba/Auth 的参数
  102. * @return Auth
  103. */
  104. public static function instance(array $options = []): Auth
  105. {
  106. $request = request();
  107. if (!isset($request->userAuth)) {
  108. $request->userAuth = new static($options);
  109. }
  110. return $request->userAuth;
  111. }
  112. /**
  113. * 根据Token初始化会员登录态
  114. * @param $token
  115. * @return bool
  116. * @throws Throwable
  117. */
  118. public function init($token): bool
  119. {
  120. $tokenData = Token::get($token);
  121. if ($tokenData) {
  122. /**
  123. * 过期检查,过期则抛出 @see TokenExpirationException
  124. */
  125. Token::tokenExpirationCheck($tokenData);
  126. $userId = intval($tokenData['user_id']);
  127. if ($tokenData['type'] == self::TOKEN_TYPE && $userId > 0) {
  128. $this->model = User::where('id', $userId)->find();
  129. if (!$this->model) {
  130. $this->setError('Account not exist');
  131. return false;
  132. }
  133. if ($this->model->status != 'enable') {
  134. $this->setError('Account disabled');
  135. return false;
  136. }
  137. $this->token = $token;
  138. $this->loginSuccessful();
  139. return true;
  140. }
  141. }
  142. $this->setError('Token login failed');
  143. $this->reset();
  144. return false;
  145. }
  146. /**
  147. * 会员注册,可使用关键词参数方式调用:$auth->register('u18888888888', email: 'test@qq.com')
  148. * @param string $username
  149. * @param string $password
  150. * @param string $mobile
  151. * @param string $email
  152. * @param int $group 会员分组 ID 号
  153. * @param array $extend 扩展数据,如 ['status' => 'disable']
  154. * @return bool
  155. */
  156. public function register(string $username, string $password = '', string $mobile = '', string $email = '', int $group = 1, array $extend = []): bool
  157. {
  158. $validate = Validate::rule([
  159. 'email|' . __('Email') => 'email|unique:user',
  160. 'mobile|' . __('Mobile') => 'mobile|unique:user',
  161. 'username|' . __('Username') => 'require|regex:/^[\s\S]{2,15}$/|unique:user',
  162. 'password|' . __('Password') => 'regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
  163. ]);
  164. $params = [
  165. 'username' => $username,
  166. 'password' => $password,
  167. 'mobile' => $mobile,
  168. 'email' => $email,
  169. ];
  170. if (!$validate->check($params)) {
  171. $this->setError($validate->getError());
  172. return false;
  173. }
  174. // 用户昵称
  175. $nickname = preg_replace_callback('/1[3-9]\d{9}/', function ($matches) {
  176. // 对 username 中出现的所有手机号进行脱敏处理
  177. $mobile = $matches[0];
  178. return substr($mobile, 0, 3) . '****' . substr($mobile, 7);
  179. }, $username);
  180. $ip = request()->ip();
  181. $time = time();
  182. $data = [
  183. 'group_id' => $group,
  184. 'nickname' => $nickname,
  185. 'join_ip' => $ip,
  186. 'join_time' => $time,
  187. 'last_login_ip' => $ip,
  188. 'last_login_time' => $time,
  189. 'status' => 'enable', // 状态:enable=启用,disable=禁用,使用 string 存储可以自定义其他状态
  190. ];
  191. $data = array_merge($params, $data);
  192. $data = array_merge($data, $extend);
  193. Db::startTrans();
  194. try {
  195. $this->model = User::create($data);
  196. $this->token = Random::uuid();
  197. Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
  198. Db::commit();
  199. if ($password) {
  200. $this->model->resetPassword($this->model->id, $password);
  201. }
  202. Event::trigger('userRegisterSuccess', $this->model);
  203. } catch (Throwable $e) {
  204. $this->setError($e->getMessage());
  205. Db::rollback();
  206. return false;
  207. }
  208. return true;
  209. }
  210. /**
  211. * 会员登录
  212. * @param string $username 用户名
  213. * @param string $password 密码
  214. * @param bool $keep 是否保持登录
  215. * @return bool
  216. * @throws Throwable
  217. */
  218. public function login(string $username, string $password, bool $keep): bool
  219. {
  220. // 判断账户类型
  221. $accountType = false;
  222. $validate = Validate::rule([
  223. 'mobile' => 'mobile',
  224. 'email' => 'email',
  225. 'username' => 'regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$',
  226. ]);
  227. if ($validate->check(['mobile' => $username])) $accountType = 'mobile';
  228. if ($validate->check(['email' => $username])) $accountType = 'email';
  229. if ($validate->check(['username' => $username])) $accountType = 'username';
  230. if (!$accountType) {
  231. $this->setError('Account not exist');
  232. return false;
  233. }
  234. $this->model = User::where($accountType, $username)->find();
  235. if (!$this->model) {
  236. $this->setError('Account not exist');
  237. return false;
  238. }
  239. if ($this->model->status == 'disable') {
  240. $this->setError('Account disabled');
  241. return false;
  242. }
  243. // 登录失败重试检查
  244. $userLoginRetry = Config::get('buildadmin.user_login_retry');
  245. if ($userLoginRetry && $this->model->last_login_time) {
  246. // 重置失败次数
  247. if ($this->model->login_failure > 0 && time() - $this->model->last_login_time >= 86400) {
  248. $this->model->login_failure = 0;
  249. $this->model->save();
  250. // 重获模型实例,避免单实例多次更新
  251. $this->model = User::where($accountType, $username)->find();
  252. }
  253. if ($this->model->login_failure >= $userLoginRetry) {
  254. $this->setError('Please try again after 1 day');
  255. return false;
  256. }
  257. }
  258. // 密码检查
  259. if (!verify_password($password, $this->model->password, ['salt' => $this->model->salt])) {
  260. $this->loginFailed();
  261. $this->setError('Password is incorrect');
  262. return false;
  263. }
  264. // 清理 token
  265. if (Config::get('buildadmin.user_sso')) {
  266. Token::clear(self::TOKEN_TYPE, $this->model->id);
  267. Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
  268. }
  269. if ($keep) {
  270. $this->setRefreshToken($this->refreshTokenKeepTime);
  271. }
  272. $this->loginSuccessful();
  273. return true;
  274. }
  275. /**
  276. * 直接登录会员账号
  277. * @param int $userId 用户ID
  278. * @return bool
  279. * @throws Throwable
  280. */
  281. public function direct(int $userId): bool
  282. {
  283. $this->model = User::find($userId);
  284. if (!$this->model) return false;
  285. if (Config::get('buildadmin.user_sso')) {
  286. Token::clear(self::TOKEN_TYPE, $this->model->id);
  287. Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
  288. }
  289. return $this->loginSuccessful();
  290. }
  291. /**
  292. * 检查旧密码是否正确
  293. * @param $password
  294. * @return bool
  295. * @deprecated 请使用 verify_password 公共函数代替
  296. */
  297. public function checkPassword($password): bool
  298. {
  299. return verify_password($password, $this->model->password, ['salt' => $this->model->salt]);
  300. }
  301. /**
  302. * 登录成功
  303. * @return bool
  304. */
  305. public function loginSuccessful(): bool
  306. {
  307. if (!$this->model) {
  308. return false;
  309. }
  310. $this->model->startTrans();
  311. try {
  312. $this->model->login_failure = 0;
  313. $this->model->last_login_time = time();
  314. $this->model->last_login_ip = request()->ip();
  315. $this->model->save();
  316. $this->loginEd = true;
  317. if (!$this->token) {
  318. $this->token = Random::uuid();
  319. Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
  320. }
  321. $this->model->commit();
  322. } catch (Throwable $e) {
  323. $this->model->rollback();
  324. $this->setError($e->getMessage());
  325. return false;
  326. }
  327. return true;
  328. }
  329. /**
  330. * 登录失败
  331. * @return bool
  332. */
  333. public function loginFailed(): bool
  334. {
  335. if (!$this->model) return false;
  336. $this->model->startTrans();
  337. try {
  338. $this->model->login_failure++;
  339. $this->model->last_login_time = time();
  340. $this->model->last_login_ip = request()->ip();
  341. $this->model->save();
  342. $this->model->commit();
  343. } catch (Throwable $e) {
  344. $this->model->rollback();
  345. $this->setError($e->getMessage());
  346. return false;
  347. }
  348. return $this->reset();
  349. }
  350. /**
  351. * 退出登录
  352. * @return bool
  353. */
  354. public function logout(): bool
  355. {
  356. if (!$this->loginEd) {
  357. $this->setError('You are not logged in');
  358. return false;
  359. }
  360. return $this->reset();
  361. }
  362. /**
  363. * 是否登录
  364. * @return bool
  365. */
  366. public function isLogin(): bool
  367. {
  368. return $this->loginEd;
  369. }
  370. /**
  371. * 获取会员模型
  372. * @return User
  373. */
  374. public function getUser(): User
  375. {
  376. return $this->model;
  377. }
  378. /**
  379. * 获取会员Token
  380. * @return string
  381. */
  382. public function getToken(): string
  383. {
  384. return $this->token;
  385. }
  386. /**
  387. * 设置刷新Token
  388. * @param int $keepTime
  389. * @return void
  390. */
  391. public function setRefreshToken(int $keepTime = 0): void
  392. {
  393. $this->refreshToken = Random::uuid();
  394. Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
  395. }
  396. /**
  397. * 获取会员刷新Token
  398. * @return string
  399. */
  400. public function getRefreshToken(): string
  401. {
  402. return $this->refreshToken;
  403. }
  404. /**
  405. * 获取会员信息 - 只输出允许输出的字段
  406. * @return array
  407. */
  408. public function getUserInfo(): array
  409. {
  410. if (!$this->model) return [];
  411. $info = $this->model->toArray();
  412. $info = array_intersect_key($info, array_flip($this->getAllowFields()));
  413. $info['token'] = $this->getToken();
  414. $info['refresh_token'] = $this->getRefreshToken();
  415. return $info;
  416. }
  417. /**
  418. * 获取允许输出字段
  419. * @return array
  420. */
  421. public function getAllowFields(): array
  422. {
  423. return $this->allowFields;
  424. }
  425. /**
  426. * 设置允许输出字段
  427. * @param $fields
  428. * @return void
  429. */
  430. public function setAllowFields($fields): void
  431. {
  432. $this->allowFields = $fields;
  433. }
  434. /**
  435. * 设置Token有效期
  436. * @param int $keepTime
  437. * @return void
  438. */
  439. public function setKeepTime(int $keepTime = 0): void
  440. {
  441. $this->keepTime = $keepTime;
  442. }
  443. public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
  444. {
  445. return parent::check($name, $uid ?: $this->id, $relation, $mode);
  446. }
  447. public function getRuleList(int $uid = 0): array
  448. {
  449. return parent::getRuleList($uid ?: $this->id);
  450. }
  451. public function getRuleIds(int $uid = 0): array
  452. {
  453. return parent::getRuleIds($uid ?: $this->id);
  454. }
  455. public function getMenus(int $uid = 0): array
  456. {
  457. return parent::getMenus($uid ?: $this->id);
  458. }
  459. /**
  460. * 是否是拥有所有权限的会员
  461. * @return bool
  462. * @throws Throwable
  463. */
  464. public function isSuperUser(): bool
  465. {
  466. return in_array('*', $this->getRuleIds());
  467. }
  468. /**
  469. * 设置错误消息
  470. * @param string $error
  471. * @return Auth
  472. */
  473. public function setError(string $error): Auth
  474. {
  475. $this->error = $error;
  476. return $this;
  477. }
  478. /**
  479. * 获取错误消息
  480. * @return string
  481. */
  482. public function getError(): string
  483. {
  484. return $this->error ? __($this->error) : '';
  485. }
  486. /**
  487. * 属性重置(注销、登录失败、重新初始化等将单例数据销毁)
  488. */
  489. protected function reset(bool $deleteToken = true): bool
  490. {
  491. if ($deleteToken && $this->token) {
  492. Token::delete($this->token);
  493. }
  494. $this->token = '';
  495. $this->loginEd = false;
  496. $this->model = null;
  497. $this->refreshToken = '';
  498. $this->setError('');
  499. $this->setKeepTime((int)Config::get('buildadmin.user_token_keep_time'));
  500. return true;
  501. }
  502. }