diff --git a/aspnet-core/LINGYUN.MicroService.SingleProject.sln b/aspnet-core/LINGYUN.MicroService.SingleProject.sln index dc98863cc..b33f0f188 100644 --- a/aspnet-core/LINGYUN.MicroService.SingleProject.sln +++ b/aspnet-core/LINGYUN.MicroService.SingleProject.sln @@ -656,6 +656,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Gdpr.HttpApi.Cl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Gdpr.Web", "modules\gdpr\LINGYUN.Abp.Gdpr.Web\LINGYUN.Abp.Gdpr.Web.csproj", "{00868838-6891-B034-AAB7-AADAA3ECDCB3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Identity.QrCode", "modules\identity\LINGYUN.Abp.Identity.QrCode\LINGYUN.Abp.Identity.QrCode.csproj", "{E40CB92D-6C03-4A61-9A47-057CA504A46B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.OpenIddict.QrCode", "modules\openIddict\LINGYUN.Abp.OpenIddict.QrCode\LINGYUN.Abp.OpenIddict.QrCode.csproj", "{4C7594CE-FB2F-4309-B95C-D4460892CF6E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Identity.AspNetCore.QrCode", "modules\identity\LINGYUN.Abp.Identity.AspNetCore.QrCode\LINGYUN.Abp.Identity.AspNetCore.QrCode.csproj", "{C9266D5D-3860-09C3-F566-489BBB57A534}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1738,6 +1744,18 @@ Global {00868838-6891-B034-AAB7-AADAA3ECDCB3}.Debug|Any CPU.Build.0 = Debug|Any CPU {00868838-6891-B034-AAB7-AADAA3ECDCB3}.Release|Any CPU.ActiveCfg = Release|Any CPU {00868838-6891-B034-AAB7-AADAA3ECDCB3}.Release|Any CPU.Build.0 = Release|Any CPU + {E40CB92D-6C03-4A61-9A47-057CA504A46B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E40CB92D-6C03-4A61-9A47-057CA504A46B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E40CB92D-6C03-4A61-9A47-057CA504A46B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E40CB92D-6C03-4A61-9A47-057CA504A46B}.Release|Any CPU.Build.0 = Release|Any CPU + {4C7594CE-FB2F-4309-B95C-D4460892CF6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C7594CE-FB2F-4309-B95C-D4460892CF6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C7594CE-FB2F-4309-B95C-D4460892CF6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C7594CE-FB2F-4309-B95C-D4460892CF6E}.Release|Any CPU.Build.0 = Release|Any CPU + {C9266D5D-3860-09C3-F566-489BBB57A534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9266D5D-3860-09C3-F566-489BBB57A534}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9266D5D-3860-09C3-F566-489BBB57A534}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9266D5D-3860-09C3-F566-489BBB57A534}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2057,6 +2075,9 @@ Global {B080B644-A835-42EE-EBF5-C47B6D075588} = {4F837B81-EA7D-472A-8482-3D5A730DF810} {170F4C72-BED2-C77A-64B0-F4CB9A232026} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {00868838-6891-B034-AAB7-AADAA3ECDCB3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {E40CB92D-6C03-4A61-9A47-057CA504A46B} = {D94D6AFE-20BD-4F21-8708-03F5E34F49FC} + {4C7594CE-FB2F-4309-B95C-D4460892CF6E} = {7C714185-D3D9-4D94-B5CB-D857A0091F04} + {C9266D5D-3860-09C3-F566-489BBB57A534} = {D94D6AFE-20BD-4F21-8708-03F5E34F49FC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {711A43C0-A2F8-4E5C-9B9F-F2551E4B3FF1} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs index c026dcd3c..f1d791cc6 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountWebModule.cs @@ -1,6 +1,7 @@ using LINGYUN.Abp.Account.Emailing; using LINGYUN.Abp.Account.Web.ProfileManagement; using LINGYUN.Abp.Identity; +using LINGYUN.Abp.Identity.AspNetCore.QrCode; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Account.Localization; using Volo.Abp.Account.Web.Pages.Account; @@ -21,6 +22,7 @@ namespace LINGYUN.Abp.Account.Web; typeof(VoloAbpAccountWebModule), typeof(AbpIdentityDomainModule), typeof(AbpAccountEmailingModule), + typeof(AbpIdentityAspNetCoreQrCodeModule), typeof(AbpAccountApplicationContractsModule))] public class AbpAccountWebModule : AbpModule { diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/GenerateQrCodeResult.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/GenerateQrCodeResult.cs new file mode 100644 index 000000000..2accfbb27 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/GenerateQrCodeResult.cs @@ -0,0 +1,6 @@ +namespace LINGYUN.Abp.Account.Web.Areas.Account.Controllers.Models; + +public class GenerateQrCodeResult +{ + public string Key { get; set; } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/QrCodeInfoResult.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/QrCodeInfoResult.cs new file mode 100644 index 000000000..8f8a2e695 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/QrCodeInfoResult.cs @@ -0,0 +1,9 @@ +using LINGYUN.Abp.Identity.QrCode; + +namespace LINGYUN.Abp.Account.Web.Areas.Account.Controllers.Models; + +public class QrCodeInfoResult +{ + public string Key { get; set; } + public QrCodeStatus Status { get; set; } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/QrCodeUserInfoResult.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/QrCodeUserInfoResult.cs new file mode 100644 index 000000000..8a9c4150b --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/Models/QrCodeUserInfoResult.cs @@ -0,0 +1,8 @@ +namespace LINGYUN.Abp.Account.Web.Areas.Account.Controllers.Models; + +public class QrCodeUserInfoResult : QrCodeInfoResult +{ + public string UserId { get; set; } + public string UserName { get; set; } + public string Picture { get; set; } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/QrCodeLoginController.cs b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/QrCodeLoginController.cs new file mode 100644 index 000000000..d3667cf79 --- /dev/null +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/QrCodeLoginController.cs @@ -0,0 +1,104 @@ +using Asp.Versioning; +using LINGYUN.Abp.Account.Web.Areas.Account.Controllers.Models; +using LINGYUN.Abp.Identity.QrCode; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Account; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Identity; +using Volo.Abp.Security.Claims; +using Volo.Abp.Users; + +namespace LINGYUN.Abp.Account.Web.Areas.Account.Controllers; + +[Controller] +[ControllerName("QrCodeLogin")] +[Area(AccountRemoteServiceConsts.ModuleName)] +[Route($"api/{AccountRemoteServiceConsts.ModuleName}/qrcode")] +[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)] +public class QrCodeLoginController : AbpControllerBase +{ + private readonly IdentityUserManager _userManager; + private readonly IQrCodeLoginProvider _qrCodeLoginProvider; + + public QrCodeLoginController( + IdentityUserManager userManager, + IQrCodeLoginProvider qrCodeLoginProvider) + { + _userManager = userManager; + _qrCodeLoginProvider = qrCodeLoginProvider; + } + + [HttpPost] + [Route("generate")] + [AllowAnonymous] + public async Task GenerateAsync() + { + var qrCodeInfo = await _qrCodeLoginProvider.GenerateAsync(); + + return new GenerateQrCodeResult + { + Key = qrCodeInfo.Key, + }; + } + + [HttpGet] + [Route("{key}/check")] + [AllowAnonymous] + public async Task CheckCodeAsync(string key) + { + var qrCodeInfo = await _qrCodeLoginProvider.GetCodeAsync(key); + + return new QrCodeUserInfoResult + { + Key = qrCodeInfo.Key, + Status = qrCodeInfo.Status, + Picture = qrCodeInfo.Picture, + UserId = qrCodeInfo.UserId, + UserName = qrCodeInfo.UserName, + }; + } + + [HttpPost] + [Route("{key}/scan")] + [Authorize] + public async Task ScanCodeAsync(string key) + { + var currentUser = await _userManager.GetByIdAsync(CurrentUser.GetId()); + + var picture = CurrentUser.FindClaim(AbpClaimTypes.Picture)?.Value; + var userName = CurrentUser.FindClaim(AbpClaimTypes.Name)?.Value ?? currentUser.UserName; + var userId = await _userManager.GetUserIdAsync(currentUser); + + var qrCodeInfo = await _qrCodeLoginProvider.ScanCodeAsync(key, + new QrCodeScanParams(userId, userName, picture, currentUser.TenantId)); + + return new QrCodeUserInfoResult + { + Key = qrCodeInfo.Key, + Status = qrCodeInfo.Status, + Picture = qrCodeInfo.Picture, + UserId = qrCodeInfo.UserId, + UserName = qrCodeInfo.UserName + }; + } + + [HttpPost] + [Route("{key}/confirm")] + [Authorize] + public async Task ConfirmCodeAsync(string key) + { + var qrCodeInfo = await _qrCodeLoginProvider.ConfirmCodeAsync(key); + + return new QrCodeUserInfoResult + { + Key = qrCodeInfo.Key, + Status = qrCodeInfo.Status, + Picture = qrCodeInfo.Picture, + UserId = qrCodeInfo.UserId, + UserName = qrCodeInfo.UserName + }; + } +} diff --git a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj index de3a3d203..ebeb4f3a4 100644 --- a/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj +++ b/aspnet-core/modules/account/LINGYUN.Abp.Account.Web/LINGYUN.Abp.Account.Web.csproj @@ -43,6 +43,7 @@ + diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/FodyWeavers.xml b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/FodyWeavers.xml new file mode 100644 index 000000000..00e1d9a1c --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/FodyWeavers.xsd b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN.Abp.Identity.AspNetCore.QrCode.csproj b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN.Abp.Identity.AspNetCore.QrCode.csproj new file mode 100644 index 000000000..94514ab9d --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN.Abp.Identity.AspNetCore.QrCode.csproj @@ -0,0 +1,24 @@ + + + + + + + net9.0 + LINGYUN.Abp.Identity.AspNetCore.QrCode + LINGYUN.Abp.Identity.AspNetCore.QrCode + false + false + false + + + + + + + + + + + + diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN/Abp/Identity/AspNetCore/QrCode/AbpIdentityAspNetCoreQrCodeModule.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN/Abp/Identity/AspNetCore/QrCode/AbpIdentityAspNetCoreQrCodeModule.cs new file mode 100644 index 000000000..940139628 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN/Abp/Identity/AspNetCore/QrCode/AbpIdentityAspNetCoreQrCodeModule.cs @@ -0,0 +1,20 @@ +using LINGYUN.Abp.Identity.QrCode; +using Microsoft.AspNetCore.Identity; +using Volo.Abp.Identity.AspNetCore; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.Identity.AspNetCore.QrCode; + +[DependsOn( + typeof(AbpIdentityQrCodeModule), + typeof(AbpIdentityAspNetCoreModule))] +public class AbpIdentityAspNetCoreQrCodeModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(builder => + { + builder.AddTokenProvider(QrCodeUserTokenProvider.ProviderName); + }); + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN/Abp/Identity/AspNetCore/QrCode/QrCodeUserTokenProvider.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN/Abp/Identity/AspNetCore/QrCode/QrCodeUserTokenProvider.cs new file mode 100644 index 000000000..a05674a85 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.AspNetCore.QrCode/LINGYUN/Abp/Identity/AspNetCore/QrCode/QrCodeUserTokenProvider.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using IdentityUser = Volo.Abp.Identity.IdentityUser; + +namespace LINGYUN.Abp.Identity.QrCode; + +public class QrCodeUserTokenProvider : DataProtectorTokenProvider +{ + public static string ProviderName => QrCodeLoginProviderConsts.Name; + public QrCodeUserTokenProvider( + IDataProtectionProvider dataProtectionProvider, + IOptions options, + ILogger> logger) + : base(dataProtectionProvider, options, logger) + { + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/FodyWeavers.xml b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/FodyWeavers.xsd b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN.Abp.Identity.QrCode.csproj b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN.Abp.Identity.QrCode.csproj new file mode 100644 index 000000000..814628966 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN.Abp.Identity.QrCode.csproj @@ -0,0 +1,25 @@ + + + + + + + net9.0 + LINGYUN.Abp.Identity.QrCode + LINGYUN.Abp.Identity.QrCode + false + false + false + + + + + + + + + + + + + diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/AbpIdentityQrCodeModule.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/AbpIdentityQrCodeModule.cs new file mode 100644 index 000000000..0a53421af --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/AbpIdentityQrCodeModule.cs @@ -0,0 +1,26 @@ +using Volo.Abp.Identity; +using Volo.Abp.Identity.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.Identity.QrCode; + +[DependsOn(typeof(AbpIdentityDomainModule))] +public class AbpIdentityQrCodeModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/Identity/QrCode/Localization/Resources"); + }); + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/IQrCodeLoginProvider.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/IQrCodeLoginProvider.cs new file mode 100644 index 000000000..457a8b72d --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/IQrCodeLoginProvider.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; + +namespace LINGYUN.Abp.Identity.QrCode; + +public interface IQrCodeLoginProvider +{ + Task GenerateAsync(); + + Task GetCodeAsync(string key); + + Task ScanCodeAsync(string key, QrCodeScanParams @params); + + Task ConfirmCodeAsync(string key); + + Task RemoveAsync(string key); +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/en.json b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/en.json new file mode 100644 index 000000000..405350931 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/en.json @@ -0,0 +1,8 @@ +{ + "culture": "en", + "texts": { + "QrCode:Invalid": "Invalid qrcode!", + "QrCode:NotScaned": "Qrcode not scaned!", + "QrCode:Scaned": "Please confirm the QR code." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..f28982da6 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/Localization/Resources/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "culture": "zh-Hans", + "texts": { + "QrCode:Invalid": "二维码已失效!", + "QrCode:NotScaned": "未扫描二维码!", + "QrCode:Scaned": "请确认二维码." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeCacheItem.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeCacheItem.cs new file mode 100644 index 000000000..cd8714285 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeCacheItem.cs @@ -0,0 +1,39 @@ +using System; +using Volo.Abp.MultiTenancy; +namespace LINGYUN.Abp.Identity.QrCode; + +[Serializable] +[IgnoreMultiTenancy] +public class QrCodeCacheItem +{ + public string Key { get; set; } + public string Token { get; set; } + public QrCodeStatus Status { get; set; } + public string UserId { get; set; } + public string UserName { get; set; } + public string Picture { get; set; } + public Guid? TenantId { get; set; } + public QrCodeCacheItem() + { + + } + public QrCodeCacheItem(string key) + { + Key = key; + Status = QrCodeStatus.Created; + } + + public QrCodeInfo GetQrCodeInfo() + { + var qrCodeInfo = new QrCodeInfo(Key) + { + UserId = UserId, + UserName = UserName, + Picture = Picture, + }; + qrCodeInfo.SetToken(Token); + qrCodeInfo.SetStatus(Status); + + return qrCodeInfo; + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeInfo.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeInfo.cs new file mode 100644 index 000000000..2040b46c6 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeInfo.cs @@ -0,0 +1,29 @@ +using System; + +namespace LINGYUN.Abp.Identity.QrCode; + +public class QrCodeInfo +{ + public string Key { get; } + public string Token { get; private set; } + public QrCodeStatus Status { get; private set; } + public string UserId { get; set; } + public string UserName { get; set; } + public string Picture { get; set; } + + public QrCodeInfo(string key) + { + Key = key; + Status = QrCodeStatus.Created; + } + + public void SetToken(string token) + { + Token = token; + } + + public void SetStatus(QrCodeStatus status) + { + Status = status; + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeLoginProvider.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeLoginProvider.cs new file mode 100644 index 000000000..c9fdd7297 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeLoginProvider.cs @@ -0,0 +1,143 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Localization; +using System; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.Caching; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; +using Volo.Abp.Identity.Localization; + +namespace LINGYUN.Abp.Identity.QrCode; + +public class QrCodeLoginProvider : IQrCodeLoginProvider, ITransientDependency +{ + protected IdentityUserManager UserManager { get; } + protected IStringLocalizer L { get; } + protected IDistributedCache QrCodeCache { get; } + + public QrCodeLoginProvider( + IStringLocalizer stringLocalizer, + IDistributedCache qrCodeCache, + IdentityUserManager userManager) + { + L = stringLocalizer; + QrCodeCache = qrCodeCache; + UserManager = userManager; + } + + public async virtual Task GenerateAsync() + { + var key = Guid.NewGuid().ToString("n"); + + var cacheItem = new QrCodeCacheItem(key); + + await QrCodeCache.SetAsync(key, cacheItem, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromSeconds(180), + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(300) + }); + + return cacheItem.GetQrCodeInfo(); + } + + public async virtual Task GetCodeAsync(string key) + { + var cacheItem = await QrCodeCache.GetAsync(key); + if (cacheItem == null) + { + var qrCodeInfo = new QrCodeInfo(key); + qrCodeInfo.SetStatus(QrCodeStatus.Invalid); + + return qrCodeInfo; + } + + return cacheItem.GetQrCodeInfo(); + } + + public async virtual Task ScanCodeAsync(string key, QrCodeScanParams @params) + { + var cacheItem = await QrCodeCache.GetAsync(key); + if (cacheItem == null) + { + var qrCodeInfo = new QrCodeInfo(key); + qrCodeInfo.SetStatus(QrCodeStatus.Invalid); + + return qrCodeInfo; + } + + if (cacheItem.Status == QrCodeStatus.Scaned) + { + return cacheItem.GetQrCodeInfo(); + } + + cacheItem.UserId = @params.UserId; + cacheItem.UserName = @params.UserName; + cacheItem.Picture = @params.Picture; + cacheItem.TenantId = @params.TenantId; + cacheItem.Status = QrCodeStatus.Scaned; + + await QrCodeCache.SetAsync(key, cacheItem, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromSeconds(180), + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(300) + }); + + return cacheItem.GetQrCodeInfo(); + } + + public async virtual Task ConfirmCodeAsync(string key) + { + var cacheItem = await QrCodeCache.GetAsync(key); + if (cacheItem == null) + { + var qrCodeInfo = new QrCodeInfo(key); + qrCodeInfo.SetStatus(QrCodeStatus.Invalid); + + return qrCodeInfo; + } + + if (cacheItem.Status == QrCodeStatus.Confirmed) + { + return cacheItem.GetQrCodeInfo(); + } + + if (cacheItem.UserId.IsNullOrWhiteSpace()) + { + throw new UserFriendlyException(L["QrCode:NotScaned"]); + } + + var token = await GenerateConfirmToken(cacheItem.UserId); + + cacheItem.Token = token; + cacheItem.Status = QrCodeStatus.Confirmed; + + await QrCodeCache.SetAsync(key, cacheItem, + new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromSeconds(180), + AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(300) + }); + + return cacheItem.GetQrCodeInfo(); + } + + public async virtual Task RemoveAsync(string key) + { + var cacheItem = await QrCodeCache.GetAsync(key); + if (cacheItem != null) + { + await QrCodeCache.RemoveAsync(key); + } + } + + protected async virtual Task GenerateConfirmToken(string userId) + { + var user = await UserManager.FindByIdAsync(userId); + + return await UserManager.GenerateUserTokenAsync(user, + QrCodeLoginProviderConsts.Name, + QrCodeLoginProviderConsts.Purpose); + }} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeLoginProviderConsts.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeLoginProviderConsts.cs new file mode 100644 index 000000000..785bb5be6 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeLoginProviderConsts.cs @@ -0,0 +1,10 @@ +namespace LINGYUN.Abp.Identity.QrCode; + +public static class QrCodeLoginProviderConsts +{ + public static string Name { get; set; } = "QrCode"; + + public static string Purpose { get; set; } = "QrCodeLogin"; + + public static string GrantType { get; set; } = "qr_code"; +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeScanParams.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeScanParams.cs new file mode 100644 index 000000000..1c4837c1e --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeScanParams.cs @@ -0,0 +1,22 @@ +using System; + +namespace LINGYUN.Abp.Identity.QrCode; + +public class QrCodeScanParams +{ + public string UserId { get; } + public string UserName { get; } + public string Picture { get; } + public Guid? TenantId { get; } + public QrCodeScanParams( + string userId, + string userName, + string picture = null, + Guid? tenantId = null) + { + UserId = userId; + UserName = userName; + Picture = picture; + TenantId = tenantId; + } +} diff --git a/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeStatus.cs b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeStatus.cs new file mode 100644 index 000000000..4ba2032e9 --- /dev/null +++ b/aspnet-core/modules/identity/LINGYUN.Abp.Identity.QrCode/LINGYUN/Abp/Identity/QrCode/QrCodeStatus.cs @@ -0,0 +1,9 @@ +namespace LINGYUN.Abp.Identity.QrCode; + +public enum QrCodeStatus +{ + Invalid = -1, + Created = 0, + Scaned = 5, + Confirmed = 10, +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.AspNetCore/LINGYUN/Abp/OpenIddict/AspNetCore/Controllers/UserInfoController.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.AspNetCore/LINGYUN/Abp/OpenIddict/AspNetCore/Controllers/UserInfoController.cs index 6c9d232b5..4bcd71716 100644 --- a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.AspNetCore/LINGYUN/Abp/OpenIddict/AspNetCore/Controllers/UserInfoController.cs +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.AspNetCore/LINGYUN/Abp/OpenIddict/AspNetCore/Controllers/UserInfoController.cs @@ -1,11 +1,11 @@ -using LINGYUN.Abp.Identity; -using OpenIddict.Abstractions; +using OpenIddict.Abstractions; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Security.Claims; +using Volo.Abp.Users; using VoloUserInfoController = Volo.Abp.OpenIddict.Controllers.UserInfoController; namespace LINGYUN.Abp.OpenIddict.AspNetCore.Controllers; @@ -26,7 +26,8 @@ public class UserInfoController : VoloUserInfoController var claims = new Dictionary(StringComparer.Ordinal) { // Note: the "sub" claim is a mandatory claim and must be included in the JSON response. - [OpenIddictConstants.Claims.Subject] = await UserManager.GetUserIdAsync(user) + [OpenIddictConstants.Claims.Subject] = await UserManager.GetUserIdAsync(user), + [AbpClaimTypes.SessionId] = CurrentUser.FindSessionId(), }; if (User.HasScope(OpenIddictConstants.Scopes.Profile)) @@ -36,7 +37,7 @@ public class UserInfoController : VoloUserInfoController claims[OpenIddictConstants.Claims.FamilyName] = user.Surname; claims[OpenIddictConstants.Claims.GivenName] = user.Name; // 重写添加用户头像 - var picture = user.Claims.FirstOrDefault(x => x.ClaimType == IdentityConsts.ClaimType.Avatar.Name); + var picture = user.Claims.FirstOrDefault(x => x.ClaimType == AbpClaimTypes.Picture); if (picture != null) { claims[OpenIddictConstants.Claims.Picture] = picture.ClaimValue; diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/FodyWeavers.xml b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/FodyWeavers.xsd b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/FodyWeavers.xsd new file mode 100644 index 000000000..3f3946e28 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/FodyWeavers.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + 'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed. + + + + + A comma-separated list of error codes that can be safely ignored in assembly verification. + + + + + 'false' to turn off automatic generation of the XML Schema file. + + + + + \ No newline at end of file diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN.Abp.OpenIddict.QrCode.csproj b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN.Abp.OpenIddict.QrCode.csproj new file mode 100644 index 000000000..8223db91b --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN.Abp.OpenIddict.QrCode.csproj @@ -0,0 +1,24 @@ + + + + + + + net9.0 + LINGYUN.Abp.OpenIddict.QrCode + LINGYUN.Abp.OpenIddict.QrCode + false + false + false + + + + + + + + + + + + diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN/Abp/OpenIddict/QrCode/AbpOpenIddictQrCodeModule.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN/Abp/OpenIddict/QrCode/AbpOpenIddictQrCodeModule.cs new file mode 100644 index 000000000..c17936561 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN/Abp/OpenIddict/QrCode/AbpOpenIddictQrCodeModule.cs @@ -0,0 +1,31 @@ +using LINGYUN.Abp.Identity.QrCode; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; +using Volo.Abp.OpenIddict; +using Volo.Abp.OpenIddict.ExtensionGrantTypes; + +namespace LINGYUN.Abp.OpenIddict.QrCode; + +[DependsOn( + typeof(AbpIdentityQrCodeModule), + typeof(AbpOpenIddictDomainModule))] +public class AbpOpenIddictQrCodeModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + PreConfigure(builder => + { + builder.AllowQrCodeFlow(); + }); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.Grants.TryAdd( + QrCodeLoginProviderConsts.GrantType, + new QrCodeTokenExtensionGrant()); + }); + } +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN/Abp/OpenIddict/QrCode/QrCodeTokenExtensionGrant.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN/Abp/OpenIddict/QrCode/QrCodeTokenExtensionGrant.cs new file mode 100644 index 000000000..c1c94d576 --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/LINGYUN/Abp/OpenIddict/QrCode/QrCodeTokenExtensionGrant.cs @@ -0,0 +1,204 @@ +using LINGYUN.Abp.Identity.QrCode; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Volo.Abp.Identity; +using Volo.Abp.MultiTenancy; +using Volo.Abp.OpenIddict; +using Volo.Abp.OpenIddict.ExtensionGrantTypes; +using IdentityUser = Volo.Abp.Identity.IdentityUser; +using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult; + +namespace LINGYUN.Abp.OpenIddict.QrCode; + +public class QrCodeTokenExtensionGrant : ITokenExtensionGrant +{ + public string Name => QrCodeLoginProviderConsts.GrantType; + + public async virtual Task HandleAsync(ExtensionGrantContext context) + { + var logger = GetRequiredService>(context); + + // 取出二维码Key + var qrcodeKey = context.Request.GetParameter("qrcode_key")?.ToString(); + if (qrcodeKey.IsNullOrWhiteSpace()) + { + logger.LogInformation("The user has not passed the QR code Key required for scanning and login."); + + var properties = new AuthenticationProperties( + new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The Qr code is invalid." + } + ); + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + var qrCodeProvider = GetRequiredService(context); + var qrCodeInfo = await qrCodeProvider.GetCodeAsync(qrcodeKey); + // 二维码扫描后用户Id不为空 + if (qrCodeInfo == null || qrCodeInfo.Token.IsNullOrWhiteSpace() == true) + { + logger.LogInformation("The QR code Key {0} is invalid or the user has not scanned the QR code.", qrcodeKey); + + var properties = new AuthenticationProperties( + new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The Qr code is invalid." + } + ); + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + Guid? tenantId = null; + var tenantIdValue = context.Request.GetParameter("tenant_id")?.ToString(); + if (!tenantIdValue.IsNullOrWhiteSpace() && Guid.TryParse(tenantIdValue, out var tenantGuid)) + { + tenantId = tenantGuid; + } + + var userManager = GetRequiredService(context); + var currentTenant = GetRequiredService(context); + + using (currentTenant.Change(tenantId)) + { + var user = await userManager.FindByIdAsync(qrCodeInfo.UserId); + if (user == null) + { + var properties = new AuthenticationProperties( + new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Invalid user id." + } + ); + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + if (!await userManager.VerifyUserTokenAsync(user, QrCodeLoginProviderConsts.Name, QrCodeLoginProviderConsts.Purpose, qrCodeInfo.Token)) + { + logger.LogInformation("Authentication failed for username: {username}, reason: the user token is invalid", user.UserName); + + var properties = new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = OpenIddictResources.GetResourceString(OpenIddictResources.ID2019), + }); + + await SaveSecurityLogAsync(context, user, OpenIddictSecurityLogActionConsts.LoginLockedout); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + // 检查是否已锁定 + if (await userManager.IsLockedOutAsync(user)) + { + logger.LogInformation("Authentication failed for username: {username}, reason: locked out", user.UserName); + + var properties = new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user account has been locked out due to invalid login attempts. Please wait a while and try again.", + }); + + await SaveSecurityLogAsync(context, user, OpenIddictSecurityLogActionConsts.LoginLockedout); + + return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } + + await qrCodeProvider.RemoveAsync(qrcodeKey); + + return await SetSuccessResultAsync(context, user, logger); + } + } + + protected virtual T GetRequiredService(ExtensionGrantContext context) + { + return context.HttpContext.RequestServices.GetRequiredService(); + } + + protected virtual Task FindClientIdAsync(ExtensionGrantContext context) + { + return Task.FromResult(context.Request.ClientId); + } + + protected async virtual Task SetSuccessResultAsync(ExtensionGrantContext context, IdentityUser user, ILogger logger) + { + logger.LogInformation("Credentials validated for username: {username}", user.UserName); + + var signInManager = GetRequiredService>(context); + + var principal = await signInManager.CreateUserPrincipalAsync(user); + + principal.SetScopes(context.Request.GetScopes()); + principal.SetResources(await GetResourcesAsync(context)); + + await SetClaimsDestinationsAsync(context, principal); + + await SaveSecurityLogAsync( + context, + user, + OpenIddictSecurityLogActionConsts.LoginSucceeded); + + return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, principal); + } + + protected async virtual Task SaveSecurityLogAsync(ExtensionGrantContext context, IdentityUser user, string action) + { + var logContext = new IdentitySecurityLogContext + { + Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict, + Action = action, + UserName = user.UserName, + ClientId = await FindClientIdAsync(context) + }; + logContext.WithProperty("GrantType", Name); + + var identitySecurityLogManager = GetRequiredService(context); + + await identitySecurityLogManager.SaveAsync(logContext); + } + + protected async virtual Task SetClaimsDestinationsAsync(ExtensionGrantContext context, ClaimsPrincipal principal) + { + var principalManager = GetRequiredService(context); + + await principalManager.HandleAsync(context.Request, principal); + } + + public virtual ForbidResult Forbid(AuthenticationProperties properties, params string[] authenticationSchemes) + { + return new ForbidResult( + authenticationSchemes, + properties); + } + + protected async virtual Task> GetResourcesAsync(ExtensionGrantContext context) + { + var scopes = context.Request.GetScopes(); + var resources = new List(); + if (!scopes.Any()) + { + return resources; + } + + var scopeManager = GetRequiredService(context); + + await foreach (var resource in scopeManager.ListResourcesAsync(scopes)) + { + resources.Add(resource); + } + return resources; + } +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/Microsoft/Extensions/DependencyInjection/QrCodeOpenIddictServerBuilderExtensions.cs b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/Microsoft/Extensions/DependencyInjection/QrCodeOpenIddictServerBuilderExtensions.cs new file mode 100644 index 000000000..4deb410cf --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/Microsoft/Extensions/DependencyInjection/QrCodeOpenIddictServerBuilderExtensions.cs @@ -0,0 +1,11 @@ +using LINGYUN.Abp.Identity.QrCode; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class QrCodeOpenIddictServerBuilderExtensions +{ + public static OpenIddictServerBuilder AllowQrCodeFlow(this OpenIddictServerBuilder builder) + { + return builder.AllowCustomFlow(QrCodeLoginProviderConsts.GrantType); + } +} diff --git a/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/README.md b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/README.md new file mode 100644 index 000000000..b68322a3c --- /dev/null +++ b/aspnet-core/modules/openIddict/LINGYUN.Abp.OpenIddict.QrCode/README.md @@ -0,0 +1,96 @@ +# LINGYUN.Abp.OpenIddict.QrCode + +[![ABP version](https://img.shields.io/badge/dynamic/xml?style=flat-square&color=yellow&label=abp&query=%2F%2FProject%2FPropertyGroup%2FAbpVersion&url=https%3A%2F%2Fraw.githubusercontent.com%2Fcolinin%2Fabp-next-admin%2Fmaster%2Faspnet-core%2Fmodules%2FopenIddict%2FLINGYUN.Abp.OpenIddict.QrCode%2FLINGYUN.Abp.OpenIddict.QrCode.csproj)](https://abp.io) +[![NuGet](https://img.shields.io/nuget/v/LINGYUN.Abp.OpenIddict.QrCode.svg?style=flat-square)](https://www.nuget.org/packages/LINGYUN.Abp.OpenIddict.QrCode) + +## 简介 + +`LINGYUN.Abp.OpenIddict.QrCode` 是 OpenIddict 的扫码登录认证扩展模块,提供了扫码登录认证功能。 + +[English](./README.EN.md) + +## 功能特性 + +* 扫码二维码登录 + +* 安全日志 + * 记录登录尝试 + * 记录登录失败 + * 记录密码修改 + +## 安装 + +```bash +dotnet add package LINGYUN.Abp.OpenIddict.QrCode +``` + +## 使用 + +1. 添加 `[DependsOn(typeof(AbpOpenIddictQrCodeModule))]` 到你的模块类。 + +2. 配置 OpenIddict 服务器: + +```csharp +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + PreConfigure(builder => + { + // 允许门户认证流程 + builder.AllowQrCodeFlow(); + }); +} +``` + +3. 使用示例: + +```http +POST /connect/token +Content-Type: application/x-www-form-urlencoded + +grant_type=qr_code_& +username=admin& +password=1q2w3E*& +enterpriseId=your-enterprise-id& +scope=openid profile +``` + +## 认证流程 + +1. 二维码验证 + * 用户提供二维码Key (qrcode_key_) + * 如未提供或无效,返回无效的二维码错误 + +## 参数说明 + +* qrcode_key_ (必填) + * 二维码Key + +* password (必填) + * 用户密码 + +* enterpriseId (必填) + * 企业ID,必须是有效的GUID格式 + +* TwoFactorProvider (可选) + * 双因素认证提供程序名称 + * 仅在启用双因素认证时需要 + +* TwoFactorCode (可选) + * 双因素认证码 + * 仅在启用双因素认证时需要 + +* ChangePasswordToken (可选) + * 修改密码令牌 + * 仅在需要修改密码时需要 + +* NewPassword (可选) + * 新密码 + * 仅在需要修改密码时需要 + +## 注意事项 + +* 企业ID必须是有效的GUID格式 +* 密码必须符合系统配置的密码策略 +* 双因素认证码有效期有限 +* 所有认证操作都会记录安全日志 +* 建议在生产环境中使用 HTTPS diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj index 3d1424502..3e8e3b41f 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj +++ b/aspnet-core/services/LY.MicroService.Applications.Single/LY.MicroService.Applications.Single.csproj @@ -188,6 +188,7 @@ + diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs index b68d3bbbb..853fbf9e3 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs +++ b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.Configure.cs @@ -1,3 +1,4 @@ +using LINGYUN.Abp.Identity.QrCode; using LY.MicroService.Applications.Single.DataSeeder; using VoloAbpExceptionHandlingOptions = Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingOptions; @@ -258,6 +259,7 @@ public partial class MicroServiceApplicationsSingleModule options.PersistentSessionGrantTypes.Add(WeChatTokenExtensionGrantConsts.OfficialGrantType); options.PersistentSessionGrantTypes.Add(WeChatTokenExtensionGrantConsts.MiniProgramGrantType); options.PersistentSessionGrantTypes.Add(AbpWeChatWorkGlobalConsts.GrantType); + options.PersistentSessionGrantTypes.Add(QrCodeLoginProviderConsts.GrantType); }); Configure(options => diff --git a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs index 7a0d9b000..6d2b1242f 100644 --- a/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs +++ b/aspnet-core/services/LY.MicroService.Applications.Single/MicroServiceApplicationsSingleModule.cs @@ -3,6 +3,7 @@ using LINGYUN.Abp.Gdpr; using LINGYUN.Abp.Gdpr.EntityFrameworkCore; using LINGYUN.Abp.Gdpr.Identity; using LINGYUN.Abp.Gdpr.Web; +using LINGYUN.Abp.OpenIddict.QrCode; namespace LY.MicroService.Applications.Single; @@ -123,6 +124,8 @@ namespace LY.MicroService.Applications.Single; typeof(AbpOpenIddictWeChatModule), // OpenIddict扩展模块 企业微信认证 typeof(AbpOpenIddictWeChatWorkModule), + // OpenIddict扩展模块 扫码登录 + typeof(AbpOpenIddictQrCodeModule), //typeof(AbpOssManagementMinioModule), // 取消注释以使用Minio // 对象存储模块 文件系统