!31 增加功能:增加手机短信验证码登录
This commit is contained in:
parent
c3bbe488ea
commit
7a75f9ec68
|
@ -2,6 +2,7 @@
|
|||
using Bootstrap.DataAccess;
|
||||
using Longbow.GiteeAuth;
|
||||
using Longbow.GitHubAuth;
|
||||
using Longbow.Security.Cryptography;
|
||||
using Longbow.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
@ -64,6 +65,50 @@ namespace Bootstrap.Admin.Controllers
|
|||
return User.Identity.IsAuthenticated ? (ActionResult)Redirect("~/Home/Index") : View("Login", new LoginModel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 短信验证登陆方法
|
||||
/// </summary>
|
||||
/// <param name="onlineUserSvr"></param>
|
||||
/// <param name="ipLocator"></param>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="phone"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> Mobile([FromServices]IOnlineUsers onlineUserSvr, [FromServices]IIPLocatorProvider ipLocator, [FromServices]IConfiguration configuration, [FromQuery]string phone, [FromQuery]string code)
|
||||
{
|
||||
var option = configuration.GetSection(nameof(SMSOptions)).Get<SMSOptions>();
|
||||
if (UserHelper.AuthenticateMobile(phone, code, option.MD5Key, loginUser => CreateLoginUser(onlineUserSvr, ipLocator, HttpContext, loginUser)))
|
||||
{
|
||||
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
identity.AddClaim(new Claim(ClaimTypes.Name, phone));
|
||||
identity.AddClaim(new Claim(ClaimTypes.Role, "Default"));
|
||||
await HttpContext.SignInAsync(new ClaimsPrincipal(identity));
|
||||
|
||||
if (UserHelper.RetrieveUserByUserName(identity) == null)
|
||||
{
|
||||
var user = new User()
|
||||
{
|
||||
ApprovedBy = "Mobile",
|
||||
ApprovedTime = DateTime.Now,
|
||||
DisplayName = "手机用户",
|
||||
UserName = phone,
|
||||
Password = LgbCryptography.GenerateSalt(),
|
||||
Icon = "default.jpg",
|
||||
Description = "手机用户",
|
||||
App = "2"
|
||||
};
|
||||
UserHelper.Save(user);
|
||||
CacheCleanUtility.ClearCache(cacheKey: UserHelper.RetrieveUsersDataKey);
|
||||
}
|
||||
|
||||
// redirect origin url
|
||||
var originUrl = Request.Query[CookieAuthenticationDefaults.ReturnUrlParameter].FirstOrDefault() ?? "~/Home/Index";
|
||||
return Redirect(originUrl);
|
||||
}
|
||||
return View("Login", new LoginModel() { AuthFailed = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Login the specified userName, password and remember.
|
||||
/// </summary>
|
||||
|
|
|
@ -5,15 +5,15 @@ using Longbow.Web;
|
|||
using Longbow.Web.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bootstrap.Admin.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
///
|
||||
/// 登陆接口
|
||||
/// </summary>
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
|
@ -28,7 +28,7 @@ namespace Bootstrap.Admin.Controllers.Api
|
|||
public QueryData<LoginUser> Get([FromQuery]QueryLoginOption value) => value.RetrieveData();
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// JWT 登陆认证接口
|
||||
/// </summary>
|
||||
/// <param name="onlineUserSvr"></param>
|
||||
/// <param name="ipLocator"></param>
|
||||
|
@ -50,7 +50,25 @@ namespace Bootstrap.Admin.Controllers.Api
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// 下发手机短信方法
|
||||
/// </summary>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="factory"></param>
|
||||
/// <param name="phone"></param>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
[HttpPut]
|
||||
public async Task<bool> Put([FromServices]IConfiguration configuration, [FromServices]IHttpClientFactory factory, [FromQuery]string phone)
|
||||
{
|
||||
if (string.IsNullOrEmpty(phone)) return false;
|
||||
|
||||
var option = configuration.GetSection(nameof(SMSOptions)).Get<SMSOptions>();
|
||||
option.Phone = phone;
|
||||
return await factory.CreateClient().SendCode(option);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 跨域握手协议
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
|
|
|
@ -47,30 +47,58 @@
|
|||
<h2 class="form-signin-heading">@Model.Title</h2>
|
||||
<div class="login-wrap" data-auth="@Model.AuthFailed" data-toggle="LgbValidate" data-valid-button="button[type='submit']">
|
||||
<div class="alert alert-danger d-none" asp-condition="@Model.AuthFailed">用户名或密码错误!</div>
|
||||
<div class="form-group">
|
||||
<div id="loginUser" class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-user"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" name="userName" class="form-control" placeholder="用户名" maxlength="16" data-required-msg="请输入用户名" value="" autofocus data-valid="true" />
|
||||
<input type="text" name="userName" class="form-control" data-toggle="tooltip" placeholder="用户名" maxlength="16" data-required-msg="请输入用户名" value="" autofocus data-valid="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div id="loginMobile" class="form-group d-none">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-user"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="txtPhone" class="form-control digits" data-toggle="tooltip" placeholder="手机号码" minlength="11" maxlength="11" data-required-msg="请输入手机号码" value="" data-valid="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="loginPwd" class="form-group">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-lock"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="password" name="password" class="form-control" value="" placeholder="密码" maxlength="16" data-required-msg="请输入密码" data-valid="true" />
|
||||
<input type="password" name="password" class="form-control" value="" data-toggle="tooltip" placeholder="密码" maxlength="16" data-required-msg="请输入密码" data-valid="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group rememberPwd" onselectstart="return false">
|
||||
<i class="fa fa-square-o"></i>
|
||||
<span>记住密码自动登录</span>
|
||||
<input id="remember" name="remember" type="hidden" value="false" />
|
||||
<div id="loginSMS" class="form-group d-none">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<div class="input-group-text">
|
||||
<span class="fa fa-lock"></span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="smscode" class="form-control digits" data-toggle="tooltip" disabled value="" placeholder="验证码" maxlength="4" data-required-msg="请输入验证码" data-valid="true" />
|
||||
<div class="input-group-append">
|
||||
<button type="button" id="btnSendCode" class="btn btn-sms" data-toggle="tooltip" title="点击发送验证码">发送验证码</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="form-group rememberPwd" onselectstart="return false">
|
||||
<i class="fa fa-square-o"></i>
|
||||
<span>记住密码自动登录</span>
|
||||
<input id="remember" name="remember" type="hidden" value="false" />
|
||||
</div>
|
||||
<div>
|
||||
<a id="loginType" data-value="username" href="#" class="">短信验证登陆</a>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-login btn-block" data-toggle="tooltip" title="不填写密码默认使用 Gitee 认证" type="submit">登 录</button>
|
||||
<div class="login-footer">
|
||||
|
|
|
@ -82,6 +82,10 @@
|
|||
"App": "0",
|
||||
"StarredUrl": "https://api.github.com/user/starred/ArgoZhang/BootstrapAdmin"
|
||||
},
|
||||
"SMSOptions": {
|
||||
"CompanyCode": "<CompanyCode>",
|
||||
"MD5Key": "MD5Key"
|
||||
},
|
||||
"LongbowCache": {
|
||||
"Enabled": true,
|
||||
"CorsItems": [
|
||||
|
|
|
@ -86,6 +86,10 @@
|
|||
"Roles": [ "Default" ],
|
||||
"App": "2"
|
||||
},
|
||||
"SMSOptions": {
|
||||
"CompanyCode": "<CompanyCode>",
|
||||
"MD5Key": "MD5Key"
|
||||
},
|
||||
"LongbowCache": {
|
||||
"Enabled": true,
|
||||
"CorsItems": [
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
box-shadow: 0 4px #e56b60;
|
||||
margin-bottom: 0.625rem;
|
||||
outline: none !important;
|
||||
margin-top: -0.5rem;
|
||||
margin-top: -0.25rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
|
@ -57,8 +57,8 @@
|
|||
border-color: #1ca0e9;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
border-color: #1ca0e9;
|
||||
.form-control, .input-group-append .btn {
|
||||
border-color: #1ca0e9 !important;
|
||||
}
|
||||
|
||||
.slidercaptcha {
|
||||
|
@ -70,6 +70,7 @@
|
|||
right: 0;
|
||||
top: -270px;
|
||||
height: 226px;
|
||||
z-index: 1080
|
||||
}
|
||||
|
||||
.slidercaptcha canvas:first-child {
|
||||
|
@ -144,3 +145,7 @@
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.login-wrap .btn-sms {
|
||||
width: 140px;
|
||||
}
|
||||
|
|
|
@ -119,13 +119,97 @@
|
|||
|
||||
// use Gitee authentication when SystemDemoModel
|
||||
var $login = $('#login');
|
||||
var $username = $('[name="userName"]');
|
||||
var $password = $('[name="password"]');
|
||||
var $loginUser = $('#loginUser');
|
||||
var $loginMobile = $('#loginMobile');
|
||||
var $loginPwd = $('#loginPwd');
|
||||
var $loginSMS = $('#loginSMS');
|
||||
if ($login.attr('data-demo') === 'True') {
|
||||
$login.find('[data-valid="true"]').attr('data-valid', 'false');
|
||||
$login.on('submit', function (e) {
|
||||
if ($('[name="userName"]').val() === '' && $('[name="password"]').val() === '') {
|
||||
location.href = "Gitee";
|
||||
e.preventDefault();
|
||||
var model = $loginType.attr('data-value');
|
||||
if (model === 'username') {
|
||||
$login.find('[data-valid="true"]').attr('data-valid', 'false');
|
||||
if ($username.val() === '' && $password.val() === '') {
|
||||
e.preventDefault();
|
||||
location.href = "Gitee";
|
||||
}
|
||||
}
|
||||
else {
|
||||
// sms
|
||||
var url = $.format('Account/Mobile?phone={0}&code={1}', $('#txtPhone').val(), $('#smscode').val());
|
||||
$login.attr('action', $.formatUrl(url));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// login type
|
||||
var $loginType = $('#loginType').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
var $this = $(this);
|
||||
$login.find('[data-toggle="tooltip"]').tooltip('hide');
|
||||
var model = $this.attr('data-value');
|
||||
if (model === 'username') {
|
||||
$loginUser.addClass('d-none');
|
||||
$loginPwd.addClass('d-none');
|
||||
$loginSMS.removeClass('d-none');
|
||||
$loginMobile.removeClass('d-none');
|
||||
|
||||
$this.attr('data-value', 'sms').text('用户名密码登陆');
|
||||
}
|
||||
else {
|
||||
// sms model
|
||||
$loginUser.removeClass('d-none');
|
||||
$loginPwd.removeClass('d-none');
|
||||
$loginSMS.addClass('d-none');
|
||||
$loginMobile.addClass('d-none');
|
||||
|
||||
$this.attr('data-value', 'username').text('短信验证登陆');
|
||||
}
|
||||
});
|
||||
|
||||
var timeHanlder = null;
|
||||
$('#btnSendCode').on('click', function () {
|
||||
// validate mobile phone
|
||||
var $phone = $('#txtPhone');
|
||||
var validator = $login.find('[data-toggle="LgbValidate"]').lgbValidator();
|
||||
if (!validator.validElement($phone.get(0))) {
|
||||
$phone.tooltip('show');
|
||||
return;
|
||||
}
|
||||
|
||||
var phone = $phone.val();
|
||||
var apiUrl = "api/Login?phone=" + phone;
|
||||
var $this = $(this);
|
||||
$.bc({
|
||||
url: apiUrl,
|
||||
method: 'PUT',
|
||||
callback: function (result) {
|
||||
$this.attr('data-original-title', result ? "发送成功" : "发送失败").tooltip('show');
|
||||
var handler = setTimeout(function () {
|
||||
clearTimeout(handler);
|
||||
$this.tooltip('hide').attr('data-original-title', "点击发送验证码");
|
||||
}, 1500);
|
||||
|
||||
if (result) {
|
||||
// send success
|
||||
$this.text('已发送').attr('disabled', true);
|
||||
$('#smscode').removeAttr('disabled');
|
||||
timeHanlder = setTimeout(function () {
|
||||
clearTimeout(timeHanlder);
|
||||
var count = 299;
|
||||
timeHanlder = setInterval(function () {
|
||||
if (count === 0) {
|
||||
clearInterval(timeHanlder);
|
||||
$this.text('发送验证码').removeAttr('disabled');
|
||||
return;
|
||||
}
|
||||
$this.text(count-- + ' 秒后可重发');
|
||||
}, 1000);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,157 @@
|
|||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Bootstrap.DataAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// 手机号登陆帮助类
|
||||
/// </summary>
|
||||
public static class SMSHelper
|
||||
{
|
||||
private static ConcurrentDictionary<string, AutoExpireValidateCode> _pool = new ConcurrentDictionary<string, AutoExpireValidateCode>();
|
||||
|
||||
/// <summary>
|
||||
/// 下发验证码方法
|
||||
/// </summary>
|
||||
/// <param name="client"></param>
|
||||
/// <param name="option"></param>
|
||||
/// <returns></returns>
|
||||
public static async System.Threading.Tasks.Task<bool> SendCode(this HttpClient client, SMSOptions option)
|
||||
{
|
||||
option.Timestamp = (DateTimeOffset.UtcNow.Ticks - 621355968000000000) / 10000000;
|
||||
var requestParameters = new Dictionary<string, string>()
|
||||
{
|
||||
{ "CompanyCode", option.CompanyCode },
|
||||
{ "Phone", option.Phone },
|
||||
{ "TimeStamp", option.Timestamp.ToString() },
|
||||
{ "Sign", option.Sign() }
|
||||
};
|
||||
|
||||
var url = QueryHelpers.AddQueryString("http://open.bluegoon.com/api/sms/sendcode", requestParameters);
|
||||
var req = await client.GetAsync(url);
|
||||
var result = JsonConvert.DeserializeObject<SMSResult>(await req.Content.ReadAsStringAsync());
|
||||
var ret = false;
|
||||
if (result.Code == "1")
|
||||
{
|
||||
_pool.AddOrUpdate(option.Phone, key => new AutoExpireValidateCode(option.Phone, result.Data, option.Expires), (key, v) => v.Reset(result.Data));
|
||||
ret = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证验证码方法
|
||||
/// </summary>
|
||||
/// <param name="phone">手机号</param>
|
||||
/// <param name="code">验证码</param>
|
||||
/// <param name="secret">密钥</param>
|
||||
/// <returns></returns>
|
||||
public static bool Validate(string phone, string code, string secret) => _pool.TryGetValue(phone, out var signKey) && Hash($"{code}{secret}") == signKey.Code;
|
||||
|
||||
private static string Sign(this SMSOptions option)
|
||||
{
|
||||
return Hash($"{option.CompanyCode}{option.Phone}{option.Timestamp}{option.MD5Key}");
|
||||
}
|
||||
|
||||
private static string Hash(string data)
|
||||
{
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
var sign = BitConverter.ToString(md5.ComputeHash(Encoding.UTF8.GetBytes(data)));
|
||||
sign = sign.Replace("-", "").ToLowerInvariant();
|
||||
return sign;
|
||||
}
|
||||
}
|
||||
|
||||
private class SMSResult
|
||||
{
|
||||
public string Code { get; set; }
|
||||
|
||||
public string Data { get; set; }
|
||||
}
|
||||
|
||||
private class AutoExpireValidateCode
|
||||
{
|
||||
public AutoExpireValidateCode(string phone, string code, TimeSpan expires)
|
||||
{
|
||||
Phone = phone;
|
||||
Code = code;
|
||||
Expires = expires;
|
||||
RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Code { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public string Phone { get; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public TimeSpan Expires { get; set; }
|
||||
|
||||
private CancellationTokenSource _tokenSource;
|
||||
|
||||
private System.Threading.Tasks.Task RunAsync() => System.Threading.Tasks.Task.Run(() =>
|
||||
{
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
if (!_tokenSource.Token.WaitHandle.WaitOne(Expires)) _pool.TryRemove(Phone, out var _);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
public AutoExpireValidateCode Reset(string code)
|
||||
{
|
||||
Code = code;
|
||||
_tokenSource.Cancel();
|
||||
RunAsync();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 短信网关配置类
|
||||
/// </summary>
|
||||
public class SMSOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获得/设置 公司编码
|
||||
/// </summary>
|
||||
public string CompanyCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 下发手机号码
|
||||
/// </summary>
|
||||
public string Phone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 签名密钥
|
||||
/// </summary>
|
||||
public string MD5Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 时间戳
|
||||
/// </summary>
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 验证码有效时长
|
||||
/// </summary>
|
||||
public TimeSpan Expires { get; set; } = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
}
|
|
@ -58,9 +58,9 @@ namespace Bootstrap.DataAccess
|
|||
/// </summary>
|
||||
/// <param name="userName"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <param name="config"></param>
|
||||
/// <param name="configure"></param>
|
||||
/// <returns>返回真表示认证通过</returns>
|
||||
public static bool Authenticate(string userName, string password, Action<LoginUser> config)
|
||||
public static bool Authenticate(string userName, string password, Action<LoginUser> configure)
|
||||
{
|
||||
if (!UserChecker(new User { UserName = userName, Password = password })) return false;
|
||||
var loginUser = new LoginUser
|
||||
|
@ -69,13 +69,36 @@ namespace Bootstrap.DataAccess
|
|||
LoginTime = DateTime.Now,
|
||||
Result = "登录失败"
|
||||
};
|
||||
config(loginUser);
|
||||
configure(loginUser);
|
||||
var ret = string.IsNullOrEmpty(userName) ? false : DbContextManager.Create<User>().Authenticate(userName, password);
|
||||
if (ret) loginUser.Result = "登录成功";
|
||||
LoginHelper.Log(loginUser);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 短信验证码认证方法
|
||||
/// </summary>
|
||||
/// <param name="phone"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="secret"></param>
|
||||
/// <param name="configure"></param>
|
||||
/// <returns></returns>
|
||||
public static bool AuthenticateMobile(string phone, string code, string secret, Action<LoginUser> configure)
|
||||
{
|
||||
var loginUser = new LoginUser
|
||||
{
|
||||
UserName = phone,
|
||||
LoginTime = DateTime.Now,
|
||||
Result = "登录失败"
|
||||
};
|
||||
configure(loginUser);
|
||||
var ret = string.IsNullOrEmpty(phone) ? false : SMSHelper.Validate(phone, code, secret);
|
||||
if (ret) loginUser.Result = "登录成功";
|
||||
LoginHelper.Log(loginUser);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询所有的新注册用户
|
||||
/// </summary>
|
||||
|
|
Loading…
Reference in New Issue