Auth.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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. * 对象实例
  37. * @var ?Auth
  38. */
  39. protected static ?Auth $instance = null;
  40. /**
  41. * 是否登录
  42. * @var bool
  43. */
  44. protected bool $loginEd = false;
  45. /**
  46. * 错误消息
  47. * @var string
  48. */
  49. protected string $error = '';
  50. /**
  51. * Model实例
  52. * @var ?User
  53. */
  54. protected ?User $model = null;
  55. /**
  56. * 令牌
  57. * @var string
  58. */
  59. protected string $token = '';
  60. /**
  61. * 刷新令牌
  62. * @var string
  63. */
  64. protected string $refreshToken = '';
  65. /**
  66. * 令牌默认有效期
  67. * 可在 config/buildadmin.php 内修改默认值
  68. * @var int
  69. */
  70. protected int $keepTime = 86400;
  71. /**
  72. * 刷新令牌有效期
  73. * @var int
  74. */
  75. protected int $refreshTokenKeepTime = 2592000;
  76. /**
  77. * 允许输出的字段
  78. * @var array
  79. */
  80. protected array $allowFields = ['id', 'username', 'nickname', 'email', 'mobile', 'avatar', 'gender', 'birthday', 'money', 'score', 'join_time', 'motto', 'last_login_time', 'last_login_ip'];
  81. public function __construct(array $config = [])
  82. {
  83. parent::__construct(array_merge([
  84. 'auth_group' => 'user_group', // 用户组数据表名
  85. 'auth_group_access' => '', // 用户-用户组关系表(关系字段)
  86. 'auth_rule' => 'user_rule', // 权限规则表
  87. ], $config));
  88. $this->setKeepTime((int)Config::get('buildadmin.user_token_keep_time'));
  89. }
  90. /**
  91. * 魔术方法-会员信息字段
  92. * @param $name
  93. * @return mixed 字段信息
  94. */
  95. public function __get($name): mixed
  96. {
  97. return $this->model?->$name;
  98. }
  99. /**
  100. * 初始化
  101. * @access public
  102. * @param array $options 传递给 /ba/Auth 的参数
  103. * @return Auth
  104. */
  105. public static function instance(array $options = []): Auth
  106. {
  107. if (is_null(self::$instance)) {
  108. self::$instance = new static($options);
  109. }
  110. return self::$instance;
  111. }
  112. /**
  113. * 根据Token初始化会员登录态
  114. * @param $token
  115. * @return bool
  116. * @throws Throwable
  117. */
  118. public function init($token): bool
  119. {
  120. if ($this->loginEd) {
  121. return true;
  122. }
  123. if ($this->error) {
  124. return false;
  125. }
  126. $tokenData = Token::get($token);
  127. if (!$tokenData) {
  128. return false;
  129. }
  130. $userId = intval($tokenData['user_id']);
  131. if ($tokenData['type'] == 'user' && $userId > 0) {
  132. $this->model = User::where('id', $userId)->find();
  133. if (!$this->model) {
  134. $this->setError('Account not exist');
  135. return false;
  136. }
  137. if ($this->model->status != 'enable') {
  138. $this->setError('Account disabled');
  139. return false;
  140. }
  141. $this->token = $token;
  142. $this->loginSuccessful();
  143. return true;
  144. } else {
  145. $this->setError('Token login failed');
  146. return false;
  147. }
  148. }
  149. /**
  150. * 会员注册
  151. * @param string $username
  152. * @param string $password
  153. * @param string $mobile
  154. * @param string $email
  155. * @param int $group
  156. * @param array $extend
  157. * @return bool
  158. */
  159. public function register(string $username, string $password, string $mobile = '', string $email = '', int $group = 1, array $extend = []): bool
  160. {
  161. $validate = Validate::rule([
  162. 'mobile' => 'mobile|unique:user',
  163. 'email' => 'email|unique:user',
  164. 'username' => 'regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$|unique:user',
  165. 'password' => 'regex:^(?!.*[&<>"\'\n\r]).{6,32}$',
  166. ]);
  167. $params = [
  168. 'username' => $username,
  169. 'password' => $password,
  170. 'mobile' => $mobile,
  171. 'email' => $email,
  172. ];
  173. if (!$validate->check($params)) {
  174. $this->setError('Registration parameter error');
  175. return false;
  176. }
  177. $ip = request()->ip();
  178. $time = time();
  179. $salt = Random::build('alnum', 16);
  180. $data = [
  181. 'password' => encrypt_password($password, $salt),
  182. 'group_id' => $group,
  183. 'nickname' => preg_match("/^1[3-9]\d{9}$/", $username) ? substr_replace($username, '****', 3, 4) : $username,
  184. 'join_ip' => $ip,
  185. 'join_time' => $time,
  186. 'last_login_ip' => $ip,
  187. 'last_login_time' => $time,
  188. 'salt' => $salt,
  189. 'status' => 'enable',
  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, 'user', $this->model->id, $this->keepTime);
  198. Db::commit();
  199. Event::trigger('userRegisterSuccess', $this->model);
  200. } catch (Throwable $e) {
  201. $this->setError($e->getMessage());
  202. Db::rollback();
  203. return false;
  204. }
  205. return true;
  206. }
  207. /**
  208. * 会员登录
  209. * @param string $username 用户名
  210. * @param string $password 密码
  211. * @param bool $keep 是否保持登录
  212. * @return bool
  213. * @throws Throwable
  214. */
  215. public function login(string $username, string $password, bool $keep): bool
  216. {
  217. // 判断账户类型
  218. $accountType = false;
  219. $validate = Validate::rule([
  220. 'mobile' => 'mobile',
  221. 'email' => 'email',
  222. 'username' => 'regex:^[a-zA-Z][a-zA-Z0-9_]{2,15}$',
  223. ]);
  224. if ($validate->check(['mobile' => $username])) $accountType = 'mobile';
  225. if ($validate->check(['email' => $username])) $accountType = 'email';
  226. if ($validate->check(['username' => $username])) $accountType = 'username';
  227. if (!$accountType) {
  228. $this->setError('Account not exist');
  229. return false;
  230. }
  231. $this->model = User::where($accountType, $username)->find();
  232. if (!$this->model) {
  233. $this->setError('Account not exist');
  234. return false;
  235. }
  236. if ($this->model->status == 'disable') {
  237. $this->setError('Account disabled');
  238. return false;
  239. }
  240. $userLoginRetry = Config::get('buildadmin.user_login_retry');
  241. if ($userLoginRetry && $this->model->login_failure >= $userLoginRetry && time() - $this->model->last_login_time < 86400) {
  242. $this->setError('Please try again after 1 day');
  243. return false;
  244. }
  245. if ($this->model->password != encrypt_password($password, $this->model->salt)) {
  246. $this->loginFailed();
  247. $this->setError('Password is incorrect');
  248. return false;
  249. }
  250. if (Config::get('buildadmin.user_sso')) {
  251. Token::clear('user', $this->model->id);
  252. Token::clear('user-refresh', $this->model->id);
  253. }
  254. if ($keep) {
  255. $this->setRefreshToken($this->refreshTokenKeepTime);
  256. }
  257. $this->loginSuccessful();
  258. return true;
  259. }
  260. /**
  261. * 直接登录会员账号
  262. * @param int $userId 用户ID
  263. * @return bool
  264. * @throws Throwable
  265. */
  266. public function direct(int $userId): bool
  267. {
  268. $this->model = User::find($userId);
  269. if (!$this->model) return false;
  270. if (Config::get('buildadmin.user_sso')) {
  271. Token::clear('user', $this->model->id);
  272. Token::clear('user-refresh', $this->model->id);
  273. }
  274. return $this->loginSuccessful();
  275. }
  276. /**
  277. * 检查旧密码是否正确
  278. * @param $password
  279. * @return bool
  280. */
  281. public function checkPassword($password): bool
  282. {
  283. if ($this->model->password != encrypt_password($password, $this->model->salt)) {
  284. return false;
  285. } else {
  286. return true;
  287. }
  288. }
  289. /**
  290. * 登录成功
  291. * @return bool
  292. */
  293. public function loginSuccessful(): bool
  294. {
  295. if (!$this->model) {
  296. return false;
  297. }
  298. $this->model->startTrans();
  299. try {
  300. $this->model->login_failure = 0;
  301. $this->model->last_login_time = time();
  302. $this->model->last_login_ip = request()->ip();
  303. $this->model->save();
  304. $this->loginEd = true;
  305. if (!$this->token) {
  306. $this->token = Random::uuid();
  307. Token::set($this->token, 'user', $this->model->id, $this->keepTime);
  308. }
  309. $this->model->commit();
  310. } catch (Throwable $e) {
  311. $this->model->rollback();
  312. $this->setError($e->getMessage());
  313. return false;
  314. }
  315. return true;
  316. }
  317. /**
  318. * 登录失败
  319. * @return bool
  320. */
  321. public function loginFailed(): bool
  322. {
  323. if (!$this->model) {
  324. return false;
  325. }
  326. $this->model->startTrans();
  327. try {
  328. $this->model->login_failure++;
  329. $this->model->last_login_time = time();
  330. $this->model->last_login_ip = request()->ip();
  331. $this->model->save();
  332. $this->token = '';
  333. $this->loginEd = false;
  334. $this->model->commit();
  335. } catch (Throwable $e) {
  336. $this->model->rollback();
  337. $this->setError($e->getMessage());
  338. return false;
  339. }
  340. $this->model = null;
  341. return true;
  342. }
  343. /**
  344. * 退出登录
  345. * @return bool
  346. */
  347. public function logout(): bool
  348. {
  349. if (!$this->loginEd) {
  350. $this->setError('You are not logged in');
  351. return false;
  352. }
  353. $this->loginEd = false;
  354. Token::delete($this->token);
  355. $this->token = '';
  356. return true;
  357. }
  358. /**
  359. * 是否登录
  360. * @return bool
  361. */
  362. public function isLogin(): bool
  363. {
  364. return $this->loginEd;
  365. }
  366. /**
  367. * 获取会员模型
  368. * @return User
  369. */
  370. public function getUser(): User
  371. {
  372. return $this->model;
  373. }
  374. /**
  375. * 获取会员Token
  376. * @return string
  377. */
  378. public function getToken(): string
  379. {
  380. return $this->token;
  381. }
  382. /**
  383. * 设置刷新Token
  384. * @param int $keepTime
  385. * @return void
  386. */
  387. public function setRefreshToken(int $keepTime = 0): void
  388. {
  389. $this->refreshToken = Random::uuid();
  390. Token::set($this->refreshToken, 'user-refresh', $this->model->id, $keepTime);
  391. }
  392. /**
  393. * 获取会员刷新Token
  394. * @return string
  395. */
  396. public function getRefreshToken(): string
  397. {
  398. return $this->refreshToken;
  399. }
  400. /**
  401. * 获取会员信息 - 只输出允许输出的字段
  402. * @return array
  403. */
  404. public function getUserInfo(): array
  405. {
  406. if (!$this->model) {
  407. return [];
  408. }
  409. $info = $this->model->toArray();
  410. $info = array_intersect_key($info, array_flip($this->getAllowFields()));
  411. $info['token'] = $this->getToken();
  412. $info['refresh_token'] = $this->getRefreshToken();
  413. return $info;
  414. }
  415. /**
  416. * 获取允许输出字段
  417. * @return array
  418. */
  419. public function getAllowFields(): array
  420. {
  421. return $this->allowFields;
  422. }
  423. /**
  424. * 设置允许输出字段
  425. * @param $fields
  426. * @return void
  427. */
  428. public function setAllowFields($fields): void
  429. {
  430. $this->allowFields = $fields;
  431. }
  432. /**
  433. * 设置Token有效期
  434. * @param int $keepTime
  435. * @return void
  436. */
  437. public function setKeepTime(int $keepTime = 0): void
  438. {
  439. $this->keepTime = $keepTime;
  440. }
  441. public function check(string $name, int $uid = 0, string $relation = 'or', string $mode = 'url'): bool
  442. {
  443. return parent::check($name, $uid ?: $this->id, $relation, $mode);
  444. }
  445. public function getRuleList(int $uid = 0): array
  446. {
  447. return parent::getRuleList($uid ?: $this->id);
  448. }
  449. public function getRuleIds(int $uid = 0): array
  450. {
  451. return parent::getRuleIds($uid ?: $this->id);
  452. }
  453. public function getMenus(int $uid = 0): array
  454. {
  455. return parent::getMenus($uid ?: $this->id);
  456. }
  457. /**
  458. * 是否是拥有所有权限的会员
  459. * @return bool
  460. * @throws Throwable
  461. */
  462. public function isSuperUser(): bool
  463. {
  464. return in_array('*', $this->getRuleIds());
  465. }
  466. /**
  467. * 设置错误消息
  468. * @param string $error
  469. * @return Auth
  470. */
  471. public function setError(string $error): Auth
  472. {
  473. $this->error = $error;
  474. return $this;
  475. }
  476. /**
  477. * 获取错误消息
  478. * @return string
  479. */
  480. public function getError(): string
  481. {
  482. return $this->error ? __($this->error) : '';
  483. }
  484. }