diff --git a/src/admin/Bootstrap.Admin/Controllers/AccountController.cs b/src/admin/Bootstrap.Admin/Controllers/AccountController.cs index 2491ad4f..a32202b7 100644 --- a/src/admin/Bootstrap.Admin/Controllers/AccountController.cs +++ b/src/admin/Bootstrap.Admin/Controllers/AccountController.cs @@ -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); } } diff --git a/src/admin/Bootstrap.Admin/Controllers/Api/LoginController.cs b/src/admin/Bootstrap.Admin/Controllers/Api/LoginController.cs index 5aaea6cf..8b4c4888 100644 --- a/src/admin/Bootstrap.Admin/Controllers/Api/LoginController.cs +++ b/src/admin/Bootstrap.Admin/Controllers/Api/LoginController.cs @@ -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 /// /// [HttpPut] - public async Task Put([FromServices]ISMSProvider provider, [FromQuery]string phone) => string.IsNullOrEmpty(phone) ? false : await provider.SendCodeAsync(phone); + public async Task Put([FromServices]ISMSProvider provider, [FromQuery]string phone) => string.IsNullOrEmpty(phone) ? new SMSResult() { Result = false, Msg = "手机号不可为空" } : await provider.SendCodeAsync(phone); /// /// 跨域握手协议 diff --git a/src/admin/Bootstrap.Admin/Extensions/SMSExtensions.cs b/src/admin/Bootstrap.Admin/Extensions/SMSExtensions.cs index cd99f189..f1e4a641 100644 --- a/src/admin/Bootstrap.Admin/Extensions/SMSExtensions.cs +++ b/src/admin/Bootstrap.Admin/Extensions/SMSExtensions.cs @@ -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 /// public static IServiceCollection AddSMSProvider(this IServiceCollection services) { - services.AddTransient(); + services.AddTransient(); return services; } } - - /// - /// 短信登录接口 - /// - public interface ISMSProvider - { - /// - /// 手机下发验证码方法 - /// - /// 手机号 - /// - Task SendCodeAsync(string phoneNumber); - - /// - /// 验证手机验证码是否正确方法 - /// - /// 手机号 - /// 验证码 - /// - bool Validate(string phoneNumber, string code); - - /// - /// 获得 配置信息 - /// - SMSOptions Option { get; } - } - - /// - /// 手机号登陆帮助类 - /// - internal class DefaultSMSProvider : ISMSProvider - { - private static ConcurrentDictionary _pool = new ConcurrentDictionary(); - - /// - /// 获得 短信配置信息 - /// - public SMSOptions Option { get; protected set; } - - private HttpClient _client; - - /// - /// 构造函数 - /// - /// - /// - public DefaultSMSProvider(IConfiguration configuration, IHttpClientFactory factory) - { - Option = configuration.GetSection().Get(); - _client = factory.CreateClient(); - } - - /// - /// 下发验证码方法 - /// - /// - /// - public async Task SendCodeAsync(string phoneNumber) - { - Option.Timestamp = (DateTimeOffset.UtcNow.Ticks - 621355968000000000) / 10000000; - Option.Phone = phoneNumber; - var requestParameters = new Dictionary() - { - { "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(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; - } - - /// - /// 验证验证码方法 - /// - /// 手机号 - /// 验证码 - /// - 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(); - } - - /// - /// - /// - public string Code { get; private set; } - - /// - /// - /// - public string Phone { get; } - - /// - /// - /// - 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 _); - }); - - /// - /// - /// - /// - public AutoExpireValidateCode Reset(string code) - { - Code = code; - _tokenSource.Cancel(); - RunAsync(); - return this; - } - } - } - - /// - /// 短信网关配置类 - /// - public class SMSOptions - { - /// - /// 获得/设置 公司编码 - /// - public string CompanyCode { get; set; } - - /// - /// 获得/设置 下发手机号码 - /// - public string Phone { get; set; } - - /// - /// 获得/设置 签名密钥 - /// - public string MD5Key { get; set; } - - /// - /// 获得/设置 时间戳 - /// - public long Timestamp { get; set; } - - /// - /// 获得/设置 验证码有效时长 - /// - public TimeSpan Expires { get; set; } = TimeSpan.FromMinutes(5); - - /// - /// 获得/设置 角色集合 - /// - public ICollection Roles { get; } = new HashSet(); - - /// - /// 获得/设置 登陆后首页 - /// - public string HomePath { get; set; } - - /// - /// 获得/设置 默认授权 App - /// - public string App { get; set; } = "0"; - - /// - /// 获得/设置 短信下发网关地址 - /// - public string RequestUrl { get; set; } = "http://open.bluegoon.com/api/sms/sendcode"; - } } diff --git a/src/admin/Bootstrap.Admin/appsettings.Development.json b/src/admin/Bootstrap.Admin/appsettings.Development.json index 9cd8411c..80ec42f5 100644 --- a/src/admin/Bootstrap.Admin/appsettings.Development.json +++ b/src/admin/Bootstrap.Admin/appsettings.Development.json @@ -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": "", "CallbackPath": "/signin-weixin", "HomePath": "/Admin/Profiles", - "Scope": [ - "snsapi_login" - ], - "Roles": [ - "Administrators" - ], + "Scope": [ "snsapi_login" ], + "Roles": [ "Administrators" ], "App": "0" }, "SMSOptions": { "CompanyCode": "", "MD5Key": "MD5Key", - "Roles": [ - "Administrators" - ], + "Roles": [ "Administrators" ], "HomePath": "/Admin/Profiles", "App": "0" }, + "TencentSMSOptions": { + "AppId": "", + "AppKey": "", + "TplId": 0, + "Sign": "", + "Roles": [ "Default" ], + "HomePath": "/Admin/Profiles", + "App": "Demo", + "Debug": true + }, "AppMenus": [ "首页", "测试页面", diff --git a/src/admin/Bootstrap.Admin/appsettings.json b/src/admin/Bootstrap.Admin/appsettings.json index d2a03f26..48c852dd 100644 --- a/src/admin/Bootstrap.Admin/appsettings.json +++ b/src/admin/Bootstrap.Admin/appsettings.json @@ -105,6 +105,15 @@ "HomePath": "/Home/Index", "App": "2" }, + "TencentSMSOptions": { + "AppId": "", + "AppKey": "", + "TplId": 0, + "Sign": "", + "Roles": [ "Default" ], + "HomePath": "/Admin/Profiles", + "App": "Demo" + }, "LongbowCache": { "Enabled": true, "CorsItems": [ diff --git a/src/admin/Bootstrap.Admin/wwwroot/js/login.js b/src/admin/Bootstrap.Admin/wwwroot/js/login.js index 76971c62..4199535b 100644 --- a/src/admin/Bootstrap.Admin/wwwroot/js/login.js +++ b/src/admin/Bootstrap.Admin/wwwroot/js/login.js @@ -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; diff --git a/src/admin/Bootstrap.DataAccess/Bootstrap.DataAccess.csproj b/src/admin/Bootstrap.DataAccess/Bootstrap.DataAccess.csproj index 9ad9d96a..7ab5772c 100644 --- a/src/admin/Bootstrap.DataAccess/Bootstrap.DataAccess.csproj +++ b/src/admin/Bootstrap.DataAccess/Bootstrap.DataAccess.csproj @@ -16,7 +16,7 @@ - + diff --git a/test/UnitTest/BAWebHost.cs b/test/UnitTest/BAWebHost.cs index f8aa41c7..6758c13f 100644 --- a/test/UnitTest/BAWebHost.cs +++ b/test/UnitTest/BAWebHost.cs @@ -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 /// /// 获得 短信配置信息 /// - public SMSOptions Option { get; protected set; } = new SMSOptions(); + public SMSOptions Options { get; protected set; } = new SMSOptions(); /// /// 下发验证码方法 /// /// /// - public Task SendCodeAsync(string phoneNumber) => Task.FromResult(true); + public Task SendCodeAsync(string phoneNumber) => Task.FromResult(new SMSResult() { Result = true }); /// /// 验证验证码方法 diff --git a/test/UnitTest/Bootstrap.Admin/Api/SQLServer/LoginTest.cs b/test/UnitTest/Bootstrap.Admin/Api/SQLServer/LoginTest.cs index e8b6dbd8..c51d379b 100644 --- a/test/UnitTest/Bootstrap.Admin/Api/SQLServer/LoginTest.cs +++ b/test/UnitTest/Bootstrap.Admin/Api/SQLServer/LoginTest.cs @@ -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(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(payload, new System.Text.Json.JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); + Assert.True(resp.Result); } [Fact] diff --git a/test/UnitTest/appsettings.json b/test/UnitTest/appsettings.json index e11222a4..12da8d7b 100644 --- a/test/UnitTest/appsettings.json +++ b/test/UnitTest/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "Logging": { "LogLevel": { "Default": "Error", @@ -63,12 +63,19 @@ "SMSOptions": { "CompanyCode": "", "MD5Key": "MD5Key", - "Roles": [ - "Administrators" - ], + "Roles": [ "Administrators" ], "HomePath": "/Admin/Profiles", "App": "0" }, + "TencentSMSOptions": { + "AppId": "", + "AppKey": "", + "TplId": "", + "Sign": "", + "Roles": [ "Default" ], + "HomePath": "/Admin/Profiles", + "App": "Demo" + }, "LongbowCache": { "Enabled": false }