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.Mvc;
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Security.Claims; using System.Security.Claims;
@ -49,18 +51,18 @@ namespace Bootstrap.Admin.Controllers
/// <summary> /// <summary>
/// 系统锁屏界面 /// 系统锁屏界面
/// </summary> /// </summary>
/// <param name="configuration"></param> /// <param name="provider"></param>
/// <param name="userName"></param> /// <param name="userName"></param>
/// <param name="password"></param> /// <param name="password"></param>
/// <param name="authType"></param> /// <param name="authType"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost] [HttpPost]
[IgnoreAntiforgeryToken] [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; 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); else ret = Login(userName, password, string.Empty);
return ret; return ret;
} }
@ -84,15 +86,14 @@ namespace Bootstrap.Admin.Controllers
/// <summary> /// <summary>
/// 短信验证登陆方法 /// 短信验证登陆方法
/// </summary> /// </summary>
/// <param name="configuration"></param> /// <param name="provider"></param>
/// <param name="phone"></param> /// <param name="phone"></param>
/// <param name="code"></param> /// <param name="code"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost()] [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 = provider.Validate(phone, code);
var auth = SMSHelper.Validate(phone, code, option.MD5Key);
HttpContext.Log(phone, auth); HttpContext.Log(phone, auth);
if (auth) if (auth)
{ {
@ -108,16 +109,26 @@ namespace Bootstrap.Admin.Controllers
Password = code, Password = code,
Icon = "default.jpg", Icon = "default.jpg",
Description = "手机用户", Description = "手机用户",
App = option.App App = provider.Option.App
}; };
UserHelper.Save(user); 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); 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> /// <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}"); var result = string.IsNullOrEmpty(content) ? new string[] { "unknown" } : regex.Select((m, i) => $"{labels[i]} {m.Groups[1].Value}");
return string.Join(" ", result); 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 }); 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 Longbow.Web.Mvc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Bootstrap.Admin.Controllers.Api namespace Bootstrap.Admin.Controllers.Api
@ -49,19 +47,15 @@ namespace Bootstrap.Admin.Controllers.Api
/// <summary> /// <summary>
/// 下发手机短信方法 /// 下发手机短信方法
/// </summary> /// </summary>
/// <param name="configuration"></param> /// <param name="provider"></param>
/// <param name="factory"></param>
/// <param name="phone"></param> /// <param name="phone"></param>
/// <returns></returns> /// <returns></returns>
[AllowAnonymous] [AllowAnonymous]
[HttpPut] [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; if (string.IsNullOrEmpty(phone)) return false;
return await provider.SendCodeAsync(phone);
var option = configuration.GetSection(nameof(SMSOptions)).Get<SMSOptions>();
option.Phone = phone;
return await factory.CreateClient().SendCode(option);
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,6 @@
using Microsoft.AspNetCore.WebUtilities; using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@ -6,59 +8,112 @@ using System.Collections.Specialized;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading;
#if NETCOREAPP3_0
using System.Text.Json; using System.Text.Json;
#else using System.Threading;
using Newtonsoft.Json; using System.Threading.Tasks;
using Newtonsoft.Json.Serialization;
#endif
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>
/// 手机号登陆帮助类 /// 手机号登陆帮助类
/// </summary> /// </summary>
public static class SMSHelper internal class DefaultSMSProvider : ISMSProvider
{ {
private static ConcurrentDictionary<string, AutoExpireValidateCode> _pool = new ConcurrentDictionary<string, AutoExpireValidateCode>(); 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>
/// 下发验证码方法 /// 下发验证码方法
/// </summary> /// </summary>
/// <param name="client"></param> /// <param name="phoneNumber"></param>
/// <param name="option"></param>
/// <returns></returns> /// <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>() var requestParameters = new Dictionary<string, string>()
{ {
{ "CompanyCode", option.CompanyCode }, { "CompanyCode", Option.CompanyCode },
{ "Phone", option.Phone }, { "Phone", Option.Phone },
{ "TimeStamp", option.Timestamp.ToString() }, { "TimeStamp", Option.Timestamp.ToString() },
{ "Sign", option.Sign() } { "Sign", Sign() }
}; };
var url = QueryHelpers.AddQueryString("http://open.bluegoon.com/api/sms/sendcode", requestParameters); var url = QueryHelpers.AddQueryString(Option.RequestUrl, requestParameters);
var req = await client.GetAsync(url); var req = await _client.GetAsync(url);
var content = await req.Content.ReadAsStringAsync(); var content = await req.Content.ReadAsStringAsync();
#if NETCOREAPP3_0
var result = JsonSerializer.Deserialize<SMSResult>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); 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; var ret = false;
if (result.Code == 1) 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; ret = true;
} }
else else
{ {
new Exception("SMS Send Fail").Log(new NameValueCollection() new Exception("SMS Send Fail").Log(new NameValueCollection()
{ {
["UserId"] = option.Phone, ["UserId"] = Option.Phone,
["url"] = url, ["url"] = url,
["content"] = content ["content"] = content
}); });
@ -71,13 +126,12 @@ namespace Bootstrap.DataAccess
/// </summary> /// </summary>
/// <param name="phone">手机号</param> /// <param name="phone">手机号</param>
/// <param name="code">验证码</param> /// <param name="code">验证码</param>
/// <param name="secret">密钥</param>
/// <returns></returns> /// <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) private static string Hash(string data)
@ -124,7 +178,7 @@ namespace Bootstrap.DataAccess
private CancellationTokenSource _tokenSource; private CancellationTokenSource _tokenSource;
private System.Threading.Tasks.Task RunAsync() => System.Threading.Tasks.Task.Run(() => private Task RunAsync() => Task.Run(() =>
{ {
_tokenSource = new CancellationTokenSource(); _tokenSource = new CancellationTokenSource();
if (!_tokenSource.Token.WaitHandle.WaitOne(Expires)) _pool.TryRemove(Phone, out var _); if (!_tokenSource.Token.WaitHandle.WaitOne(Expires)) _pool.TryRemove(Phone, out var _);
@ -188,5 +242,10 @@ namespace Bootstrap.DataAccess
/// 获得/设置 默认授权 App /// 获得/设置 默认授权 App
/// </summary> /// </summary>
public string App { get; set; } 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.AssumeDefaultVersionWhenUnspecified = true;
option.ApiVersionReader = ApiVersionReader.Combine(new HeaderApiVersionReader("api-version"), new QueryStringApiVersionReader("api-version")); 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. // 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> <span class="fa fa-user"></span>
</div> </div>
</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> </div>
<div id="loginSMS" class="form-group d-none"> <div id="loginSMS" class="form-group d-none">
@ -85,7 +85,7 @@
<span class="fa fa-lock"></span> <span class="fa fa-lock"></span>
</div> </div>
</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"> <div class="input-group-append">
<button type="button" id="btnSendCode" class="btn btn-sms" data-toggle="tooltip" title="点击发送验证码">发送验证码</button> <button type="button" id="btnSendCode" class="btn btn-sms" data-toggle="tooltip" title="点击发送验证码">发送验证码</button>
</div> </div>

View File

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