committed by
GitHub
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