1、准备工作
1.1、准备依赖包
- 这个网站提供的都是PHP包,挺有用的 https://packagist.org/
- 另外还可以在GitHub上面下载 https://github.com/
1、短信方面 我这里用到 packagist 里面的 overtrue/easy-sms
下载依赖包之前记得要看下依赖的PHP版本、短信平台等。
我们可以看到这个依赖包提供的平台有以下:
2、Redis方面 我用到了Predis的包
另外他包里面提供了使用方法和类型,需要去看下,不过本人进行了代码封装。
安装这些包的话直接打开cmd命令行输入 composer require XXX 即可,
XXX是对应包名,XXX后面如果加 :1.*就相当于版本号,包里面会介绍相关的操作。
2、代码封装
2.1、封装的文件
- 包括两个 服务类 和 config.php ,用于封装不同业务类型的存储和发送的方法,如下图
2.2、配置文件
- config.php 文件,用于保存短信配置和白名单等,内容如下
//通用发送短信配置(短信消息配置)
'easy_sms' => [
// HTTP 请求的超时时间(秒)
'timeout' => 5.0,
// 默认发送配置
'default' => [
// 网关调用策略,默认:顺序调用
'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class,
// 默认可用的发送网关
'gateways' => [
'yunpian',
],
],
// 可用的网关配置
'gateways' => [
'errorlog' => [
'file' => '/tmp/easy-sms.log',
],
//云片
'yunpian' => [
'api_key' => '',
],
//阿里云
'aliyun' => [
'access_key_id' => '',
'access_key_secret' => '',
'sign_name' => 'aa',
],
//...
],
],
//发送消息限制配置
'sms_limit' => [
'white_list' => [ //发送短信白名单
'13512341234',
'18664337604',
'13450681681',
],
'save_time' => 3600, //设置保存时间 默认一小时
'send_total' => 5, //限制时间内最多发送5条
'expires' => 900, //设置验证码过期时间 默认15分钟
],
//Redis配置
'redis_host' => Env::get('redis.hostname','192.168.2.168'),
'redis_password' => Env::get('redis.password',''),
'redis_port' => Env::get('redis.hostport','6379'),
'redis_prefix' => Env::get('redis.prefix','su::'),
其中 Env 是对应的配置文件,你也可以直接在第二个参数填写默认的配置即可。
2.3、Redis服务类
<?php
namespace app\common\service;
use Predis\Client;
class RedisService
{
//Redis保存的key
//短信部分
const SU_SMS_LOGIN = 'sms::login::'; //短信验证码|登录和找回密码(后面跟 国际区号-用户手机号)
const SU_SMS_RESET_PWD = 'sms::reset::pwd::'; //短信验证码|重置账号密码(后面跟 国际区号-用户手机号)
const SU_SMS_RESET_MOBILE = 'sms::reset::mobile::'; //短信验证码|重置手机号码(后面跟 国际区号-用户手机号)
const SU_SMS_CREATE_ACCOUNT_MOBILE = 'sms::create::account::mobile::'; //创建子账号|修改子账号手机号(后面跟 国际区号-用户手机号)
const SU_SMS_OTHER = 'sms::other::'; //短信验证码|其他情况(后面跟 国际区号-用户手机号)
const SU_SMS_NUM = 'sms::num::'; //手机短信发送次数(后面跟 国际区号-用户手机号)
private static $prefix = '';
private static $client;
/**
* 单例模式获取redis连接实例
* @return Client
*/
public static function connect()
{
if (!self::$client) {
self::$prefix = config('redis_prefix');
$config = [
'scheme' => 'tcp',
'host' => config('redis_host'),
'port' => config('redis_port'),
];
//没有配置密码时,不传入密码项参数
if (config('redis_password')) $config['password'] = config('redis_password');
self::$client = new Client($config, ['prefix' => self::$prefix]);
}
return self::$client;
}
/**
* 校验短信验证码
* @param string $areaCode 手机国际区号
* @param string $mobile 手机号
* @param string $smsCode 验证码
* @param string $prefix 根据业务区分的短信前缀
* @param bool $isThrowException 是否抛出异常
* @param bool $isDel 检查完后是否删除该缓存
* @return array
*/
public static function checkSmsCode(string $areaCode, string $mobile, string $smsCode, string $prefix = self::SU_SMS_LOGIN, bool $isThrowException = true, bool $isDel = true)
{
$res = [true, '短信验证码正确!'];
if (!self::connect()->exists($prefix .$areaCode . '-' . $mobile)) {
$isThrowException ? throwResult('手机验证码失效') : $res = [false, '手机验证码失效'];
} else {
if (!hash_equals($smsCode, self::connect()->get($prefix . $areaCode . '-' . $mobile)))
$isThrowException ? throwResult('手机验证码不正确') : $res = [false, '手机验证码不正确'];
}
if ($isDel) self::connect()->del($prefix . $mobile);
return $res;
}
最上面定义的常量都是用于定义业务类型,后面跟着手机号
throwResult 是封装好的抛出异常的方法,使用自己封装的即可。
connect 方法是用于实例化,每次用redis时候直接RedisService::connect();
checkSmsCode 方法是用于检验验证码是否正确的,其中需要传业务类型前缀。
2.4、发送短信服务类
<?php
namespace app\common\service;
use Overtrue\EasySms\EasySms;
use Overtrue\EasySms\Exceptions\NoGatewayAvailableException;
class SmsService
{
/**
* 发送通用短信验证码
* @param string $areaCode 手机国际区号
* @param string $mobile 手机号
* @param int $smsUseType 业务类型:0=注册登录,1=更换密码,2=修改手机号,3=创建子账号,10=其他
* @param int $userId 用户ID
* @return int
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException
* @throws \app\common\exception\WorkException
*/
public static function sendSmsCode(string $areaCode,string $mobile, int $smsUseType = 0, int $userId = 0)
{
$redis = RedisService::connect();
$countKey = RedisService::SU_SMS_NUM . $mobile; //记录该手机发送的次数
//验证码业务类型
switch ($smsUseType) {
case 0:
//注册登录
$smsKey = RedisService::SU_SMS_LOGIN . $areaCode . '-' . $mobile;
break;
case 1:
//重置密码
$smsKey = RedisService::SU_SMS_RESET_PWD . $areaCode . '-' . $mobile;
break;
case 2:
//修改手机号
$smsKey = RedisService::SU_SMS_RESET_MOBILE . $areaCode . '-' . $mobile;
break;
case 3:
//创建子账号
$smsKey = RedisService::SU_SMS_CREATE_ACCOUNT_MOBILE . $areaCode . '-' . $mobile;
break;
case 10:
//其他
$smsKey = RedisService::SU_SMS_OTHER . $areaCode . '-' . $mobile;
break;
default:
//通用
$smsKey = RedisService::SU_SMS_OTHER . $areaCode . '-' . $mobile;
break;
}
//白名单可以无限制使用发送次数
if (!in_array($mobile, config('sms_limit.white_list'))) {
//防止恶意发送,每小时限制5次
$count = $redis->get($countKey);
if ($count && $count >= config('sms_limit.send_total')) {
throwResult('超过发送次数限制,请稍后再试');
}
}
//检查该手机号一段时间内已发送的次数
$redisIncr = $redis->incr($countKey);
if ($redisIncr == 1) $redis->expire($countKey, config('sms_limit.save_time')); //设置保存时间 默认一小时
//查找如果存在相同键名的短信码则更新采用旧的编码
$smsCode = config('app_debug') ? 8888 : ($redis->get($smsKey) ?: mt_rand(1000, 9999));
//保存验证码并设置15分钟有效时间
$redis->set($smsKey, $smsCode, 'EX', config('sms_limit.expires')); //todo 短信验证码过期时间
//发送短信并保存记录
if (!config('app_debug')) self::send($areaCode,$mobile, ['content' => $smsContent], $smsCode, $smsUseType, $userId);
return $smsCode;
}
/**
* 通用短信发送方法
* @param string $areaCode 手机国际区号
* @param string $mobile 手机号码
* @param string $content 发送内容
* @param string $smsCode 手机验证码(没有则不需要传参)
* @param int $smsUseType 0=注册登录,1=更换密码,2=修改手机号,3=创建子账号,10=其他
* @param int $userId 用户ID 默认为0(用户不存在时候不需要传参)
* @return bool
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException
*/
public static function send(string $areaCode, string $mobile, string $content, string $smsCode = '', int $smsUseType = 0, int $userId = 0)
{
//发送短信的内容
$isSuccess = 1;
try {
$mobile = new PhoneNumber($mobile, $areaCode);
$option = [
'content' => $content, //文字内容,使用在像云片类似的以文字内容发送的平台
'template' => 'SMS_001', // 模板 ID,使用在以模板ID来发送短信的平台
'data' => //模板变量,使用在以模板ID来发送短信的平台
[
'code' => 6379
],
];
$easySms = new EasySms(config('easy_sms'));
$easySms->send($mobile, $option);
} catch (NoGatewayAvailableException $e) {
$isSuccess = 3;
}
//短信验证码记录成功与失败到表
// SmsModel::createOne($mobile, $option['content'], $isSuccess, $smsUseType, $smsCode, $userId);
return $isSuccess == 1 ? true : false;
}
}
我封装的时候加了一些附加条件:
业务类型 我们根据不同的业务类型进行不同的 键值 保存和操作。
限制次数 白名单用户可以无限次发送,而普通用户限制每小时最多发送多少次,主要通过 SU_SMS_NUM 这个redis 键值去判断,这个键值需要根据具体业务设置个时间,比如1小时。
验证码时长 验证码需要存入redis中并设置一个时间,保证验证码的有效时长。
稳定验证码 每次发送验证码之前需要去查看是否存在 有效验证码 ,有的话则直接发送该验证码。
3、实现过程
3.1、发送短信
/**
* @ApiTitle (发送短信验证码)
* @ApiSummary (用于发送短信验证码)
* @ApiMethod (POST)
* @ApiRoute (/api/User/sendSmsByPhone)
* @ApiHeaders (name=Authorization, type=string, required=false, description="用户Token,其中修改手机号需要传")
* @ApiParams (name="mobile", type="string", required=true, description="手机号")
* @ApiParams (name="area_code", type="string", required=true, description="国际区号")
* @ApiParams (name="sms_type", type="string", required=false, description="业务类型:0=注册登录,1=更换密码,2=修改手机号,3=创建子账号和修改子账号手机号,10=其他,默认为0")
*/
public function sendSmsByPhone()
{
$areaCode = input('area_code'); //国际区号
$mobile = input('mobile'); //手机号
$smsType = intval(input('sms_type')) ?? 0; //短信类型
//验证数据
$this->validate([
'mobile' => $mobile,
'area_code' => $areaCode,
'sms_type' => $smsType,
], 'UserValidate.send_sms_by_phone');
//业务类型判断
if ($smsType == 1) { //重置、找回密码
// $user = $this->auth->getUser();
// if (!$user) $this->error(MSG_NEED_LOGIN);
} elseif ($smsType == 2) { //修改手机号需要登录和验证手机号
$user = $this->auth->getUser();
if (!$user) $this->error(MSG_NEED_LOGIN);
$oldMobile = $user->toArray()['mobile'];
$oldAreaCode = $user->toArray()['area_code'];
if ($areaCode . $mobile == $oldAreaCode . $oldMobile) $this->error('修改的手机号和原手机号相同!');
} elseif ($smsType == 3) { //创建子账号
//判断账户是否存在
$id = db('user')->where('mobile', $mobile)->where('area_code', $areaCode)->value('id');
if ($id) {
$this->error('该账户已存在');
}
}
//验证该手机号是否存在,存在则验证状态 状态:0=正常,1=检测中,2=已冻结,3=封号
$user = (new UserModel)->where('mobile', $mobile)->field(['id', 'status'])->find();
$userid = 0;
if ($user) {
if ($user['status'] == 2) $this->error(MSG_USER_STATUS_FREEZE);
if ($user['status'] == 3) $this->error(MSG_USER_STATUS_BAN);
$userid = $user['id'];
}
//判断业务类型
$smsCode = SmsService::sendSmsCode($areaCode, $mobile, $smsType, $userid);
//调试模式时,接口返回短信验证码的值
$res['mobile'] = $mobile;
$res['area_code'] = $areaCode;
if (config('app_debug')) $res['_sms_code'] = $smsCode;
$this->success(MSG_OK, $res, '手机验证码发送成功');
}
3.2、验证短信并登陆
/**
* @ApiTitle (会员手机号注册和登录)
* @ApiSummary (用户用于手机号注册和登录平台)
* @ApiMethod (POST)
* @ApiRoute (/api/User/mobileLogin)
* @ApiParams (name="area_code", type="string", required=true, description="手机国际区号")
* @ApiParams (name="mobile", type="string", required=true, description="手机号")
* @ApiParams (name="sms_code", type="string", required=true, description="手机验证码")
* @ApiParams (name="type", type="int", required=true, description="类型:0=未知设备,1=安卓APP,2=IOSAPP,3=微信小程序,4=H5页面,5=PC端")
*/
public function mobileLogin()
{
$areaCode = input('area_code'); //手机国际区号
$mobile = input('mobile'); //手机号
$smsCode = input('sms_code'); //手机验证码
$type = input('type') ?? 0; //类型:0=未知设备,1=安卓APP,2=IOSAPP,3=微信小程序,4=H5页面,5=PC端
//验证数据
$this->validate([
'mobile' => $mobile,
'sms_code' => $smsCode,
'area_code' => $areaCode,
], 'UserValidate.mobile_login');
//发送短信检测
RedisService::checkSmsCode(areaCode,$mobile, $smsCode, RedisService::SU_SMS_LOGIN);
$userId = (new UserModel)->where('mobile', $mobile)->where('area_code', $areaCode)->value('id');
$ip = request()->ip();
$time = time();
$redis = new Redis();
if (!$userId) {
//如果不存在则直接注册
$data = [
'area_code' => $areaCode, //手机国际区号
'mobile' => $mobile, //手机号
'score' => 0, //积分(考虑第一次注册是否有积分)
'logintime' => $time, //登录时间
'loginip' => $ip, //登录IP
'prevtime' => $time, //上次登录时间
'status' => 0 //状态:0=正常,1=检测中,2=已冻结,3=封号
];
//账号注册时需要开启事务,避免出现垃圾数据
Db::startTrans();
try {
//插入用户表
$userId = (new UserModel)->insertGetId($data);
$rand1 = rand(10000000, 99999999);
$rand2 = rand(10, 99);
$username = 'SuLink-' . substr($rand1 . $userId . $rand2, -11); //取10位数
//再设置用户昵称
(new UserModel)->where('id', $userId)->update([
'nickname' => $username, //昵称
'username' => $username, //用户名
'avatar' => letter_avatar($username)
]);
//插入登录记录
(new UserLoginLogModel)->insert([
'user_id' => $userId,
'type' => $type,
'user_agent' => \request()->header('User-Agent'),
'login_ip' => $ip,
]);
//新增用户设置表
(new UserSettingModel)->insert(['user_id' => $userId]);
//设置Token
$token = Random::uuid();
$redis->set($token, $userId, config('token.keep_time'));
Db::commit();
} catch (Exception $e) {
Db::rollback();
$this->error(MSG_ERR, null, $e->getMessage());
}
} else {
$token = $this->loginSuccess($userId, $type, $time);
}
$this->success(MSG_OK, ['id' => $userId, 'mobile' => $mobile, 'token' => $token], '登录成功');
}
3.3、总结
- 从上面我们可以提取出主要的部分,调用时候主要使用两个部分就行了
1、发送验证码只需要简单的调用短信服务类方法 sendSmsCode
//判断业务类型
$smsCode = SmsService::sendSmsCode($mobile, $smsType, $userid);
2、验证验证码只需要简单的调用Redis服务类方法 checkSmsCode
//发送短信检测
RedisService::checkSmsCode($mobile, $smsCode, RedisService::SU_SMS_LOGIN);
如果觉得那部分不清楚的可以评论发出提问,或者哪里写的不好的也可以提出来。
谢谢大家的观赏,楼上的方法都是本人亲自封装。