feat: 增加手机登录
This commit is contained in:
parent
1d94e53866
commit
892049c2cb
|
@ -1,5 +1,6 @@
|
|||
using BootstrapAdmin.Web.Core;
|
||||
using BootstrapAdmin.Web.Services;
|
||||
using BootstrapAdmin.Web.Services.SMS;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -15,46 +16,9 @@ namespace Bootstrap.Admin.Controllers
|
|||
[AllowAnonymous]
|
||||
public class AccountController : Controller
|
||||
{
|
||||
//private const string MobileSchema = "Mobile";
|
||||
///// <summary>
|
||||
///// 系统锁屏界面
|
||||
///// </summary>
|
||||
///// <returns></returns>
|
||||
//[HttpGet]
|
||||
//public async Task<ActionResult> Lock()
|
||||
//{
|
||||
// if (!User.Identity!.IsAuthenticated) return Login();
|
||||
|
||||
// var authenticationType = User.Identity.AuthenticationType;
|
||||
// await HttpContext.SignOutAsync();
|
||||
// var urlReferrer = Request.Headers["Referer"].FirstOrDefault();
|
||||
// if (urlReferrer?.Contains("/Pages", StringComparison.OrdinalIgnoreCase) ?? false) urlReferrer = "/Pages";
|
||||
// return View(new LockModel(User.Identity.Name)
|
||||
// {
|
||||
// AuthenticationType = authenticationType,
|
||||
// ReturnUrl = WebUtility.UrlEncode(string.IsNullOrEmpty(urlReferrer) ? CookieAuthenticationDefaults.LoginPath.Value : urlReferrer)
|
||||
// });
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// 系统锁屏界面
|
||||
///// </summary>
|
||||
///// <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] ISMSProvider provider, string userName, string password, string authType)
|
||||
//{
|
||||
// // 根据不同的登陆方式
|
||||
// Task<IActionResult> ret;
|
||||
// if (authType == MobileSchema) ret = Mobile(provider, userName, password);
|
||||
// else ret = Login(userName, password, string.Empty);
|
||||
// return ret;
|
||||
//}
|
||||
private const string MobileSchema = "Mobile";
|
||||
|
||||
#region UserLogin
|
||||
/// <summary>
|
||||
/// Login the specified userName, password and remember.
|
||||
/// </summary>
|
||||
|
@ -96,6 +60,18 @@ namespace Bootstrap.Admin.Controllers
|
|||
return Redirect(originUrl);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Logout
|
||||
/// <summary>
|
||||
/// Logout this instance.
|
||||
/// </summary>
|
||||
|
@ -108,57 +84,51 @@ namespace Bootstrap.Admin.Controllers
|
|||
await HttpContext.SignOutAsync();
|
||||
return Redirect(QueryHelpers.AddQueryString(Request.PathBase + CookieAuthenticationDefaults.LoginPath, "AppId", appId ?? context.AppId));
|
||||
}
|
||||
#endregion
|
||||
|
||||
private IActionResult RedirectLogin()
|
||||
#region Mobile Login
|
||||
/// <summary>
|
||||
/// 短信验证登陆方法
|
||||
/// </summary>
|
||||
/// <param name="provider"></param>
|
||||
/// <param name="loginService"></param>
|
||||
/// <param name="phone"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost()]
|
||||
public async Task<IActionResult> Mobile([FromServices] ISMSProvider provider, [FromServices] ILogins loginService, string phone, string code)
|
||||
{
|
||||
var query = Request.Query.Aggregate(new Dictionary<string, string?>(), (d, v) =>
|
||||
if (string.IsNullOrEmpty(phone) || string.IsNullOrEmpty(code)) return RedirectLogin();
|
||||
|
||||
var auth = provider.Validate(phone, code);
|
||||
await loginService.Log(phone, auth);
|
||||
if (auth)
|
||||
{
|
||||
d.Add(v.Key, v.Value.ToString());
|
||||
return d;
|
||||
});
|
||||
return Redirect(QueryHelpers.AddQueryString(Request.PathBase + CookieAuthenticationDefaults.LoginPath, query));
|
||||
//var user = UserHelper.Retrieves().FirstOrDefault(u => u.UserName == phone);
|
||||
//if (user == null)
|
||||
//{
|
||||
// user = new User()
|
||||
// {
|
||||
// ApprovedBy = "Mobile",
|
||||
// ApprovedTime = DateTime.Now,
|
||||
// DisplayName = "手机用户",
|
||||
// UserName = phone,
|
||||
// Password = code,
|
||||
// Icon = "default.jpg",
|
||||
// Description = "手机用户",
|
||||
// App = provider.Options.App
|
||||
// };
|
||||
// if (UserHelper.Save(user) && !string.IsNullOrEmpty(user.Id))
|
||||
// {
|
||||
// // 根据配置文件设置默认角色
|
||||
// var roles = RoleHelper.Retrieves().Where(r => provider.Options.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) : RedirectLogin();
|
||||
}
|
||||
|
||||
///// <summary>
|
||||
///// 短信验证登陆方法
|
||||
///// </summary>
|
||||
///// <param name="provider"></param>
|
||||
///// <param name="phone"></param>
|
||||
///// <param name="code"></param>
|
||||
///// <returns></returns>
|
||||
//[HttpPost()]
|
||||
//public async Task<IActionResult> Mobile([FromServices] ISMSProvider provider, string phone, string code)
|
||||
//{
|
||||
// if (string.IsNullOrEmpty(phone) || string.IsNullOrEmpty(code)) return RedirectLogin();
|
||||
|
||||
// var auth = provider.Validate(phone, code);
|
||||
// await HttpContext.Log(phone, auth);
|
||||
// if (auth)
|
||||
// {
|
||||
// var user = UserHelper.Retrieves().FirstOrDefault(u => u.UserName == phone);
|
||||
// if (user == null)
|
||||
// {
|
||||
// user = new User()
|
||||
// {
|
||||
// ApprovedBy = "Mobile",
|
||||
// ApprovedTime = DateTime.Now,
|
||||
// DisplayName = "手机用户",
|
||||
// UserName = phone,
|
||||
// Password = code,
|
||||
// Icon = "default.jpg",
|
||||
// Description = "手机用户",
|
||||
// App = provider.Options.App
|
||||
// };
|
||||
// if (UserHelper.Save(user) && !string.IsNullOrEmpty(user.Id))
|
||||
// {
|
||||
// // 根据配置文件设置默认角色
|
||||
// var roles = RoleHelper.Retrieves().Where(r => provider.Options.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) : RedirectLogin();
|
||||
//}
|
||||
#endregion
|
||||
|
||||
///// <summary>
|
||||
///// Accesses the denied.
|
||||
|
@ -222,5 +192,43 @@ namespace Bootstrap.Admin.Controllers
|
|||
// var enabled = config.GetValue($"{nameof(WeChatOptions)}:Enabled", false);
|
||||
// return Challenge(enabled ? WeChatDefaults.AuthenticationScheme : CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
//}
|
||||
///// <summary>
|
||||
///// 系统锁屏界面
|
||||
///// </summary>
|
||||
///// <returns></returns>
|
||||
//[HttpGet]
|
||||
//public async Task<ActionResult> Lock()
|
||||
//{
|
||||
// if (!User.Identity!.IsAuthenticated) return Login();
|
||||
|
||||
// var authenticationType = User.Identity.AuthenticationType;
|
||||
// await HttpContext.SignOutAsync();
|
||||
// var urlReferrer = Request.Headers["Referer"].FirstOrDefault();
|
||||
// if (urlReferrer?.Contains("/Pages", StringComparison.OrdinalIgnoreCase) ?? false) urlReferrer = "/Pages";
|
||||
// return View(new LockModel(User.Identity.Name)
|
||||
// {
|
||||
// AuthenticationType = authenticationType,
|
||||
// ReturnUrl = WebUtility.UrlEncode(string.IsNullOrEmpty(urlReferrer) ? CookieAuthenticationDefaults.LoginPath.Value : urlReferrer)
|
||||
// });
|
||||
//}
|
||||
|
||||
///// <summary>
|
||||
///// 系统锁屏界面
|
||||
///// </summary>
|
||||
///// <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] ISMSProvider provider, string userName, string password, string authType)
|
||||
//{
|
||||
// // 根据不同的登陆方式
|
||||
// Task<IActionResult> ret;
|
||||
// if (authType == MobileSchema) ret = Mobile(provider, userName, password);
|
||||
// else ret = Login(userName, password, string.Empty);
|
||||
// return ret;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using BootstrapAdmin.Web.Services;
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using BootstrapAdmin.Web.Services.SMS.Tencent;
|
||||
using BootstrapAdmin.Web.Services.SMS;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
|
@ -62,6 +64,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
// 增加 BootstrapBlazor 组件
|
||||
services.AddBootstrapBlazor();
|
||||
|
||||
// 增加手机短信服务
|
||||
services.AddTransient<ISMSProvider, TencentSMSProvider>();
|
||||
|
||||
// 增加认证授权服务
|
||||
services.AddBootstrapAdminSecurity<AdminService>();
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using BootstrapAdmin.Web.Core;
|
||||
using BootstrapAdmin.Web.Services;
|
||||
|
||||
namespace BootstrapAdmin.Web.Pages.Account
|
||||
{
|
||||
|
@ -24,10 +23,6 @@ namespace BootstrapAdmin.Web.Pages.Account
|
|||
[NotNull]
|
||||
private IDicts? DictsService { get; set; }
|
||||
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private IUsers? UserService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
|
||||
namespace BootstrapAdmin.Web.Services.SMS
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
internal class AutoExpireValidateCode
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="phone"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <param name="expires"></param>
|
||||
/// <param name="expiredCallback"></param>
|
||||
public AutoExpireValidateCode(string phone, string code, TimeSpan expires, Action<string> expiredCallback)
|
||||
{
|
||||
Phone = phone;
|
||||
Code = code;
|
||||
Expires = expires;
|
||||
ExpiredCallback = expiredCallback;
|
||||
RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
protected Action<string> ExpiredCallback { get; set; }
|
||||
|
||||
/// <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 Task RunAsync() => Task.Run(() =>
|
||||
{
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
if (!_tokenSource.Token.WaitHandle.WaitOne(Expires)) ExpiredCallback(Phone);
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="code"></param>
|
||||
public AutoExpireValidateCode Reset(string code)
|
||||
{
|
||||
Code = code;
|
||||
_tokenSource?.Cancel();
|
||||
RunAsync();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Specialized;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BootstrapAdmin.Web.Services.SMS
|
||||
{
|
||||
/// <summary>
|
||||
/// 手机号登陆帮助类
|
||||
/// </summary>
|
||||
public class DefaultSMSProvider : ISMSProvider
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, AutoExpireValidateCode> _pool = new ConcurrentDictionary<string, AutoExpireValidateCode>();
|
||||
|
||||
/// <summary>
|
||||
/// 获得 短信配置信息
|
||||
/// </summary>
|
||||
public SMSOptions Options { get { return _options; } }
|
||||
|
||||
private readonly DefaultSMSOptions _options;
|
||||
private readonly HttpClient _client;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="factory"></param>
|
||||
public DefaultSMSProvider(IConfiguration configuration, IHttpClientFactory factory)
|
||||
{
|
||||
_options = configuration.GetSection(nameof(SMSOptions)).Get<DefaultSMSOptions>();
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下发验证码方法
|
||||
/// </summary>
|
||||
/// <param name="phoneNumber"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<SMSResult> SendCodeAsync(string phoneNumber)
|
||||
{
|
||||
Options.Timestamp = (DateTimeOffset.UtcNow.Ticks - 621355968000000000) / 10000000;
|
||||
Options.Phone = phoneNumber;
|
||||
var requestParameters = new Dictionary<string, string?>()
|
||||
{
|
||||
{ "CompanyCode", _options.CompanyCode },
|
||||
{ "Phone", Options.Phone },
|
||||
{ "TimeStamp", Options.Timestamp.ToString() },
|
||||
{ "Sign", Sign() }
|
||||
};
|
||||
|
||||
var url = QueryHelpers.AddQueryString(Options.RequestUrl, requestParameters);
|
||||
var req = await _client.GetAsync(url);
|
||||
var content = await req.Content.ReadAsStringAsync();
|
||||
#if !NETSTANDARD2_0
|
||||
var result = JsonSerializer.Deserialize<DefaultSMSResult>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
|
||||
#else
|
||||
var result = JsonConvert.DeserializeObject<DefaultSMSResult>(content);
|
||||
#endif
|
||||
var ret = new SMSResult() { Result = result!.Code == 1, Msg = result.Msg };
|
||||
if (ret.Result)
|
||||
{
|
||||
_pool.AddOrUpdate(Options.Phone, key => new AutoExpireValidateCode(Options.Phone, result.Data, Options.Expires, phone => _pool.TryRemove(phone, out var _)), (key, v) => v.Reset(result.Data));
|
||||
}
|
||||
else
|
||||
{
|
||||
new Exception(result.Msg).Format(new NameValueCollection()
|
||||
{
|
||||
["UserId"] = Options.Phone,
|
||||
["url"] = url,
|
||||
["content"] = content
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证验证码方法
|
||||
/// </summary>
|
||||
/// <param name="phone">手机号</param>
|
||||
/// <param name="code">验证码</param>
|
||||
/// <returns></returns>
|
||||
public bool Validate(string phone, string code) => _pool.TryGetValue(phone, out var signKey) && Hash($"{code}{_options.MD5Key}") == signKey.Code;
|
||||
|
||||
private string Sign()
|
||||
{
|
||||
return Hash($"{_options.CompanyCode}{Options.Phone}{Options.Timestamp}{_options.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 DefaultSMSResult
|
||||
{
|
||||
public int Code { get; set; }
|
||||
|
||||
public string Data { get; set; } = "";
|
||||
|
||||
public string Msg { get; set; } = "";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class DefaultSMSOptions : SMSOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获得/设置 公司编码
|
||||
/// </summary>
|
||||
public string CompanyCode { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 签名密钥
|
||||
/// </summary>
|
||||
public string MD5Key { get; set; } = "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
|
||||
namespace BootstrapAdmin.Web.Services.SMS
|
||||
{
|
||||
/// <summary>
|
||||
/// 短信登录接口
|
||||
/// </summary>
|
||||
public interface ISMSProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 手机下发验证码方法
|
||||
/// </summary>
|
||||
/// <param name="phoneNumber">手机号</param>
|
||||
/// <returns></returns>
|
||||
Task<SMSResult> SendCodeAsync(string phoneNumber);
|
||||
|
||||
/// <summary>
|
||||
/// 验证手机验证码是否正确方法
|
||||
/// </summary>
|
||||
/// <param name="phoneNumber">手机号</param>
|
||||
/// <param name="code">验证码</param>
|
||||
/// <returns></returns>
|
||||
bool Validate(string phoneNumber, string code);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
SMSOptions Options { get; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
|
||||
namespace BootstrapAdmin.Web.Services.SMS
|
||||
{
|
||||
/// <summary>
|
||||
/// 短信网关配置类
|
||||
/// </summary>
|
||||
public class SMSOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获得/设置 下发手机号码
|
||||
/// </summary>
|
||||
public string Phone { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 时间戳
|
||||
/// </summary>
|
||||
public long Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 验证码有效时长
|
||||
/// </summary>
|
||||
public TimeSpan Expires { get; set; } = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 角色集合
|
||||
/// </summary>
|
||||
public ICollection<string> Roles { get; } = new HashSet<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 登陆后首页
|
||||
/// </summary>
|
||||
public string HomePath { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 默认授权 App
|
||||
/// </summary>
|
||||
public string App { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 短信下发网关地址
|
||||
/// </summary>
|
||||
public string RequestUrl { get; set; } = "http://open.bluegoon.com/api/sms/sendcode";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
|
||||
namespace BootstrapAdmin.Web.Services.SMS
|
||||
{
|
||||
/// <summary>
|
||||
/// 短信结果实体类
|
||||
/// </summary>
|
||||
public class SMSResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 短信验证码是否发送成功
|
||||
/// </summary>
|
||||
public bool Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 短信验证码返回数据
|
||||
/// </summary>
|
||||
public string? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 短信失败原因提示信息
|
||||
/// </summary>
|
||||
public string? Msg { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
|
||||
namespace BootstrapAdmin.Web.Services.SMS.Tencent
|
||||
{
|
||||
/// <summary>
|
||||
/// 腾讯短信配置实体类
|
||||
/// </summary>
|
||||
public class TencentSMSOptions : SMSOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 腾讯 AppId
|
||||
/// </summary>
|
||||
public string AppId { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯 AppKey
|
||||
/// </summary>
|
||||
public string AppKey { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯 模板 ID
|
||||
/// </summary>
|
||||
public int TplId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 腾讯 应用签名
|
||||
/// </summary>
|
||||
public string Sign { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// 是否为 Debug 模式 默认为 False
|
||||
/// </summary>
|
||||
public bool Debug { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
|
||||
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Specialized;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace BootstrapAdmin.Web.Services.SMS.Tencent
|
||||
{
|
||||
/// <summary>
|
||||
/// 腾讯云短信平台接口
|
||||
/// </summary>
|
||||
public class TencentSMSProvider : ISMSProvider
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public SMSOptions Options { get { return _options; } }
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly TencentSMSOptions _options;
|
||||
private readonly Random _random;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="configuration"></param>
|
||||
/// <param name="factory"></param>
|
||||
public TencentSMSProvider(IConfiguration configuration, IHttpClientFactory factory)
|
||||
{
|
||||
_options = configuration.GetSection(nameof(TencentSMSOptions)).Get<TencentSMSOptions>();
|
||||
Options.RequestUrl = "https://yun.tim.qq.com/v5/tlssmssvr/sendsms";
|
||||
_client = factory.CreateClient();
|
||||
_random = new Random();
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, AutoExpireValidateCode> _pool = new ConcurrentDictionary<string, AutoExpireValidateCode>();
|
||||
/// <summary>
|
||||
/// 手机下发验证码方法
|
||||
/// </summary>
|
||||
/// <param name="phoneNumber"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<SMSResult> SendCodeAsync(string phoneNumber)
|
||||
{
|
||||
// post https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=xxxxx&random=xxxx
|
||||
Options.Timestamp = (DateTimeOffset.UtcNow.Ticks - 621355968000000000) / 10000000;
|
||||
Options.Phone = phoneNumber;
|
||||
var requestParameters = new Dictionary<string, string?>()
|
||||
{
|
||||
{ "sdkappid", _options.AppId },
|
||||
{ "random", Options.Timestamp.ToString() }
|
||||
};
|
||||
|
||||
var url = QueryHelpers.AddQueryString(Options.RequestUrl, requestParameters);
|
||||
var postData = new TencentSendData()
|
||||
{
|
||||
Sig = Sign(),
|
||||
Sign = _options.Sign,
|
||||
Time = Options.Timestamp,
|
||||
Tel = new TencentPhone() { Mobile = Options.Phone },
|
||||
Tpl_id = _options.TplId
|
||||
};
|
||||
var code = _random.Next(1000, 9999).ToString();
|
||||
postData.Params.Add(code);
|
||||
postData.Params.Add(Options.Expires.Minutes.ToString());
|
||||
|
||||
var result = _options.Debug ? await Task.FromResult(new TencenResponse() { Result = 0 }) : await RequestSendCodeUrl(url, postData);
|
||||
var ret = new SMSResult() { Result = result.Result == 0, Msg = result.Errmsg };
|
||||
|
||||
// debug 模式下发验证码到客户端
|
||||
if (_options.Debug) ret.Data = code;
|
||||
if (ret.Result)
|
||||
{
|
||||
_pool.AddOrUpdate(Options.Phone, key => new AutoExpireValidateCode(Options.Phone, code, Options.Expires, phone => _pool.TryRemove(phone, out var _)), (key, v) => v.Reset(code));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
private async Task<TencenResponse> RequestSendCodeUrl(string url, TencentSendData postData)
|
||||
{
|
||||
var req = await _client.PostAsJsonAsync(url, postData, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
||||
var content = await req.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<TencenResponse>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
|
||||
|
||||
if (result!.Result != 0)
|
||||
{
|
||||
new Exception(result.Errmsg).Format(new NameValueCollection()
|
||||
{
|
||||
["UserId"] = Options.Phone,
|
||||
["url"] = url,
|
||||
["content"] = content
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private string Sign()
|
||||
{
|
||||
return Hash($"appkey={_options.AppKey}&random={Options.Timestamp}&time={Options.Timestamp}&mobile={Options.Phone}");
|
||||
}
|
||||
|
||||
private static string Hash(string data)
|
||||
{
|
||||
using var algo = SHA256.Create();
|
||||
var sign = BitConverter.ToString(algo.ComputeHash(Encoding.UTF8.GetBytes(data)));
|
||||
sign = sign.Replace("-", "").ToLowerInvariant();
|
||||
return sign;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证手机验证码是否正确方法
|
||||
/// </summary>
|
||||
/// <param name="phoneNumber"></param>
|
||||
/// <param name="code"></param>
|
||||
/// <returns></returns>
|
||||
public bool Validate(string phoneNumber, string code) => _pool.TryGetValue(phoneNumber, out var signKey) && code == signKey.Code;
|
||||
|
||||
/// <summary>
|
||||
/// 文档 https://cloud.tencent.com/document/product/382/5976
|
||||
/// </summary>
|
||||
private class TencentSendData
|
||||
{
|
||||
public string Ext { get; set; } = "";
|
||||
|
||||
public string Extend { get; set; } = "";
|
||||
|
||||
public ICollection<string> Params { get; } = new HashSet<string>();
|
||||
|
||||
public string Sig { get; set; } = "";
|
||||
|
||||
public string Sign { get; set; } = "";
|
||||
|
||||
public TencentPhone Tel { get; set; } = new TencentPhone();
|
||||
|
||||
public long Time { get; set; }
|
||||
|
||||
public int Tpl_id { get; set; }
|
||||
}
|
||||
|
||||
private class TencentPhone
|
||||
{
|
||||
public string Mobile { get; set; } = "";
|
||||
|
||||
public string Nationcode { get; set; } = "86";
|
||||
}
|
||||
|
||||
private class TencenResponse
|
||||
{
|
||||
public int Result { get; set; } = -1;
|
||||
|
||||
public string Errmsg { get; set; } = "";
|
||||
|
||||
public string Ext { get; set; } = "";
|
||||
|
||||
public int Fee { get; set; }
|
||||
|
||||
public string Sid { get; set; } = "";
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue