在上一个关于脚手架的文章中我们谈到如何在注册时添加自定义字段,并且用用户名或者邮箱登录。在这篇文章中我们将讲解如何添加手机号码,用获取验证码来进行注册。
现在几乎所有的正规的网站或APP都要用手机来注册,接验证码注册。本文要做的是把Laravel的脚手架改装成用手机号码登录。我们的起点是安装laravel, 安装脚手架。
业务逻辑:
我们在前端要加一个点击获取验证码的按钮,并配上倒计时,点击以后倒计时开始并且按钮状态为不可能用,倒计时结束以后变可点,并且文字变成重新发送邀请码。
在后端我们要加一个键值对,用缓存或者session。当有手机号提交的时候,后端在键值对中保存此手机号码,后端调用手机短信服务发送短信,里面包含验证码,后端将此验证码和手机保存在一个键值对里。如键是我们后端生成的唯一字符串,值是一个数组,包含手机号码,短信验证码和过期时间。
当用户点击获取验证码的时候,前端把手机号码提交到后端,并生成键值对,把键发送到前端一个隐藏的输入框里,待用户把验证码和其他信息如密码等一同提交到后端的时候,后端进行比对,如果有错则发送到前端,并把错误信息展示在前端。
这里有两种情况就是,键值对的缓存已经过期,或者验证码匹配错误。
1.构建基础
首先我们来安装脚手架,短短几行命令就完成了。
composer require laravel/ui --dev
在之前的Laravel版本,脚手架的安装方式不同,从6.X开始,脚手架归到了LARAVEL/UI这个包里,所以我们要先安装这个包。然后我们来安装这个脚手架。
php artisan ui vue --auth
--auth这个后缀意味着要有登录和注册的功能也要有。
在这之后我们不要直接执行迁移文件,而是要对迁移文件进行一个小的修改。迁移文件在database/migrations/这里。
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('cellphone')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}
我们把email改成了cellphone,然后把其他一些不必要的字段删除了。然后我们执行:
php artisan migrate
便完成了数据库的迁移。
2.前端视图的修改
在前端方面我们要把name这个字段删掉,加上验证码这个字段并加上倒计时可点按钮,也就是说每隔60秒才能点一次的按钮。找到resources/views/auth/register.blade.php文件。我们把这个文件修改成:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header text-center">{{ __('Register') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('register') }}">
@csrf
<div class="form-group row">
<label for="cellphone" class="col-md-4 col-form-label text-md-right">{{ __('手机号码') }}</label>
<div class="col-md-6">
<input id="cellphone" type="tel" class="form-control @error('cellphone') is-invalid @enderror" name="cellphone" placeholder="请输入您的手机号码" value="{{ old('cellphone') }}" required autocomplete="cellphone">
@error('cellphone')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="vcode" class="col-md-4 col-form-label text-md-right">{{ __('验证码') }}</label>
<div class="col-md-6">
<div class="input-group">
<input id="vcode" type="tel" class="form-control @error('vcode') is-invalid @enderror" name="vcode" value="{{ old('vcode') }}" placeholder="请输入收到的验证码" required autocomplete="vcode">
<div class="input-group-append">
<input type="button" class="btn btn-outline-secondary" value="获取验证码" id="getvcode" onclick="sendMessages()">
</div>
</div>
<span class="vcode-error"></span>
@error('vcode')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" placeholder="请输入密码" required autocomplete="new-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="请再次输入密码" required autocomplete="new-password">
</div>
</div>
<input type="hidden" name="verification_key" id="verification_key">
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Register') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('additionalJs')
<script>
var InterValObj; //timer变量,控制时间
var verification_key;
var count = 10; //间隔函数,1秒执行
var curCount; //当前剩余秒数
var code = ""; //验证码
var codeLength = 6; //验证码长度
var getvcode = $("#getvcode");
function sendMessages() {
curCount = count;
var phone = $("#cellphone").val();
if(validatePhone(phone)) {
alert('请填写手机号码!');
return;
}
if(phone != "") {
//设置button效果,开始计时
getvcode.attr("disabled", "true");
getvcode.text("请在" + curCount + "秒内输入");
InterValObj = window.setInterval(SetRemainTimes, 1000); //启动计时器,1秒执行一次
//向后台发送处理数据,用ajax发送此处开始
axios.post('/verificationcodes',{"cellphone":phone})
.then(function(response){
if(response){
verification_key = response.data.key;
$("#verification_key").val(verification_key);
console.log(response.data);
}
});
} else {
alert("手机号码不能为空!!!!!!");
}
}
//timer处理函数
function SetRemainTimes() {
if(curCount == 0) {
window.clearInterval(InterValObj); //停止计时器
getvcode.removeAttr("disabled"); //启用按钮
getvcode.val("重发送验证码");
code = ""; //清除验证码。如果不清除,过时间后,输入收到的验证码依然有效
} else {
curCount--;
getvcode.val("请在" + curCount + "秒内输入");
}
}
</script>
@endsection
在这里我们加了一个新的section, additionalJs加到layouts里面。
@yield('additionalJs')
3.短信服务申请
这样前端的部分我们就完成了。下面我们再来安装一个laravel插件easysms,此处我们用的是阿里云的短信服务,所以我们来讲解下阿里云短信服务的申请。短信服务一般分为签名和模板这两个部分。签名一般显示在短信的开始,一般用括号括起来的,例如【阿里云】,而短信模板是短信的内容,里面通常有一个变量,例如验证码。
登录阿里云的后台,搜索产品,短信,然后选择国内消息,我们先添加签名,签名一般是公司或者服务的名称,然后选择验证码,填写描述。等待审核通过,一般在2个小时内审核通过。
审核通过之后我们再选择模板,然后填写模板名称,模板内容,申请说明等等提交。等待审核通过,一般在2个小时内审核通过。
我们在短信服务里面获取我们的AccessKey:
点击进入,在这里我们要创建一个
AccessKey ID
Access Key Secret
4.安装和配置easysms插件
这里我们要要用到的是一个laravel扩展,叫easysms。现在我们来安装和配置它。
composer require "overtrue/easy-sms"
这个组件没有Laravel 的 ServiceProvider,为了方便我们来封装一下:
创建一个配置文件config/easysms.php,然后把下面的代码贴进去:
<?php
return [
// HTTP 请求的超时时间(秒)
'timeout' => 10.0,
// 默认发送配置
'default' => [
// 网关调用策略,默认:顺序调用
'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,
// 默认可用的发送网关
'gateways' => [
'aliyun',
],
],
// 可用的网关配置
'gateways' => [
'errorlog' => [
'file' => '/tmp/easy-sms.log',
],
'aliyun' => [
'access_key_id' => env('SMS_ALIYUN_ACCESS_KEY_ID'),
'access_key_secret' => env('SMS_ALIYUN_ACCESS_KEY_SECRET'),
'sign_name' => '把刚才填入的签名填进去',
'templates' => [
'register' => env('SMS_ALIYUN_TEMPLATE_REGISTER'),
]
],
],
];
大家可以看到我们此处引用的是.env文件里的设置常量,所以我们在.env文件里面也设置一下。
# aliyun 短信
SMS_ALIYUN_ACCESS_KEY_ID=你自己的accesskey id
SMS_ALIYUN_ACCESS_KEY_SECRET=你自己的accesskey secret
SMS_ALIYUN_TEMPLATE_REGISTER=你的template code
我们总结下,我们从服务商里获得四个数据,分别是签名,模板code, accesskey id和accesskey secret,这些我们都要配置到里面。
5.封装easysms
我们先创建一个provider,然后把注册下这个provider, 并且把它填到cofig/app的provider列表里面。
php artisan make:provider EasySmsServiceProvider
修改app/providers/EasySmsServiceProvider.php,这个文件:
<?php
namespace App\Providers;
use Overtrue\EasySms\EasySms;
use Illuminate\Support\ServiceProvider;
class EasySmsServiceProvider extends ServiceProvider
{
public function boot()
{
//
}
public function register()
{
$this->app->singleton(EasySms::class, function ($app) {
return new EasySms(config('easysms'));
});
$this->app->alias(EasySms::class, 'easysms');
}
}
在config/app里面添加这个提供商到列表里:
App\Providers\EasySmsServiceProvider::class,
接着我们用tinker来调试下是否能够发送短信。
php artisan tinker
直接粘贴:
$sms = app('easysms');
然后换行:
try {
$sms->send(您的手机号码, [
'template' => '你的template code',
'data' => [
'code' => 1234
],
]);
} catch (\Overtrue\EasySms\Exceptions\NoGatewayAvailableException $exception) {
$message = $exception->getException('aliyun')->getMessage();
dd($message);
}
如果发送成功,表明发送短信成功。
6.后端控制器的逻辑
我们首先验证下这个数据,验证规则在http/controllers/Auth/regiterController.php里面。在validator方法里:
'cellphone' => ['required', 'numeric', 'regex:/^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199)\d{8}$/','unique:users'],
我们的业务逻辑是这样的,我们在前端有个按钮,获取验证码,通过这个按钮,我们会发送一个ajax请求到后端,把手机号码发过去。后端通过easysms的扩展发一个验证码到用户手机,然后后端再负责把这个验证码,手机号和一个生成的key字符串储存到缓存里,并设置一个过期时间。
我们把这个key字符串和过期时间发送到前端储存到input hidden里面。然后跟其他注册信息提交到后端进行验证。
提交的时候后台在进行比对,通过key比对过期时间和验证码,如果对才可以注册,如果不对则要重新进行验证码请求。
先创建一个控制器和方法:
php artisan make:controller SmsController
我们的控制器的代码:
public function store(Request $request, EasySms $easySms)
{
$phone = $request->phone;
// 生成4位随机数,左侧补0
$code = str_pad(random_int(1, 9999), 4, 0, STR_PAD_LEFT);
try {
$result = $easySms->send($phone, [
'template' => config('easysms.gateways.aliyun.templates.register'),
'data' => [
'code' => $code
],
]);
} catch (\Overtrue\EasySms\Exceptions\NoGatewayAvailableException $exception) {
$message = $exception->getException('aliyun')->getMessage();
abort(500, $message ?: '短信发送异常');
}
$key = 'verificationCode_'.Str::random(15);
$expiredAt = now()->addMinutes(5);
// 缓存验证码 5 分钟过期。
\Cache::put($key, ['phone' => $phone, 'code' => $code], $expiredAt);
return response()->json([
'key' => $key,
'expired_at' => $expiredAt->toDateTimeString(),
])->setStatusCode(201);
}
在头部我们要引入
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
7.修改注册控制器
我们打开http/controllers/Auth/regiterController.php这个文件:
$verifyData = Cache::get($data['verification_key']);
if(!$verifyData) {
abort(403, '验证码已失效');
}
//hash_equals 是可防止时序攻击的字符串比较
if (!hash_equals($verifyData['code'], $data['vcode'])){
// 返回401
throw new AuthenticationException('验证码错误');
}
Cache::forget($data['verification_key']);
return User::create([
'cellphone' => $data['cellphone'],
'password' => Hash::make($data['password']),
]);
8.实施安全措施
为了保护我们的接口,我们要加一个验证码在手机号码和验证码的中间,如果没有填写验证码的话就不能填手机验证码。同时,我们限制在这个接口上在单位时间请求的次数。
在获取验证码的路由上加一个中间件:
middleware('throttle:1,1')
这个表明我们在60秒里面只可以请求一次。