feat: 增加腾云云短信接口

This commit is contained in:
Argo Windows 2019-11-01 21:39:40 +08:00
parent 908dd4cc0e
commit 84d7d43058
10 changed files with 71 additions and 279 deletions

View File

@ -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);
}
}

View File

@ -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>
/// 跨域握手协议

View File

@ -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";
}
}

View File

@ -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": [
"首页",
"测试页面",

View File

@ -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": [

View File

@ -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;

View File

@ -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" />

View File

@ -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>
/// 验证验证码方法

View File

@ -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]

View File

@ -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
}