Auth.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  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. * 会员注册
  148. * @param string $username
  149. * @param string $password
  150. * @param string $mobile
  151. * @param string $email
  152. * @param int $group
  153. * @param array $extend
  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. 'mobile' => 'mobile|unique:user',
  160. 'email' => 'email|unique:user',
  161. 'username' => 'regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
  162. '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('Registration parameter error');
  172. return false;
  173. }
  174. $ip = request()->ip();
  175. $time = time();
  176. $salt = Random::build('alnum', 16);
  177. $data = [
  178. 'password' => encrypt_password($password, $salt),
  179. 'group_id' => $group,
  180. 'nickname' => preg_match("/^1[3-9]\d{9}$/", $username) ? substr_replace($username, '****', 3, 4) : $username,
  181. 'join_ip' => $ip,
  182. 'join_time' => $time,
  183. 'last_login_ip' => $ip,
  184. 'last_login_time' => $time,
  185. 'salt' => $salt,
  186. 'status' => 'enable',
  187. ];
  188. $data = array_merge($params, $data);
  189. $data = array_merge($data, $extend);
  190. Db::startTrans();
  191. try {
  192. $this->model = User::create($data);
  193. $this->token = Random::uuid();
  194. Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
  195. Db::commit();
  196. Event::trigger('userRegisterSuccess', $this->model);
  197. } catch (Throwable $e) {
  198. $this->setError($e->getMessage());
  199. Db::rollback();
  200. return false;
  201. }
  202. return true;
  203. }
  204. /**
  205. * 会员登录
  206. * @param string $username 用户名
  207. * @param string $password 密码
  208. * @param bool $keep 是否保持登录
  209. * @return bool
  210. * @throws Throwable
  211. */
  212. public function login(string $username, string $password, bool $keep): bool
  213. {
  214. // 判断账户类型
  215. $accountType = false;
  216. $validate = Validate::rule([
  217. 'mobile' => 'mobile',
  218. 'email' => 'email',
  219. 'username' => 'regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$',
  220. ]);
  221. if ($validate->check(['mobile' => $username])) $accountType = 'mobile';
  222. if ($validate->check(['email' => $username])) $accountType = 'email';
  223. if ($validate->check(['username' => $username])) $accountType = 'username';
  224. if (!$accountType) {
  225. $this->setError('Account not exist');
  226. return false;
  227. }
  228. $this->model = User::where($accountType, $username)->find();
  229. if (!$this->model) {
  230. $this->setError('Account not exist');
  231. return false;
  232. }
  233. if ($this->model->status == 'disable') {
  234. $this->setError('Account disabled');
  235. return false;
  236. }
  237. $userLoginRetry = Config::get('buildadmin.user_login_retry');
  238. if ($userLoginRetry && $this->model->login_failure >= $userLoginRetry && time() - $this->model->last_login_time < 86400) {
  239. $this->setError('Please try again after 1 day');
  240. return false;
  241. }
  242. if ($this->model->password != encrypt_password($password, $this->model->salt)) {
  243. $this->loginFailed();
  244. $this->setError('Password is incorrect');
  245. return false;
  246. }
  247. if (Config::get('buildadmin.user_sso')) {
  248. Token::clear(self::TOKEN_TYPE, $this->model->id);
  249. Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
  250. }
  251. if ($keep) {
  252. $this->setRefreshToken($this->refreshTokenKeepTime);
  253. }
  254. $this->loginSuccessful();
  255. return true;
  256. }
  257. /**
  258. * 直接登录会员账号
  259. * @param int $userId 用户ID
  260. * @return bool
  261. * @throws Throwable
  262. */
  263. public function direct(int $userId): bool
  264. {
  265. $this->model = User::find($userId);
  266. if (!$this->model) return false;
  267. if (Config::get('buildadmin.user_sso')) {
  268. Token::clear(self::TOKEN_TYPE, $this->model->id);
  269. Token::clear(self::TOKEN_TYPE . '-refresh', $this->model->id);
  270. }
  271. return $this->loginSuccessful();
  272. }
  273. /**
  274. * 检查旧密码是否正确
  275. * @param $password
  276. * @return bool
  277. */
  278. public function checkPassword($password): bool
  279. {
  280. if ($this->model->password != encrypt_password($password, $this->model->salt)) {
  281. return false;
  282. } else {
  283. return true;
  284. }
  285. }
  286. /**
  287. * 登录成功
  288. * @return bool
  289. */
  290. public function loginSuccessful(): bool
  291. {
  292. if (!$this->model) {
  293. return false;
  294. }
  295. $this->model->startTrans();
  296. try {
  297. $this->model->login_failure = 0;
  298. $this->model->last_login_time = time();
  299. $this->model->last_login_ip = request()->ip();
  300. $this->model->save();
  301. $this->loginEd = true;
  302. if (!$this->token) {
  303. $this->token = Random::uuid();
  304. Token::set($this->token, self::TOKEN_TYPE, $this->model->id, $this->keepTime);
  305. }
  306. $this->model->commit();
  307. } catch (Throwable $e) {
  308. $this->model->rollback();
  309. $this->setError($e->getMessage());
  310. return false;
  311. }
  312. return true;
  313. }
  314. /**
  315. * 登录失败
  316. * @return bool
  317. */
  318. public function loginFailed(): bool
  319. {
  320. if (!$this->model) return false;
  321. $this->model->startTrans();
  322. try {
  323. $this->model->login_failure++;
  324. $this->model->last_login_time = time();
  325. $this->model->last_login_ip = request()->ip();
  326. $this->model->save();
  327. $this->model->commit();
  328. } catch (Throwable $e) {
  329. $this->model->rollback();
  330. $this->setError($e->getMessage());
  331. return false;
  332. }
  333. return $this->reset();
  334. }
  335. /**
  336. * 退出登录
  337. * @return bool
  338. */
  339. public function logout(): bool
  340. {
  341. if (!$this->loginEd) {
  342. $this->setError('You are not logged in');
  343. return false;
  344. }
  345. return $this->reset();
  346. }
  347. /**
  348. * 是否登录
  349. * @return bool
  350. */
  351. public function isLogin(): bool
  352. {
  353. return $this->loginEd;
  354. }
  355. /**
  356. * 获取会员模型
  357. * @return User
  358. */
  359. public function getUser(): User
  360. {
  361. return $this->model;
  362. }
  363. /**
  364. * 获取会员Token
  365. * @return string
  366. */
  367. public function getToken(): string
  368. {
  369. return $this->token;
  370. }
  371. /**
  372. * 设置刷新Token
  373. * @param int $keepTime
  374. * @return void
  375. */
  376. public function setRefreshToken(int $keepTime = 0): void
  377. {
  378. $this->refreshToken = Random::uuid();
  379. Token::set($this->refreshToken, self::TOKEN_TYPE . '-refresh', $this->model->id, $keepTime);
  380. }
  381. /**
  382. * 获取会员刷新Token
  383. * @return string
  384. */
  385. public function getRefreshToken(): string
  386. {
  387. return $this->refreshToken;
  388. }
  389. /**
  390. * 获取会员信息 - 只输出允许输出的字段
  391. * @return array
  392. */
  393. public function getUserInfo(): array
  394. {
  395. if (!$this->model) return [];
  396. $info = $this->model->toArray();
  397. $info = array_intersect_key($info, array_flip($this->getAllowFields()));
  398. $info['token'] = $this->getToken();
  399. $info['refresh_token'] = $this->getRefreshToken();
  400. return $info;
  401. }
  402. /**
  403. * 获取允许输出字段
  404. * @return array
  405. */
  406. public function getAllowFields(): array
  407. {
  408. return $this->allowFields;
  409. }
  410. /**
  411. * 设置允许输出字段
  412. * @param $fields
  413. * @return void
  414. */
  415. public function setAllowFields($fields): void
  416. {
  417. $this->allowFields = $fields;
  418. }
  419. /**
  420. * 设置Token有效期
  421. * @param int $keepTime
  422. * @return void
  423. */
  424. public function setKeepTime(int $keepTime = 0): void
  425. {
  426. $this->keepTime = $keepTime;
  427. }
  428. public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
  429. {
  430. return parent::check($name, $uid ?: $this->id, $relation, $mode);
  431. }
  432. public function getRuleList(int $uid = 0): array
  433. {
  434. return parent::getRuleList($uid ?: $this->id);
  435. }
  436. public function getRuleIds(int $uid = 0): array
  437. {
  438. return parent::getRuleIds($uid ?: $this->id);
  439. }
  440. public function getMenus(int $uid = 0): array
  441. {
  442. return parent::getMenus($uid ?: $this->id);
  443. }
  444. /**
  445. * 是否是拥有所有权限的会员
  446. * @return bool
  447. * @throws Throwable
  448. */
  449. public function isSuperUser(): bool
  450. {
  451. return in_array('*', $this->getRuleIds());
  452. }
  453. /**
  454. * 设置错误消息
  455. * @param string $error
  456. * @return Auth
  457. */
  458. public function setError(string $error): Auth
  459. {
  460. $this->error = $error;
  461. return $this;
  462. }
  463. /**
  464. * 获取错误消息
  465. * @return string
  466. */
  467. public function getError(): string
  468. {
  469. return $this->error ? __($this->error) : '';
  470. }
  471. /**
  472. * 属性重置(注销、登录失败、重新初始化等将单例数据销毁)
  473. */
  474. protected function reset(bool $deleteToken = true): bool
  475. {
  476. if ($deleteToken && $this->token) {
  477. Token::delete($this->token);
  478. }
  479. $this->token = '';
  480. $this->loginEd = false;
  481. $this->model = null;
  482. $this->refreshToken = '';
  483. $this->setError('');
  484. $this->setKeepTime((int)Config::get('buildadmin.user_token_keep_time'));
  485. return true;
  486. }
  487. }