feat: 增加腾云云短信接口
This commit is contained in:
parent
908dd4cc0e
commit
84d7d43058
|
@ -3,6 +3,7 @@ using Bootstrap.DataAccess;
|
|||
using Bootstrap.Security.Mvc;
|
||||
using Longbow.GiteeAuth;
|
||||
using Longbow.GitHubAuth;
|
||||
using Longbow.Web.SMS;
|
||||
using Longbow.WeChatAuth;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
@ -11,7 +12,6 @@ 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;
|
||||
|
@ -109,12 +109,12 @@ namespace Bootstrap.Admin.Controllers
|
|||
Password = code,
|
||||
Icon = "default.jpg",
|
||||
Description = "手机用户",
|
||||
App = provider.Option.App
|
||||
App = provider.Options.App
|
||||
};
|
||||
UserHelper.Save(user);
|
||||
|
||||
// 根据配置文件设置默认角色
|
||||
var roles = RoleHelper.Retrieves().Where(r => provider.Option.Roles.Any(rl => rl.Equals(r.RoleName, StringComparison.OrdinalIgnoreCase))).Select(r => r.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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
using Bootstrap.DataAccess;
|
||||
using Bootstrap.Security.Authentication;
|
||||
using Longbow.Web.Mvc;
|
||||
using Longbow.Web.SMS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bootstrap.Admin.Controllers.Api
|
||||
|
@ -52,7 +52,7 @@ namespace Bootstrap.Admin.Controllers.Api
|
|||
/// <param name="phone"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPut]
|
||||
public async Task<bool> Put([FromServices]ISMSProvider provider, [FromQuery]string phone) => string.IsNullOrEmpty(phone) ? false : await provider.SendCodeAsync(phone);
|
||||
public async Task<SMSResult> Put([FromServices]ISMSProvider provider, [FromQuery]string phone) => string.IsNullOrEmpty(phone) ? new SMSResult() { Result = false, Msg = "手机号不可为空" } : await provider.SendCodeAsync(phone);
|
||||
|
||||
/// <summary>
|
||||
/// 跨域握手协议
|
||||
|
|
|
@ -1,15 +1,5 @@
|
|||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Longbow.Web.SMS;
|
||||
using Longbow.Web.SMS.Tencent;
|
||||
|
||||
namespace Microsoft.Extensions.DependencyInjection
|
||||
{
|
||||
|
@ -25,228 +15,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
/// <returns></returns>
|
||||
public static IServiceCollection AddSMSProvider(this IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<ISMSProvider, DefaultSMSProvider>();
|
||||
services.AddTransient<ISMSProvider, TencentSMSProvider>();
|
||||
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>
|
||||
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="phoneNumber"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<bool> SendCodeAsync(string phoneNumber)
|
||||
{
|
||||
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", Sign() }
|
||||
};
|
||||
|
||||
var url = QueryHelpers.AddQueryString(Option.RequestUrl, requestParameters);
|
||||
var req = await _client.GetAsync(url);
|
||||
var content = await req.Content.ReadAsStringAsync();
|
||||
var result = JsonSerializer.Deserialize<SMSResult>(content, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
new Exception(result.Msg).Log(new NameValueCollection()
|
||||
{
|
||||
["UserId"] = Option.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}{Option.MD5Key}") == signKey.Code;
|
||||
|
||||
private string Sign()
|
||||
{
|
||||
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 int Code { get; set; }
|
||||
|
||||
public string Data { get; set; }
|
||||
|
||||
public string Msg { 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 Task RunAsync() => 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);
|
||||
|
||||
/// <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; } = "0";
|
||||
|
||||
/// <summary>
|
||||
/// 获得/设置 短信下发网关地址
|
||||
/// </summary>
|
||||
public string RequestUrl { get; set; } = "http://open.bluegoon.com/api/sms/sendcode";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,13 +70,8 @@
|
|||
"ClientSecret": "3427f2d901ba9afc76c1842a7303b2d67f8e098e71acc15051f89fe6f3d265db",
|
||||
"CallbackPath": "/signin-gitee",
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"Scope": [
|
||||
"user_info",
|
||||
"projects"
|
||||
],
|
||||
"Roles": [
|
||||
"Administrators"
|
||||
],
|
||||
"Scope": [ "user_info", "projects" ],
|
||||
"Roles": [ "Administrators" ],
|
||||
"App": "0",
|
||||
"StarredUrl": "https://gitee.com/api/v5/user/starred/LongbowEnterprise/BootstrapAdmin"
|
||||
},
|
||||
|
@ -86,13 +81,8 @@
|
|||
"ClientSecret": "ffa759ca599df941b869efecb5e750bc1b27334e",
|
||||
"CallbackPath": "/signin-github",
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"Scope": [
|
||||
"user_info",
|
||||
"repo"
|
||||
],
|
||||
"Roles": [
|
||||
"Administrators"
|
||||
],
|
||||
"Scope": [ "user_info", "repo" ],
|
||||
"Roles": [ "Administrators" ],
|
||||
"App": "0",
|
||||
"StarredUrl": "https://api.github.com/user/starred/ArgoZhang/BootstrapAdmin"
|
||||
},
|
||||
|
@ -102,23 +92,27 @@
|
|||
"ClientSecret": "<secret>",
|
||||
"CallbackPath": "/signin-weixin",
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"Scope": [
|
||||
"snsapi_login"
|
||||
],
|
||||
"Roles": [
|
||||
"Administrators"
|
||||
],
|
||||
"Scope": [ "snsapi_login" ],
|
||||
"Roles": [ "Administrators" ],
|
||||
"App": "0"
|
||||
},
|
||||
"SMSOptions": {
|
||||
"CompanyCode": "<CompanyCode>",
|
||||
"MD5Key": "MD5Key",
|
||||
"Roles": [
|
||||
"Administrators"
|
||||
],
|
||||
"Roles": [ "Administrators" ],
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"App": "0"
|
||||
},
|
||||
"TencentSMSOptions": {
|
||||
"AppId": "<TencentAppId>",
|
||||
"AppKey": "<TencentAppKey>",
|
||||
"TplId": 0,
|
||||
"Sign": "<TencentSign>",
|
||||
"Roles": [ "Default" ],
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"App": "Demo",
|
||||
"Debug": true
|
||||
},
|
||||
"AppMenus": [
|
||||
"首页",
|
||||
"测试页面",
|
||||
|
|
|
@ -105,6 +105,15 @@
|
|||
"HomePath": "/Home/Index",
|
||||
"App": "2"
|
||||
},
|
||||
"TencentSMSOptions": {
|
||||
"AppId": "<TencentAppId>",
|
||||
"AppKey": "<TencentAppKey>",
|
||||
"TplId": 0,
|
||||
"Sign": "<TencentSign>",
|
||||
"Roles": [ "Default" ],
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"App": "Demo"
|
||||
},
|
||||
"LongbowCache": {
|
||||
"Enabled": true,
|
||||
"CorsItems": [
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
return false;
|
||||
});
|
||||
|
||||
$('button[type="submit"]').on('click', function (e) {
|
||||
var $loginButton = $('button[type="submit"]').on('click', function (e) {
|
||||
$.captchaCheck($('#login .slidercaptcha'), function () {
|
||||
$('form').submit();
|
||||
});
|
||||
|
@ -165,6 +165,7 @@
|
|||
$loginMobile.removeClass('d-none');
|
||||
|
||||
$this.attr('data-value', 'sms').text('用户名密码登陆');
|
||||
$loginButton.attr('data-original-title', '请输入手机号码并点击发送验证码');
|
||||
}
|
||||
else {
|
||||
// sms model
|
||||
|
@ -174,6 +175,7 @@
|
|||
$loginMobile.addClass('d-none');
|
||||
|
||||
$this.attr('data-value', 'username').text('短信验证登陆');
|
||||
$loginButton.attr('data-original-title', '不填写密码默认使用 Gitee 认证');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -195,16 +197,22 @@
|
|||
url: apiUrl,
|
||||
method: 'PUT',
|
||||
callback: function (result) {
|
||||
$this.attr('data-original-title', result ? "发送成功" : "短信登录体验活动结束").tooltip('show');
|
||||
$this.attr('data-original-title', result.Result ? "发送成功" : "短信登录体验活动结束").tooltip('show');
|
||||
var handler = setTimeout(function () {
|
||||
clearTimeout(handler);
|
||||
$this.tooltip('hide').attr('data-original-title', "点击发送验证码");
|
||||
}, 1000);
|
||||
}, 2000);
|
||||
|
||||
if (result) {
|
||||
if (result.Result) {
|
||||
// send success
|
||||
$this.text('已发送').attr('disabled', true);
|
||||
$('#code').removeAttr('disabled');
|
||||
if (result.Data === null) $loginButton.attr('data-original-title', '请输入验证码');
|
||||
else {
|
||||
$('#code').val(result.Data);
|
||||
$loginButton.attr('data-original-title', '点击登录系统');
|
||||
}
|
||||
|
||||
timeHanlder = setTimeout(function () {
|
||||
clearTimeout(timeHanlder);
|
||||
var count = 299;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<PackageReference Include="Longbow.PetaPoco" Version="1.0.2" />
|
||||
<PackageReference Include="Longbow.Security.Cryptography" Version="1.3.0" />
|
||||
<PackageReference Include="Longbow.Tasks" Version="3.0.0" />
|
||||
<PackageReference Include="Longbow.Web" Version="3.0.0" />
|
||||
<PackageReference Include="Longbow.Web" Version="3.0.1-beta1" />
|
||||
<PackageReference Include="Longbow.WeChatAuth" Version="3.0.0" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="3.0.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.7.0" />
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
using Longbow.Data;
|
||||
using Longbow.Web.SMS;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.AspNetCore.Mvc.Testing.Handlers;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using UnitTest;
|
||||
using Xunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Bootstrap.Admin
|
||||
{
|
||||
|
@ -138,14 +139,14 @@ namespace Bootstrap.Admin
|
|||
/// <summary>
|
||||
/// 获得 短信配置信息
|
||||
/// </summary>
|
||||
public SMSOptions Option { get; protected set; } = new SMSOptions();
|
||||
public SMSOptions Options { get; protected set; } = new SMSOptions();
|
||||
|
||||
/// <summary>
|
||||
/// 下发验证码方法
|
||||
/// </summary>
|
||||
/// <param name="phoneNumber"></param>
|
||||
/// <returns></returns>
|
||||
public Task<bool> SendCodeAsync(string phoneNumber) => Task.FromResult(true);
|
||||
public Task<SMSResult> SendCodeAsync(string phoneNumber) => Task.FromResult(new SMSResult() { Result = true });
|
||||
|
||||
/// <summary>
|
||||
/// 验证验证码方法
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using Bootstrap.DataAccess;
|
||||
using Longbow.Web.Mvc;
|
||||
using Longbow.Web.SMS;
|
||||
using System.Net.Http;
|
||||
using Xunit;
|
||||
|
||||
|
@ -36,12 +37,14 @@ namespace Bootstrap.Admin.Api.SqlServer
|
|||
public async void Put_Ok()
|
||||
{
|
||||
var resq = await Client.PutAsync("?phone=", new StringContent(""));
|
||||
var _token = await resq.Content.ReadAsStringAsync();
|
||||
Assert.Equal("false", _token);
|
||||
var payload = await resq.Content.ReadAsStringAsync();
|
||||
var resp = System.Text.Json.JsonSerializer.Deserialize<SMSResult>(payload, new System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
|
||||
Assert.False(resp.Result);
|
||||
|
||||
resq = await Client.PutAsync("?phone=18910001000", new StringContent(""));
|
||||
_token = await resq.Content.ReadAsStringAsync();
|
||||
Assert.Equal("true", _token);
|
||||
resq = await Client.PutAsync("?phone=18910281024", new StringContent(""));
|
||||
payload = await resq.Content.ReadAsStringAsync();
|
||||
resp = System.Text.Json.JsonSerializer.Deserialize<SMSResult>(payload, new System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true });
|
||||
Assert.True(resp.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Error",
|
||||
|
@ -63,12 +63,19 @@
|
|||
"SMSOptions": {
|
||||
"CompanyCode": "<CompanyCode>",
|
||||
"MD5Key": "MD5Key",
|
||||
"Roles": [
|
||||
"Administrators"
|
||||
],
|
||||
"Roles": [ "Administrators" ],
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"App": "0"
|
||||
},
|
||||
"TencentSMSOptions": {
|
||||
"AppId": "<TencentAppId>",
|
||||
"AppKey": "<TencentAppKey>",
|
||||
"TplId": "<TencentTplId>",
|
||||
"Sign": "<TencentSign>",
|
||||
"Roles": [ "Default" ],
|
||||
"HomePath": "/Admin/Profiles",
|
||||
"App": "Demo"
|
||||
},
|
||||
"LongbowCache": {
|
||||
"Enabled": false
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue