发送短信验证码
parent
4a8ee05688
commit
37a9cc4a54
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Exceptions\BizException;
|
||||
use App\Http\Requests\Api\V1\SmsCode\StoreRequest;
|
||||
use App\Services\CaptchaService;
|
||||
use App\Services\SmsCodeService;
|
||||
use Throwable;
|
||||
|
||||
class SmsCodeController extends Controller
|
||||
{
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param \App\Http\Requests\Api\V1\SmsCode\StoreRequest $request
|
||||
* @param \App\Services\CaptchaService $captchaService
|
||||
* @param \App\Services\SmsCodeService $smsCodeService
|
||||
* @return \Illuminate\Http\Response
|
||||
*
|
||||
* @throws \App\Exceptions\BizException
|
||||
*/
|
||||
public function store(
|
||||
StoreRequest $request,
|
||||
CaptchaService $captchaService,
|
||||
SmsCodeService $smsCodeService,
|
||||
) {
|
||||
if (! app()->isLocal()) {
|
||||
$captchaService->validatePhrase(
|
||||
(string) $request->input('captcha_key'),
|
||||
(string) $request->input('captcha_phrase')
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
$smsCodeService->send(
|
||||
$request->input('phone'),
|
||||
$request->input('type'),
|
||||
app()->isProduction() ? mt_rand(100000, 999999) : '666666',
|
||||
);
|
||||
} catch (BizException $e) {
|
||||
throw $e;
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\V1\SmsCode;
|
||||
|
||||
use App\Rules\PhoneNumber;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'phone' => ['bail', 'required', new PhoneNumber()],
|
||||
'type' => ['bail', 'required', 'int'],
|
||||
'captcha_phrase' => ['bail', 'required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Notifications\SmsCodeCreated;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class SmsCode extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
|
||||
public const TYPE_REGISTER = 1;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $attributes = [
|
||||
'is_use' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'phone',
|
||||
'type',
|
||||
'code',
|
||||
'is_use',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $casts = [
|
||||
'type' => 'int',
|
||||
'is_use' => 'bool',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 允许发送短信的验证码类型
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $allowedTypes = [
|
||||
self::TYPE_REGISTER,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($smsCode) {
|
||||
if (app()->isProduction()) {
|
||||
$smsCode->notify(new SmsCodeCreated($smsCode));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认此验证码是否无效
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isInvalid(): bool
|
||||
{
|
||||
return $this->is_use || ($this->expires_at && $this->expires_at->lte(now()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Notifications\Notification $notification
|
||||
* @return string
|
||||
*/
|
||||
public function routeNotificationForSms($notification): string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications\Channels;
|
||||
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Overtrue\EasySms\EasySms;
|
||||
use Overtrue\EasySms\Exceptions\NoGatewayAvailableException;
|
||||
use Overtrue\EasySms\Message;
|
||||
|
||||
class SmsChannel
|
||||
{
|
||||
/**
|
||||
* @param \Overtrue\EasySms\EasySms $easySms
|
||||
*/
|
||||
public function __construct(
|
||||
protected EasySms $easySms
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $notifiable
|
||||
* @param \Illuminate\Notifications\Notification $notification
|
||||
* @return void
|
||||
*/
|
||||
public function send($notifiable, Notification $notification): void
|
||||
{
|
||||
$message = $notification->toSms($notifiable);
|
||||
$phone = $notifiable->routeNotificationFor('sms', $notification);
|
||||
|
||||
if (! $phone && ! $message instanceof Message) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->easySms->send($phone, $message);
|
||||
} catch (NoGatewayAvailableException $e) {
|
||||
foreach ($e->getExceptions() as $exception) {
|
||||
report($exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications\Messages;
|
||||
|
||||
use App\Models\SmsCode;
|
||||
use Overtrue\EasySms\Message;
|
||||
|
||||
class SmsCodeMessage extends Message
|
||||
{
|
||||
/**
|
||||
* @param \App\Models\SmsCode $smsCode
|
||||
*/
|
||||
public function __construct(
|
||||
protected SmsCode $smsCode,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\SmsCode;
|
||||
use App\Notifications\Channels\SmsChannel;
|
||||
use App\Notifications\Messages\SmsCodeMessage;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Overtrue\EasySms\Message as SmsMessage;
|
||||
|
||||
class SmsCodeCreated extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* @param \App\Models\SmsCode $smsCode
|
||||
*/
|
||||
public function __construct(
|
||||
protected SmsCode $smsCode,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return [SmsChannel::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信消息通知.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return \Overtrue\EasySms\Message
|
||||
*/
|
||||
public function toSms($notifiable): SmsMessage
|
||||
{
|
||||
return new SmsCodeMessage($this->smsCode);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ namespace App\Providers;
|
|||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Overtrue\EasySms\EasySms;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -15,6 +16,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->registerEasySms();
|
||||
$this->registerRequestRealIp();
|
||||
}
|
||||
|
||||
|
|
@ -30,6 +32,18 @@ class AppServiceProvider extends ServiceProvider
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册短信发送服务
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function registerEasySms()
|
||||
{
|
||||
$this->app->singleton(EasySms::class, function ($app) {
|
||||
return new EasySms($app['config']->get('easysms'));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 在请求上注册 realIp 宏
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\BizException;
|
||||
use App\Models\SmsCode;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
|
||||
class SmsCodeService
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $expires = 180;
|
||||
|
||||
/**
|
||||
* @param \Illuminate\Contracts\Cache\Repository $cache
|
||||
*/
|
||||
public function __construct(
|
||||
protected Cache $cache,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param string $phone
|
||||
* @param int $type
|
||||
* @param string $code
|
||||
* @param int $decaySeconds
|
||||
* @return \App\Models\SmsCode
|
||||
*
|
||||
* @throws \App\Exceptions\BizException
|
||||
*/
|
||||
public function send(
|
||||
string $phone,
|
||||
int $type,
|
||||
string $code,
|
||||
int $decaySeconds = 60,
|
||||
): SmsCode {
|
||||
if (! in_array($type, SmsCode::$allowedTypes)) {
|
||||
throw new BizException(__('Invalid verification code type'));
|
||||
}
|
||||
|
||||
if ($type === SmsCode::TYPE_REGISTER) {
|
||||
if (User::where('phone', $phone)->exists()) {
|
||||
throw new BizException(__('The phone number is already registered'));
|
||||
}
|
||||
}
|
||||
|
||||
if (! $this->cache->add("sms_lock_{$type}_{$phone}", 1, $decaySeconds)) {
|
||||
throw new BizException(__('Sending too frequently, please try again later'));
|
||||
}
|
||||
|
||||
return SmsCode::create([
|
||||
'phone' => $phone,
|
||||
'code' => $code,
|
||||
'type' => $type,
|
||||
'expires_at' => now()->addSeconds($this->expires),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码是否正确
|
||||
*
|
||||
* @param string $phone
|
||||
* @param int $type
|
||||
* @param string $code
|
||||
* @return void
|
||||
*
|
||||
* @throws \App\Exceptions\BizException
|
||||
*/
|
||||
public function validate(string $phone, int $type, string $code): void
|
||||
{
|
||||
$smsCode = SmsCode::where('phone', $phone)
|
||||
->where('type', $type)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
if ($smsCode === null || $smsCode->code !== $code || $smsCode->isInvalid()) {
|
||||
throw new BizException(__('Invalid verification code'));
|
||||
}
|
||||
|
||||
$smsCode->update([
|
||||
'is_use' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,7 +19,8 @@
|
|||
"kalnoy/nestedset": "^6.0",
|
||||
"laravel/framework": "^8.65",
|
||||
"laravel/sanctum": "^2.12",
|
||||
"laravel/tinker": "^2.5"
|
||||
"laravel/tinker": "^2.5",
|
||||
"overtrue/easy-sms": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"facade/ignition": "^2.5",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "865b93f26360dff74bb3e2c2c6dac800",
|
||||
"content-hash": "d93e7912aa10ac47d7b1923ce5dd28aa",
|
||||
"packages": [
|
||||
{
|
||||
"name": "asm89/stack-cors",
|
||||
|
|
@ -2858,6 +2858,81 @@
|
|||
},
|
||||
"time": "2021-04-09T13:42:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "overtrue/easy-sms",
|
||||
"version": "2.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/overtrue/easy-sms.git",
|
||||
"reference": "8a9d45cdd090dc66b26faad127614a24c6c1b049"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/overtrue/easy-sms/zipball/8a9d45cdd090dc66b26faad127614a24c6c1b049",
|
||||
"reference": "8a9d45cdd090dc66b26faad127614a24c6c1b049",
|
||||
"shasum": "",
|
||||
"mirrors": [
|
||||
{
|
||||
"url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%",
|
||||
"preferred": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"guzzlehttp/guzzle": "^6.2 || ^7.0",
|
||||
"php": ">=5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"brainmaestro/composer-git-hooks": "^2.8",
|
||||
"friendsofphp/php-cs-fixer": "^3.0",
|
||||
"jetbrains/phpstorm-attributes": "^1.0",
|
||||
"mockery/mockery": "~1.3.3 || ^1.4.2",
|
||||
"phpstan/phpstan": "^1.0",
|
||||
"phpunit/phpunit": "^5.7 || ^7.5 || ^8.5.19 || ^9.5.8",
|
||||
"vimeo/psalm": "^4.10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"hooks": {
|
||||
"pre-commit": [
|
||||
"composer check-style",
|
||||
"composer psalm",
|
||||
"composer test"
|
||||
],
|
||||
"pre-push": [
|
||||
"composer check-style"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Overtrue\\EasySms\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "overtrue",
|
||||
"email": "i@overtrue.me"
|
||||
}
|
||||
],
|
||||
"description": "The easiest way to send short message.",
|
||||
"support": {
|
||||
"issues": "https://github.com/overtrue/easy-sms/issues",
|
||||
"source": "https://github.com/overtrue/easy-sms/tree/2.0.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/overtrue",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2021-11-19T03:46:38+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoption/phpoption",
|
||||
"version": "1.8.0",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
// HTTP 请求的超时时间(秒)
|
||||
'timeout' => 5.0,
|
||||
|
||||
// 默认发送配置
|
||||
'default' => [
|
||||
// 网关调用策略,默认:顺序调用
|
||||
'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,
|
||||
|
||||
// 默认可用的发送网关
|
||||
'gateways' => [
|
||||
//
|
||||
],
|
||||
],
|
||||
// 可用的网关配置
|
||||
'gateways' => [
|
||||
'errorlog' => [
|
||||
'file' => storage_path('logs/easysms.log'),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateSmsCodesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('sms_codes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id')->nullable();
|
||||
$table->string('phone', 20)->comment('手机号');
|
||||
$table->tinyInteger('type')->comment('验证码类型');
|
||||
$table->string('code')->comment('验证码');
|
||||
$table->boolean('is_use')->default(0)->comment('是否使用');
|
||||
$table->timestamp('expires_at')->nullable()->comment('过期时间');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['phone', 'type']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('sms_codes');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,5 +2,9 @@
|
|||
":resource not found": ":resource 未找到",
|
||||
"Invalid captcha": "无效的验证码",
|
||||
"Invalid invitation code": "无效的邀请码",
|
||||
"Registration failed, please try again": "注册失败,请重试"
|
||||
"Invalid verification code": "无效的验证码",
|
||||
"Invalid verification code type": "无效的验证码类型",
|
||||
"Registration failed, please try again": "注册失败,请重试",
|
||||
"Sending too frequently, please try again later": "发送过于频繁,请稍后再试",
|
||||
"The phone number is already registered": "手机号码已被注册"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\V1\CaptchaController;
|
||||
use App\Http\Controllers\Api\V1\SmsCodeController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::post('captchas', [CaptchaController::class, 'store']);
|
||||
Route::get('captchas/{captcha}', [CaptchaController::class, 'show']);
|
||||
|
||||
Route::post('sms-codes', [SmsCodeController::class, 'store']);
|
||||
|
|
|
|||
Loading…
Reference in New Issue