6
0
Fork 0

发送短信验证码

release
李静 2021-11-24 16:07:30 +08:00
parent 4a8ee05688
commit 37a9cc4a54
14 changed files with 522 additions and 3 deletions

View File

@ -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();
}
}

View File

@ -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'],
];
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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,
) {
}
}

View File

@ -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);
}
}

View File

@ -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
*

View File

@ -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,
]);
}
}

View File

@ -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",

77
composer.lock generated
View File

@ -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",

23
config/easysms.php 100644
View File

@ -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'),
],
],
];

View File

@ -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');
}
}

View File

@ -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": "手机号码已被注册"
}

View File

@ -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']);