refactor: 短信验证码登录方式使用注入服务

This commit is contained in:
Argo Zhang 2019-10-08 16:37:17 +08:00
parent 235614e0b9
commit f271b2402c
No known key found for this signature in database
GPG Key ID: 152E398953DDF19F
7 changed files with 135 additions and 69 deletions

View File

@ -11,7 +11,9 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Claims;
@ -49,18 +51,18 @@ namespace Bootstrap.Admin.Controllers
/// <summary>
/// 系统锁屏界面
/// </summary>
/// <param name="configuration"></param>
/// <param name="provider"></param>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <param name="authType"></param>
/// <returns></returns>
[HttpPost]
[IgnoreAntiforgeryToken]
public Task<IActionResult> Lock([FromServices]IConfiguration configuration, string userName, string password, string authType)
public Task<IActionResult> Lock([FromServices]ISMSProvider provider, string userName, string password, string authType)
{
// 根据不同的登陆方式
Task<IActionResult> ret;
if (authType == MobileSchema) ret = Mobile(configuration, userName, password);
if (authType == MobileSchema) ret = Mobile(provider, userName, password);
else ret = Login(userName, password, string.Empty);
return ret;
}
@ -84,15 +86,14 @@ namespace Bootstrap.Admin.Controllers
/// <summary>
/// 短信验证登陆方法
/// </summary>
/// <param name="configuration"></param>
/// <param name="provider"></param>
/// <param name="phone"></param>
/// <param name="code"></param>
/// <returns></returns>
[HttpPost()]
public async Task<IActionResult> Mobile([FromServices]IConfiguration configuration, [FromQuery]string phone, [FromQuery]string code)
public async Task<IActionResult> Mobile([FromServices]ISMSProvider provider, string phone, string code)
{
var option = configuration.GetSection(nameof(SMSOptions)).Get<SMSOptions>();
var auth = SMSHelper.Validate(phone, code, option.MD5Key);
var auth = provider.Validate(phone, code);
HttpContext.Log(phone, auth);
if (auth)
{
@ -108,16 +109,26 @@ namespace Bootstrap.Admin.Controllers
Password = code,
Icon = "default.jpg",
Description = "手机用户",
App = option.App
App = provider.Option.App
};
UserHelper.Save(user);
// 根据配置文件设置默认角色
var roles = RoleHelper.Retrieves().Where(r => option.Roles.Any(rl => rl.Equals(r.RoleName, StringComparison.OrdinalIgnoreCase))).Select(r => r.Id);
var roles = RoleHelper.Retrieves().Where(r => provider.Option.Roles.Any(rl => rl.Equals(r.RoleName, StringComparison.OrdinalIgnoreCase))).Select(r => r.Id);
RoleHelper.SaveByUserId(user.Id, roles);
}
}
return auth ? await SignInAsync(phone, true, MobileSchema) : View("Login", new LoginModel() { AuthFailed = true });
return auth ? await SignInAsync(phone, true, MobileSchema) : RedirectLogin();
}
private IActionResult RedirectLogin()
{
var query = Request.Query.Aggregate(new Dictionary<string, string>(), (d, v) =>
{
d.Add(v.Key, v.Value.ToString());
return d;
});
return Redirect(QueryHelpers.AddQueryString(Request.PathBase + CookieAuthenticationDefaults.LoginPath, query));
}
/// <summary>

View File

@ -36,7 +36,7 @@ namespace Bootstrap.Admin.Controllers.Api
var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}");
return string.Join(" ", result);
});
color = ret.StartsWith("open 0 progressing 0") ? "success" : color;
color = ret.StartsWith("open 0 progressing 0", StringComparison.OrdinalIgnoreCase) ? "success" : color;
return new JsonResult(new { schemaVersion = 1, label, message = ret, color });
}

View File

@ -4,9 +4,7 @@ using Bootstrap.Security.Authentication;
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 Microsoft.Extensions.DependencyInjection;
using System.Threading.Tasks;
namespace Bootstrap.Admin.Controllers.Api
@ -49,19 +47,15 @@ namespace Bootstrap.Admin.Controllers.Api
/// <summary>
/// 下发手机短信方法
/// </summary>
/// <param name="configuration"></param>
/// <param name="factory"></param>
/// <param name="provider"></param>
/// <param name="phone"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpPut]
public async Task<bool> Put([FromServices]IConfiguration configuration, [FromServices]IHttpClientFactory factory, [FromQuery]string phone)
public async Task<bool> Put([FromServices]ISMSProvider provider, [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);
return await provider.SendCodeAsync(phone);
}
/// <summary>

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@ -6,59 +8,112 @@ using System.Collections.Specialized;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
#if NETCOREAPP3_0
using System.Text.Json;
#else
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
#endif
using System.Threading;
using System.Threading.Tasks;
namespace Bootstrap.DataAccess
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// 短信登录扩展类
/// </summary>
public static class SMSExtensions
{
/// <summary>
/// 注入短信登录服务到容器中
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddSMSProvider(this IServiceCollection services)
{
services.AddTransient<ISMSProvider, DefaultSMSProvider>();
return services;
}
}
/// <summary>
/// 短信登录接口
/// </summary>
public interface ISMSProvider
{
/// <summary>
/// 手机下发验证码方法
/// </summary>
/// <param name="phoneNumber">手机号</param>
/// <returns></returns>
Task<bool> SendCodeAsync(string phoneNumber);
/// <summary>
/// 验证手机验证码是否正确方法
/// </summary>
/// <param name="phoneNumber">手机号</param>
/// <param name="code">验证码</param>
/// <returns></returns>
bool Validate(string phoneNumber, string code);
/// <summary>
/// 获得 配置信息
/// </summary>
SMSOptions Option { get; }
}
/// <summary>
/// 手机号登陆帮助类
/// </summary>
public static class SMSHelper
internal class DefaultSMSProvider : ISMSProvider
{
private static ConcurrentDictionary<string, AutoExpireValidateCode> _pool = new ConcurrentDictionary<string, AutoExpireValidateCode>();
/// <summary>
/// 获得 短信配置信息
/// </summary>
public SMSOptions Option { get; protected set; }
private HttpClient _client;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="configuration"></param>
/// <param name="factory"></param>
public DefaultSMSProvider(IConfiguration configuration, IHttpClientFactory factory)
{
Option = configuration.GetSection<SMSOptions>().Get<SMSOptions>();
_client = factory.CreateClient();
}
/// <summary>
/// 下发验证码方法
/// </summary>
/// <param name="client"></param>
/// <param name="option"></param>
/// <param name="phoneNumber"></param>
/// <returns></returns>
public static async System.Threading.Tasks.Task<bool> SendCode(this HttpClient client, SMSOptions option)
public async Task<bool> SendCodeAsync(string phoneNumber)
{
option.Timestamp = (DateTimeOffset.UtcNow.Ticks - 621355968000000000) / 10000000;
Option.Timestamp = (DateTimeOffset.UtcNow.Ticks - 621355968000000000) / 10000000;
Option.Phone = phoneNumber;
var requestParameters = new Dictionary<string, string>()
{
{ "CompanyCode", option.CompanyCode },
{ "Phone", option.Phone },
{ "TimeStamp", option.Timestamp.ToString() },
{ "Sign", option.Sign() }
{ "CompanyCode", Option.CompanyCode },
{ "Phone", Option.Phone },
{ "TimeStamp", Option.Timestamp.ToString() },
{ "Sign", Sign() }
};
var url = QueryHelpers.AddQueryString("http://open.bluegoon.com/api/sms/sendcode", requestParameters);
var req = await client.GetAsync(url);
var url = QueryHelpers.AddQueryString(Option.RequestUrl, requestParameters);
var req = await _client.GetAsync(url);
var content = await req.Content.ReadAsStringAsync();
#if NETCOREAPP3_0
var result = JsonSerializer.Deserialize<SMSResult>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
#else
var result = JsonConvert.DeserializeObject<SMSResult>(content, new JsonSerializerSettings() { ContractResolver = new DefaultContractResolver() });
#endif
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));
_pool.AddOrUpdate(Option.Phone, key => new AutoExpireValidateCode(Option.Phone, result.Data, Option.Expires), (key, v) => v.Reset(result.Data));
ret = true;
}
else
{
new Exception("SMS Send Fail").Log(new NameValueCollection()
{
["UserId"] = option.Phone,
["UserId"] = Option.Phone,
["url"] = url,
["content"] = content
});
@ -71,13 +126,12 @@ namespace Bootstrap.DataAccess
/// </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;
public bool Validate(string phone, string code) => _pool.TryGetValue(phone, out var signKey) && Hash($"{code}{Option.MD5Key}") == signKey.Code;
private static string Sign(this SMSOptions option)
private string Sign()
{
return Hash($"{option.CompanyCode}{option.Phone}{option.Timestamp}{option.MD5Key}");
return Hash($"{Option.CompanyCode}{Option.Phone}{Option.Timestamp}{Option.MD5Key}");
}
private static string Hash(string data)
@ -124,7 +178,7 @@ namespace Bootstrap.DataAccess
private CancellationTokenSource _tokenSource;
private System.Threading.Tasks.Task RunAsync() => System.Threading.Tasks.Task.Run(() =>
private Task RunAsync() => Task.Run(() =>
{
_tokenSource = new CancellationTokenSource();
if (!_tokenSource.Token.WaitHandle.WaitOne(Expires)) _pool.TryRemove(Phone, out var _);
@ -188,5 +242,10 @@ namespace Bootstrap.DataAccess
/// 获得/设置 默认授权 App
/// </summary>
public string App { get; set; }
/// <summary>
/// 获得/设置 短信下发网关地址
/// </summary>
public string RequestUrl { get; set; } = "http://open.bluegoon.com/api/sms/sendcode";
}
}

View File

@ -79,6 +79,7 @@ namespace Bootstrap.Admin
option.AssumeDefaultVersionWhenUnspecified = true;
option.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version"));
});
services.AddSMSProvider();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -75,7 +75,7 @@
<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" />
<input type="text" id="phone" name="phone" class="form-control digits" data-toggle="tooltip" placeholder="手机号码" minlength="11" maxlength="11" data-required-msg="请输入手机号码" value="" data-valid="true" />
</div>
</div>
<div id="loginSMS" class="form-group d-none">
@ -85,7 +85,7 @@
<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" />
<input type="text" id="code" name="code" 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>

View File

@ -132,23 +132,24 @@
var $loginSMS = $('#loginSMS');
if ($login.attr('data-demo') === 'True') {
$login.find('[name="userName"], [name="password"]').attr('data-valid', 'false');
$login.on('submit', function (e) {
var model = $loginType.attr('data-value');
if (model === 'username') {
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.on('submit', function (e) {
var model = $loginType.attr('data-value');
if (model === 'username') {
if ($username.val() === '' && $password.val() === '') {
e.preventDefault();
location.href = "Gitee";
}
}
else {
// sms
var url = $.format('Account/Mobile{0}', location.search);
$login.attr('action', $.formatUrl(url));
return true;
}
});
// login type
var $loginType = $('#loginType').on('click', function (e) {
e.preventDefault();
@ -177,7 +178,7 @@
var timeHanlder = null;
$('#btnSendCode').on('click', function () {
// validate mobile phone
var $phone = $('#txtPhone');
var $phone = $('#phone');
var validator = $login.find('[data-toggle="LgbValidate"]').lgbValidator();
if (!validator.validElement($phone.get(0))) {
$phone.tooltip('show');
@ -200,7 +201,7 @@
if (result) {
// send success
$this.text('已发送').attr('disabled', true);
$('#smscode').removeAttr('disabled');
$('#code').removeAttr('disabled');
timeHanlder = setTimeout(function () {
clearTimeout(timeHanlder);
var count = 299;