Laravel6.X脚手架(scaffold)添加手机号码验证码注册方式

在上一个关于脚手架的文章中我们谈到如何在注册时添加自定义字段,并且用用户名或者邮箱登录。在这篇文章中我们将讲解如何添加手机号码,用获取验证码来进行注册。

现在几乎所有的正规的网站或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:


aliyun1.png

点击进入,在这里我们要创建一个
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秒里面只可以请求一次。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容

  • 1、简介 Laravel 提供了多种方法来验证应用输入数据。默认情况下,Laravel 的控制器基类使用Valid...
    伊Summer阅读 1,519评论 0 4
  • 去年有段时间得空,就把谷歌GAE的API权威指南看了一遍,收获颇丰,特别是在自己几乎独立开发了公司的云数据中心之后...
    骑单车的勋爵阅读 20,420评论 0 41
  • 点我查看本文集的说明及目录。 本项目相关内容包括: 实现过程: CH7 创建在线商店 CH8 管理支付和订单 CH...
    学以致用123阅读 3,518评论 0 6
  • 点击查看原文 Web SDK 开发手册 SDK 概述 网易云信 SDK 为 Web 应用提供一个完善的 IM 系统...
    layjoy阅读 13,634评论 0 15
  • 开发环境 安装常用浏览器,如:火狐安装常用的轻量级的文本编辑器,如:notepad++... IDE websto...
    豆约翰阅读 246评论 0 0