36 changed files with 1025 additions and 4 deletions
@ -0,0 +1,6 @@ |
|||
namespace LINGYUN.Abp.Account.Web.Areas.Account.Controllers.Models; |
|||
|
|||
public class GenerateQrCodeResult |
|||
{ |
|||
public string Key { get; set; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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<GenerateQrCodeResult> GenerateAsync() |
|||
{ |
|||
var qrCodeInfo = await _qrCodeLoginProvider.GenerateAsync(); |
|||
|
|||
return new GenerateQrCodeResult |
|||
{ |
|||
Key = qrCodeInfo.Key, |
|||
}; |
|||
} |
|||
|
|||
[HttpGet] |
|||
[Route("{key}/check")] |
|||
[AllowAnonymous] |
|||
public async Task<QrCodeUserInfoResult> 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<QrCodeUserInfoResult> 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<QrCodeUserInfoResult> 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 |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net9.0</TargetFramework> |
|||
<AssemblyName>LINGYUN.Abp.Identity.AspNetCore.QrCode</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Identity.AspNetCore.QrCode</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Identity.AspNetCore" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Identity.QrCode\LINGYUN.Abp.Identity.QrCode.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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<IdentityBuilder>(builder => |
|||
{ |
|||
builder.AddTokenProvider<QrCodeUserTokenProvider>(QrCodeUserTokenProvider.ProviderName); |
|||
}); |
|||
} |
|||
} |
|||
@ -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<IdentityUser> |
|||
{ |
|||
public static string ProviderName => QrCodeLoginProviderConsts.Name; |
|||
public QrCodeUserTokenProvider( |
|||
IDataProtectionProvider dataProtectionProvider, |
|||
IOptions<DataProtectionTokenProviderOptions> options, |
|||
ILogger<DataProtectorTokenProvider<IdentityUser>> logger) |
|||
: base(dataProtectionProvider, options, logger) |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,25 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFrameworks>net9.0</TargetFrameworks> |
|||
<AssemblyName>LINGYUN.Abp.Identity.QrCode</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.Identity.QrCode</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<Content Remove="LINGYUN\Abp\Identity\QrCode\Localization\Resources\*.json" /> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Identity\QrCode\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Identity.Domain" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpIdentityQrCodeModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<IdentityResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/Identity/QrCode/Localization/Resources"); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.Identity.QrCode; |
|||
|
|||
public interface IQrCodeLoginProvider |
|||
{ |
|||
Task<QrCodeInfo> GenerateAsync(); |
|||
|
|||
Task<QrCodeInfo> GetCodeAsync(string key); |
|||
|
|||
Task<QrCodeInfo> ScanCodeAsync(string key, QrCodeScanParams @params); |
|||
|
|||
Task<QrCodeInfo> ConfirmCodeAsync(string key); |
|||
|
|||
Task RemoveAsync(string key); |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"culture": "en", |
|||
"texts": { |
|||
"QrCode:Invalid": "Invalid qrcode!", |
|||
"QrCode:NotScaned": "Qrcode not scaned!", |
|||
"QrCode:Scaned": "Please confirm the QR code." |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"QrCode:Invalid": "二维码已失效!", |
|||
"QrCode:NotScaned": "未扫描二维码!", |
|||
"QrCode:Scaned": "请确认二维码." |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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<IdentityResource> L { get; } |
|||
protected IDistributedCache<QrCodeCacheItem> QrCodeCache { get; } |
|||
|
|||
public QrCodeLoginProvider( |
|||
IStringLocalizer<IdentityResource> stringLocalizer, |
|||
IDistributedCache<QrCodeCacheItem> qrCodeCache, |
|||
IdentityUserManager userManager) |
|||
{ |
|||
L = stringLocalizer; |
|||
QrCodeCache = qrCodeCache; |
|||
UserManager = userManager; |
|||
} |
|||
|
|||
public async virtual Task<QrCodeInfo> 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<QrCodeInfo> 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<QrCodeInfo> 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<QrCodeInfo> 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<string> GenerateConfirmToken(string userId) |
|||
{ |
|||
var user = await UserManager.FindByIdAsync(userId); |
|||
|
|||
return await UserManager.GenerateUserTokenAsync(user, |
|||
QrCodeLoginProviderConsts.Name, |
|||
QrCodeLoginProviderConsts.Purpose); |
|||
}} |
|||
@ -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"; |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace LINGYUN.Abp.Identity.QrCode; |
|||
|
|||
public enum QrCodeStatus |
|||
{ |
|||
Invalid = -1, |
|||
Created = 0, |
|||
Scaned = 5, |
|||
Confirmed = 10, |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,30 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> |
|||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. --> |
|||
<xs:element name="Weavers"> |
|||
<xs:complexType> |
|||
<xs:all> |
|||
<xs:element name="ConfigureAwait" minOccurs="0" maxOccurs="1"> |
|||
<xs:complexType> |
|||
<xs:attribute name="ContinueOnCapturedContext" type="xs:boolean" /> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:all> |
|||
<xs:attribute name="VerifyAssembly" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string"> |
|||
<xs:annotation> |
|||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
<xs:attribute name="GenerateXsd" type="xs:boolean"> |
|||
<xs:annotation> |
|||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation> |
|||
</xs:annotation> |
|||
</xs:attribute> |
|||
</xs:complexType> |
|||
</xs:element> |
|||
</xs:schema> |
|||
@ -0,0 +1,24 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net9.0</TargetFramework> |
|||
<AssemblyName>LINGYUN.Abp.OpenIddict.QrCode</AssemblyName> |
|||
<PackageId>LINGYUN.Abp.OpenIddict.QrCode</PackageId> |
|||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> |
|||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> |
|||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.OpenIddict.AspNetCore" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\identity\LINGYUN.Abp.Identity.QrCode\LINGYUN.Abp.Identity.QrCode.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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<OpenIddictServerBuilder>(builder => |
|||
{ |
|||
builder.AllowQrCodeFlow(); |
|||
}); |
|||
} |
|||
|
|||
public override void ConfigureServices(ServiceConfigurationContext context) |
|||
{ |
|||
Configure<AbpOpenIddictExtensionGrantsOptions>(options => |
|||
{ |
|||
options.Grants.TryAdd( |
|||
QrCodeLoginProviderConsts.GrantType, |
|||
new QrCodeTokenExtensionGrant()); |
|||
}); |
|||
} |
|||
} |
|||
@ -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<IActionResult> HandleAsync(ExtensionGrantContext context) |
|||
{ |
|||
var logger = GetRequiredService<ILogger<QrCodeTokenExtensionGrant>>(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<string, string> |
|||
{ |
|||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, |
|||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The Qr code is invalid." |
|||
} |
|||
); |
|||
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); |
|||
} |
|||
|
|||
var qrCodeProvider = GetRequiredService<IQrCodeLoginProvider>(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<string, string> |
|||
{ |
|||
[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<IdentityUserManager>(context); |
|||
var currentTenant = GetRequiredService<ICurrentTenant>(context); |
|||
|
|||
using (currentTenant.Change(tenantId)) |
|||
{ |
|||
var user = await userManager.FindByIdAsync(qrCodeInfo.UserId); |
|||
if (user == null) |
|||
{ |
|||
var properties = new AuthenticationProperties( |
|||
new Dictionary<string, string> |
|||
{ |
|||
[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<string, string> |
|||
{ |
|||
[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<string, string> |
|||
{ |
|||
[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<T>(ExtensionGrantContext context) |
|||
{ |
|||
return context.HttpContext.RequestServices.GetRequiredService<T>(); |
|||
} |
|||
|
|||
protected virtual Task<string> FindClientIdAsync(ExtensionGrantContext context) |
|||
{ |
|||
return Task.FromResult(context.Request.ClientId); |
|||
} |
|||
|
|||
protected async virtual Task<IActionResult> SetSuccessResultAsync(ExtensionGrantContext context, IdentityUser user, ILogger<QrCodeTokenExtensionGrant> logger) |
|||
{ |
|||
logger.LogInformation("Credentials validated for username: {username}", user.UserName); |
|||
|
|||
var signInManager = GetRequiredService<SignInManager<IdentityUser>>(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<IdentitySecurityLogManager>(context); |
|||
|
|||
await identitySecurityLogManager.SaveAsync(logContext); |
|||
} |
|||
|
|||
protected async virtual Task SetClaimsDestinationsAsync(ExtensionGrantContext context, ClaimsPrincipal principal) |
|||
{ |
|||
var principalManager = GetRequiredService<AbpOpenIddictClaimsPrincipalManager>(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<IEnumerable<string>> GetResourcesAsync(ExtensionGrantContext context) |
|||
{ |
|||
var scopes = context.Request.GetScopes(); |
|||
var resources = new List<string>(); |
|||
if (!scopes.Any()) |
|||
{ |
|||
return resources; |
|||
} |
|||
|
|||
var scopeManager = GetRequiredService<IOpenIddictScopeManager>(context); |
|||
|
|||
await foreach (var resource in scopeManager.ListResourcesAsync(scopes)) |
|||
{ |
|||
resources.Add(resource); |
|||
} |
|||
return resources; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
# LINGYUN.Abp.OpenIddict.QrCode |
|||
|
|||
[](https://abp.io) |
|||
[](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<OpenIddictServerBuilder>(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 |
|||
Loading…
Reference in new issue