新增手机验证码登录

pull/438/head
Karson 2023-03-15 16:37:34 +08:00
parent 9c422d6285
commit a5fd9067c1
14 changed files with 233 additions and 59 deletions

View File

@ -1 +1 @@
<script src="__CDN__/assets/js/require{$Think.config.app_debug?'':'.min'}.js" data-main="__CDN__/assets/js/require-backend{$Think.config.app_debug?'':'.min'}.js?v={$site.version|htmlentities}"></script>
<script src="__CDN__/assets/js/require.min.js" data-main="__CDN__/assets/js/require-backend{$Think.config.app_debug?'':'.min'}.js?v={$site.version|htmlentities}"></script>

View File

@ -30,9 +30,17 @@ class Ems extends Api
public function send()
{
$email = $this->request->post("email");
$captcha = $this->request->post("captcha");
$event = $this->request->post("event");
$event = $event ? $event : 'register';
//发送前验证码
if (config('fastadmin.user_api_captcha')) {
if (!\think\Validate::is($captcha, 'captcha')) {
$this->error("验证码不正确");
}
}
$last = Emslib::get($email, $event);
if ($last && time() - $last['createtime'] < 60) {
$this->error(__('发送频繁'));

View File

@ -25,9 +25,16 @@ class Sms extends Api
public function send()
{
$mobile = $this->request->post("mobile");
$captcha = $this->request->post("captcha");
$event = $this->request->post("event");
$event = $event ? $event : 'register';
//发送前验证码
if (config('fastadmin.user_api_captcha')) {
if (!\think\Validate::is($captcha, 'captcha')) {
$this->error("验证码不正确");
}
}
if (!$mobile || !\think\Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('手机号不正确'));
}

View File

@ -105,7 +105,7 @@ class User extends Api
* @param string $password 密码
* @param string $email 邮箱
* @param string $mobile 手机号
* @param string $code 验证码
* @param string $captcha 验证码
*/
public function register()
{
@ -113,7 +113,8 @@ class User extends Api
$password = $this->request->post('password');
$email = $this->request->post('email');
$mobile = $this->request->post('mobile');
$code = $this->request->post('code');
$captcha = $this->request->post("captcha", $this->request->post('code'));
if (!$username || !$password) {
$this->error(__('Invalid parameters'));
}
@ -123,7 +124,7 @@ class User extends Api
if ($mobile && !Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
$ret = Sms::check($mobile, $code, 'register');
$ret = Sms::check($mobile, $captcha, 'register');
if (!$ret) {
$this->error(__('Captcha is incorrect'));
}

View File

@ -232,6 +232,8 @@ return [
'captcha' => [
// 验证码字符集合
'codeSet' => '2345678abcdefhijkmnpqrstuvwxyzABCDEFGHJKLMNPQRTUVWXY',
// 验证码过期时间s
'expire' => 600,
// 验证码字体大小(px)
'fontSize' => 18,
// 是否画混淆曲线
@ -265,7 +267,9 @@ return [
//是否开启前台会员中心
'usercenter' => true,
//会员注册验证码类型email/mobile/wechat/text/false
'user_register_captcha' => 'text',
'user_register_captcha' => 'mobile',
//是否启用发送前验证码(用于短信和邮件发送)
'user_api_captcha' => true,
//登录验证码
'login_captcha' => true,
//登录失败超过10次则1天后重试

View File

@ -7,6 +7,7 @@ use app\common\controller\Frontend;
use app\common\library\Ems;
use app\common\library\Sms;
use app\common\model\Attachment;
use fast\Random;
use think\Config;
use think\Cookie;
use think\Hook;
@ -19,7 +20,7 @@ use think\Validate;
class User extends Frontend
{
protected $layout = 'default';
protected $noNeedLogin = ['login', 'register', 'third'];
protected $noNeedLogin = ['login', 'mobilelogin', 'register', 'third'];
protected $noNeedRight = ['*'];
public function _initialize()
@ -65,7 +66,7 @@ class User extends Frontend
*/
public function register()
{
$url = $this->request->request('url', '', 'trim');
$url = $this->request->request('url', '', 'trim|xss_clean');
if ($this->auth->id) {
$this->success(__('You\'ve logged in, do not login again'), $url ? $url : url('user/index'));
}
@ -144,7 +145,7 @@ class User extends Frontend
*/
public function login()
{
$url = $this->request->request('url', '', 'trim');
$url = $this->request->request('url', '', 'trim|xss_clean');
if ($this->auth->id) {
$this->success(__('You\'ve logged in, do not login again'), $url ? $url : url('user/index'));
}
@ -193,6 +194,53 @@ class User extends Frontend
return $this->view->fetch();
}
/**
* 手机号验证码登录
*/
public function mobilelogin()
{
$url = $this->request->request('url', '', 'trim|xss_clean');
if ($this->request->isPost()) {
$mobile = $this->request->post('mobile');
$captcha = $this->request->post('captcha');
if (!$mobile || !$captcha) {
$this->error(__('Invalid parameters'));
}
if (!Validate::regex($mobile, "^1\d{10}$")) {
$this->error(__('Mobile is incorrect'));
}
if (!Sms::check($mobile, $captcha, 'mobilelogin')) {
$this->error(__('Captcha is incorrect'));
}
$user = \app\common\model\User::getByMobile($mobile);
if ($user) {
if ($user->status != 'normal') {
$this->error(__('Account is locked'));
}
//如果已经有账号则直接登录
$ret = $this->auth->direct($user->id);
} else {
$ret = $this->auth->register($mobile, Random::alnum(), '', $mobile, []);
}
if ($ret) {
Sms::flush($mobile, 'mobilelogin');
$data = ['userinfo' => $this->auth->getUserinfo()];
$this->success(__('Logged in successful'), $url);
} else {
$this->error($this->auth->getError());
}
}
//判断来源
$referer = $this->request->server('HTTP_REFERER');
if (!$url && (strtolower(parse_url($referer, PHP_URL_HOST)) == strtolower($this->request->host()))
&& !preg_match("/(user\/login|user\/register|user\/logout)/i", $referer)) {
$url = $referer;
}
$this->view->assign('url', $url);
$this->view->assign('title', __('Login'));
return $this->view->fetch();
}
/**
* 退出登录
*/

View File

@ -19,8 +19,12 @@ return [
'Change' => '修改',
'Click to edit' => '点击编辑',
'Email/Mobile/Username' => '邮箱/手机/用户名',
'Sign in with account' => '使用账号密码登录',
'Sign in with mobile phone' => '使用手机验证码登录',
'Sign up successful' => '注册成功',
'Email active successful' => '邮箱激活成功',
'Please enter your mobile phone number' => '请输入你的手机号',
'Please enter %s numbers' => '请输入%s位数字',
'Username can not be empty' => '用户名不能为空',
'Username must be 3 to 30 characters' => '用户名必须3-30个字符',
'Username must be 6 to 30 characters' => '用户名必须6-30个字符',
@ -61,7 +65,7 @@ return [
'Logout successful' => '退出成功',
'User center already closed' => '会员中心已经关闭',
'Don\'t have an account? Sign up' => '还没有账号?点击注册',
'Already have an account? Sign in' => '已经有账号?点击登录',
'Already have an account? Sign in' => '已经有账号?点击登录',
'Operation failed' => '操作失败',
'Invalid parameters' => '参数不正确',
'Change password failure' => '修改密码失败',

View File

@ -1,19 +1,19 @@
<!--@formatter:off-->
{if "[type]" == 'email'}
<input type="text" name="captcha" class="form-control" data-rule="required;length({$Think.config.captcha.length});digits;remote({:url('api/validate/check_ems_correct')}, event=[event], email:#email)" />
<input type="text" name="captcha" class="form-control" placeholder="{:__('Please enter %s numbers', $Think.config.captcha.length)}" data-rule="required;length({$Think.config.captcha.length});digits;remote({:url('api/validate/check_ems_correct')}, event=[event], email:#email)" />
<span class="input-group-btn" style="padding:0;border:none;">
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:url('api/ems/send')}" data-type="email" data-event="[event]">发送验证码</a>
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:url('api/ems/send')}" data-type="email" data-event="[event]">{:__('Send verification code')}</a>
</span>
{elseif "[type]" == 'mobile'/}
<input type="text" name="captcha" class="form-control" data-rule="required;length({$Think.config.captcha.length});digits;remote({:url('api/validate/check_sms_correct')}, event=[event], mobile:#mobile)" />
<input type="text" name="captcha" class="form-control" placeholder="{:__('Please enter %s numbers', $Think.config.captcha.length)}" data-rule="required;length({$Think.config.captcha.length});digits;remote({:url('api/validate/check_sms_correct')}, event=[event], mobile:#mobile)" />
<span class="input-group-btn" style="padding:0;border:none;">
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:url('api/sms/send')}" data-type="mobile" data-event="[event]">发送验证码</a>
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:url('api/sms/send')}" data-type="mobile" data-event="[event]">{:__('Send verification code')}</a>
</span>
{elseif "[type]" == 'wechat'/}
{if get_addon_info('wechat')}
<input type="text" name="captcha" class="form-control" data-rule="required;length({$Think.config.captcha.length});remote({:addon_url('wechat/captcha/check')}, event=[event])" />
<span class="input-group-btn" style="padding:0;border:none;">
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:addon_url('wechat/captcha/send')}" data-type="wechat" data-event="[event]">获取验证码</a>
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:addon_url('wechat/captcha/send')}" data-type="wechat" data-event="[event]">{:__('Send verification code')}</a>
</span>
{else/}
请在后台插件管理中安装《微信管理插件》
@ -24,4 +24,4 @@
<img src="{:captcha_src()}" width="100" height="32" onclick="this.src = '{:captcha_src()}?r=' + Math.random();"/>
</span>
{/if}
<!--@formatter:on-->
<!--@formatter:on-->

View File

@ -1 +1,24 @@
<script src="__CDN__/assets/js/require{$Think.config.app_debug?'':'.min'}.js" data-main="__CDN__/assets/js/require-frontend{$Think.config.app_debug?'':'.min'}.js?v={$site.version|htmlentities}"></script>
{if $Think.config.fastadmin.user_api_captcha}
<script type="text/html" id="captchatpl">
<div class="p-4 form-section">
<form name="captcha-form" class="form-vertical" action="" method="post">
<div class="form-group mb-4">
<label class="control-label hidden">{:__('Captcha')}</label>
<div class="controls">
<div class="input-group">
<input type="text" name="captcha" class="form-control" data-rule="required;length({$Think.config.captcha.length})" />
<span class="input-group-btn" style="padding:0;border:none;">
<img src="{:captcha_src()}" width="107" height="32" onclick="this.src = '{:captcha_src()}?r=' + Math.random();"/>
</span>
</div>
<span class="msg-box n-right" style="left:0;top:33px;text-align: left;" for="captcha"></span>
</div>
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" type="submit">{:__('Submit')}</button>
</div>
</form>
</div>
</script>
{/if}
<script src="__CDN__/assets/js/require.min.js" data-main="__CDN__/assets/js/require-frontend{$Think.config.app_debug?'':'.min'}.js?v={$site.version|htmlentities}"></script>

View File

@ -1,19 +1,40 @@
<div id="content-container" class="container">
<div class="user-section login-section">
<div class="logon-tab clearfix"><a class="active">{:__('Sign in')}</a> <a href="{:url('user/register')}?url={$url|urlencode|htmlentities}">{:__('Sign up')}</a></div>
<div class="logon-tab clearfix"><a class="active">{:__('Sign in')}</a> <a href="{:url('user/register')}?url={$url|xss_clean|urlencode}">{:__('Sign up')}</a></div>
<div class="login-main">
<form name="form" id="login-form" class="form-vertical" method="POST" action="">
<form name="form" id="login-form" class="form-vertical" method="POST" action="{:url('user/mobilelogin')}">
<!--@IndexLoginFormBegin-->
<input type="hidden" name="url" value="{$url|htmlentities}"/>
{:token()}
<div class="form-group">
<div class="form-group" data-login="mobile">
<label class="control-label">{:__('Mobile')}</label>
<div class="controls">
<input type="text" id="mobile" name="mobile" data-rule="required;mobile" class="form-control" placeholder="{:__('Please enter your mobile phone number')}">
<p class="help-block"></p>
</div>
</div>
<div class="form-group" data-login="mobile">
<label class="control-label">{:__('Captcha')}</label>
<div class="controls">
<div class="input-group">
<input type="text" name="captcha" class="form-control" placeholder="{:__('Please enter %s numbers', $Think.config.captcha.length)}" maxlength="{$Think.config.captcha.length}" data-rule="required;length({$Think.config.captcha.length});integer[+]"/>
<span class="input-group-btn" style="padding:0;border:none;">
<a href="javascript:;" class="btn btn-info btn-captcha" data-url="{:url('api/sms/send')}" data-type="mobile" data-event="mobilelogin">{:__('Send verification code')}</a>
</span>
</div>
<p class="help-block"></p>
</div>
</div>
<div class="form-group hidden" data-login="account">
<label class="control-label" for="account">{:__('Account')}</label>
<div class="controls">
<input class="form-control" id="account" type="text" name="account" value="" data-rule="required" placeholder="{:__('Email/Mobile/Username')}" autocomplete="off">
<div class="help-block"></div>
</div>
</div>
<div class="form-group">
<div class="form-group hidden" data-login="account">
<label class="control-label" for="password">{:__('Password')}</label>
<div class="controls">
<input class="form-control" id="password" type="password" name="password" data-rule="required;password" placeholder="{:__('Password')}" autocomplete="off">
@ -31,7 +52,18 @@
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-lg btn-block">{:__('Sign in')}</button>
<a href="{:url('user/register')}?url={$url|urlencode|htmlentities}" class="btn btn-default btn-lg btn-block mt-3 no-border">{:__("Don't have an account? Sign up")}</a>
<div class="row">
<div class="col-xs-6 text-left py-2">
<a href="javascript:" class="btn-switchlogin" data-type="mobile"
data-account-action="{:url('user/login')}"
data-mobile-action="{:url('user/mobilelogin')}"
data-account-text='{:__("Sign in with account")}'
data-mobile-text='{:__("Sign in with mobile phone")}'>{:__("Sign in with account")}</a>
</div>
<div class="col-xs-6 text-right py-2">
<a href="{:url('user/register')}?url={$url|xss_clean|urlencode}">{:__("Don't have an account? Sign up")}</a>
</div>
</div>
</div>
<!--@IndexLoginFormEnd-->
</form>

View File

@ -1,11 +1,11 @@
<div id="content-container" class="container">
<div class="user-section login-section">
<div class="logon-tab clearfix"><a href="{:url('user/login')}?url={$url|urlencode|htmlentities}">{:__('Sign in')}</a> <a class="active">{:__('Sign up')}</a></div>
<div class="logon-tab clearfix"><a href="{:url('user/login')}?url={$url|xss_clean|urlencode}">{:__('Sign in')}</a> <a class="active">{:__('Sign up')}</a></div>
<div class="login-main">
<form name="form1" id="register-form" class="form-vertical" method="POST" action="">
<!--@IndexRegisterFormBegin-->
<input type="hidden" name="invite_user_id" value="0"/>
<input type="hidden" name="url" value="{$url|htmlentities}"/>
<input type="hidden" name="url" value="{$url|xss_clean}"/>
{:token()}
<div class="form-group">
<label class="control-label required">{:__('Email')}<span class="text-success"></span></label>
@ -31,7 +31,7 @@
<div class="form-group">
<label class="control-label">{:__('Mobile')}</label>
<div class="controls">
<input type="text" id="mobile" name="mobile" data-rule="required;mobile" class="form-control" placeholder="{:__('Mobile')}">
<input type="text" id="mobile" name="mobile" data-rule="required;mobile" class="form-control" placeholder="{:__('Mobile')}" value="13241111111">
<p class="help-block"></p>
</div>
</div>
@ -52,7 +52,7 @@
<div class="form-group">
<button type="submit" class="btn btn-primary btn-lg btn-block">{:__('Sign up')}</button>
<a href="{:url('user/login')}?url={$url|urlencode|htmlentities}" class="btn btn-default btn-lg btn-block mt-3 no-border">{:__('Already have an account? Sign in')}</a>
<a href="{:url('user/login')}?url={$url|xss_clean|urlencode}" class="btn btn-default btn-lg btn-block mt-3 no-border">{:__('Already have an account? Sign in')}</a>
</div>
<!--@IndexRegisterFormEnd-->
</form>

View File

@ -173,7 +173,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
var iconfunc = function () {
Layer.open({
type: 1,
area: ['99%', '98%'], //宽高
area: ['80%', '80%'], //宽高
content: Template('chooseicontpl', {iconlist: iconlist})
});
};
@ -185,8 +185,8 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'template'], function
});
$(document).on('click', ".btn-search-icon", function () {
if (iconlist.length == 0) {
$.get(Config.site.cdnurl + "/assets/libs/font-awesome/less/variables.less", function (ret) {
var exp = /fa-var-(.*):/ig;
$.get(Config.site.cdnurl + "/assets/libs/font-awesome/css/font-awesome.css", function (ret) {
var exp = /fa-(.*):before/ig;
var result;
while ((result = exp.exec(ret)) != null) {
iconlist.push(result[1]);

View File

@ -1,34 +1,59 @@
define(['fast', 'template', 'moment'], function (Fast, Template, Moment) {
var Frontend = {
api: Fast.api,
init: function () {
var si = {};
api: {
//发送验证码
sendcaptcha: function (btn, type, data, callback) {
$(btn).addClass("disabled", true).text("发送中...");
var si = {};
Frontend.api.ajax({url: $(btn).data("url"), data: data}, function (data, ret) {
clearInterval(si[type]);
var seconds = 60;
si[type] = setInterval(function () {
seconds--;
if (seconds <= 0) {
clearInterval(si);
$(btn).removeClass("disabled").text("发送验证码");
} else {
$(btn).addClass("disabled").text(seconds + "秒后可再次发送");
}
}, 1000);
if (typeof callback == 'function') {
callback.call(this, data, ret);
}
}, function () {
$(btn).removeClass("disabled").text('发送验证码');
});
},
//准备验证码
preparecaptcha: function (btn, type, data) {
require(['form'], function (Form) {
Layer.open({
title: '请完成验证',
type: 1,
content: Template("captchatpl", {}),
btnAlign: 'c',
success: function (layero, index) {
var form = $("form", layero);
form.data("validator-options", {
valid: function (ret) {
data.captcha = $("input[name=captcha]", form).val();
Frontend.api.sendcaptcha(btn, type, data, function (data, ret) {
Layer.close(index);
});
return true;
}
})
Form.api.bindevent(form);
}
});
});
}
},
init: function () {
//点击发送验证码
$(document).on("click", ".btn-captcha", function (e) {
var type = $(this).data("type") ? $(this).data("type") : 'mobile';
var btn = this;
Frontend.api.sendcaptcha = function (btn, type, data, callback) {
$(btn).addClass("disabled", true).text("发送中...");
Frontend.api.ajax({url: $(btn).data("url"), data: data}, function (data, ret) {
clearInterval(si[type]);
var seconds = 60;
si[type] = setInterval(function () {
seconds--;
if (seconds <= 0) {
clearInterval(si);
$(btn).removeClass("disabled").text("发送验证码");
} else {
$(btn).addClass("disabled").text(seconds + "秒后可再次发送");
}
}, 1000);
if (typeof callback == 'function') {
callback.call(this, data, ret);
}
}, function () {
$(btn).removeClass("disabled").text('发送验证码');
});
};
if (['mobile', 'email'].indexOf(type) > -1) {
var element = $(this).data("input-id") ? $("#" + $(this).data("input-id")) : $("input[name='" + type + "']", $(this).closest("form"));
var text = type === 'email' ? '邮箱' : '手机号码';
@ -47,9 +72,13 @@ define(['fast', 'template', 'moment'], function (Fast, Template, Moment) {
}
element.isValid(function (v) {
if (v) {
var data = {event: $(btn).data("event")};
var data = {event: $(btn).data("event"), id: $(btn).data("id")};
data[type] = element.val();
Frontend.api.sendcaptcha(btn, type, data);
if ($("#captchatpl").length === 0) {
Frontend.api.sendcaptcha(btn, type, data);
} else {
Frontend.api.preparecaptcha(btn, type, data);
}
} else {
Layer.msg("请确认已经输入了正确的" + text + "");
}
@ -57,7 +86,12 @@ define(['fast', 'template', 'moment'], function (Fast, Template, Moment) {
} else {
var data = {event: $(btn).data("event")};
Frontend.api.sendcaptcha(btn, type, data, function (data, ret) {
Layer.open({title: false, area: ["400px", "430px"], content: "<img src='" + data.image + "' width='400' height='400' /><div class='text-center panel-title'>扫一扫关注公众号获取验证码</div>", type: 1});
Layer.open({
title: false,
area: [Math.min($(window).width(), 400) + "px", "430px"],
content: "<img src='" + data.image + "' width='400' height='400' /><div class='text-center panel-title'>扫一扫关注公众号获取验证码</div>",
type: 1
});
});
}
return false;

View File

@ -4,7 +4,8 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
$.each(errors, function (i, j) {
Layer.msg(j);
});
}
},
ignore: ':hidden'
};
var Controller = {
login: function () {
@ -26,7 +27,7 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
Layer.open({
type: 1,
title: __('Reset password'),
area: ["450px", "355px"],
area: [Math.min($(window).width(), 450) + "px", "355px"],
content: content,
success: function (layero) {
var rule = $("#resetpwd-form input[name='captcha']").data("rule");
@ -45,6 +46,18 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
}
});
});
//切换账号手机验证码登录
$(document).on("click", ".btn-switchlogin", function () {
var form = $(this).closest("form");
var current = $(this).data("type");
var type = current == 'mobile' ? 'account' : 'mobile';
var text = $(this).data(current + "-text");
$(this).text(text).data("type", type);
$("[data-login]").addClass("hidden");
$("[data-login='"+type+"']").removeClass("hidden");
form.attr("action", $(this).data(type + "-action"));
});
},
register: function () {
//本地验证未通过时提示
@ -85,7 +98,7 @@ define(['jquery', 'bootstrap', 'frontend', 'form', 'template'], function ($, und
Layer.open({
type: 1,
title: "修改",
area: ["400px", "250px"],
area: [Math.min($(window).width(), 400) + "px", "250px"],
content: content,
success: function (layero) {
var form = $("form", layero);