SmsLogin.php 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. <?php
  2. declare(strict_types=1);
  3. namespace app\miniapi\service;
  4. use app\miniapi\auth\SmsCode;
  5. use app\miniapi\exception\ApiException;
  6. use app\miniapi\http\Fields;
  7. use app\miniapi\model\MiniUser;
  8. final class SmsLogin
  9. {
  10. public function __construct(private readonly SmsSender $sender = new SmsbaoSender())
  11. {
  12. }
  13. /**
  14. * Always answers with the same payload whether or not the phone belongs to an account.
  15. * Distinguishing the two cases would let a caller enumerate registered numbers, so an
  16. * unbound phone silently consumes nothing and receives no message.
  17. */
  18. public function sendCode(array $input, string $ip): array
  19. {
  20. if (!SmsCode::enabled()) {
  21. throw new ApiException(50301, '短信登录未开启', 503);
  22. }
  23. Fields::only($input, ['phone']);
  24. $phone = self::mobile($input);
  25. SmsCode::assertSendAllowed($phone, $ip);
  26. if (!MiniUser::query()->where('phone', $phone)->where('status', 1)->exists()) {
  27. return ['sent' => true, 'expires_in' => SmsCode::ttl()];
  28. }
  29. $code = SmsCode::issue($phone, $ip);
  30. try {
  31. $this->sender->send($phone, sprintf('您的验证码是 %s,%d 分钟内有效。请勿转发给他人。', $code, intdiv(SmsCode::ttl(), 60)));
  32. } catch (ApiException $error) {
  33. // A code the caller never received must not remain verifiable.
  34. SmsCode::discard($phone, $code);
  35. throw $error;
  36. }
  37. return ['sent' => true, 'expires_in' => SmsCode::ttl()];
  38. }
  39. public function login(array $input): array
  40. {
  41. if (!SmsCode::enabled()) {
  42. throw new ApiException(50301, '短信登录未开启', 503);
  43. }
  44. Fields::only($input, ['phone', 'code']);
  45. $phone = self::mobile($input);
  46. SmsCode::consume($phone, Fields::text($input, 'code', 6));
  47. $user = MiniUser::query()->where('phone', $phone)->first();
  48. if (!$user || (int) $user->status !== 1) {
  49. throw new ApiException(40102, '验证码已失效,请重新获取', 401);
  50. }
  51. return (new AuthService())->issueForExisting($user);
  52. }
  53. /** SMS gateways only reach mainland mobile numbers; other formats never leave the server. */
  54. public static function mobile(array $input): string
  55. {
  56. $phone = Fields::phone($input);
  57. if (!preg_match('/^1[3-9][0-9]{9}$/D', $phone)) {
  58. throw new ApiException(42201, '请填写有效的中国大陆手机号码', 422, ['phone' => '须为1开头的11位手机号']);
  59. }
  60. return $phone;
  61. }
  62. }