Browse Source

feat(account): add external login binding

pull/1318/head
colin 5 months ago
parent
commit
7993114797
  1. 7
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginInfoDto.cs
  2. 8
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginResultDto.cs
  3. 11
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RemoveExternalLoginInput.cs
  4. 7
      aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/UserLoginInfoDto.cs
  5. 25
      aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN.Abp.Account.WeChat.Work.csproj
  6. 27
      aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/AbpAccountWeChatWorkModule.cs
  7. 23
      aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/Controllers/WeChatWorkAccountController.cs
  8. 1
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs
  9. 111
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs
  10. 4
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml
  11. 117
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml.cs
  12. 65
      aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs
  13. 2
      aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs

7
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginInfoDto.cs

@ -0,0 +1,7 @@
namespace LINGYUN.Abp.Account;
public class ExternalLoginInfoDto
{
public string Name { get; set; }
public string DisplayName { get; set; }
}

8
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/ExternalLoginResultDto.cs

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace LINGYUN.Abp.Account;
public class ExternalLoginResultDto
{
public List<UserLoginInfoDto> UserLogins { get; set; } = new List<UserLoginInfoDto>();
public List<ExternalLoginInfoDto> ExternalLogins { get; set; } = new List<ExternalLoginInfoDto>();
}

11
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/RemoveExternalLoginInput.cs

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace LINGYUN.Abp.Account;
public class RemoveExternalLoginInput
{
[Required]
public string LoginProvider { get; set; }
[Required]
public string ProviderKey { get; set; }
}

7
aspnet-core/modules/account/LINGYUN.Abp.Account.Application.Contracts/LINGYUN/Abp/Account/Dto/UserLoginInfoDto.cs

@ -0,0 +1,7 @@
namespace LINGYUN.Abp.Account;
public class UserLoginInfoDto
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
public string ProviderDisplayName { get; set; }
}

25
aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN.Abp.Account.WeChat.Work.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\configureawait.props" />
<Import Project="..\..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<AssemblyName>LINGYUN.Abp.Account.WeChat.Work</AssemblyName>
<PackageId>LINGYUN.Abp.Account.WeChat.Work</PackageId>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\framework\wechat\LINGYUN.Abp.WeChat.Work\LINGYUN.Abp.WeChat.Work.csproj" />
<ProjectReference Include="..\..\identity\LINGYUN.Abp.Identity.Domain\LINGYUN.Abp.Identity.Domain.csproj" />
</ItemGroup>
</Project>

27
aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/AbpAccountWeChatWorkModule.cs

@ -0,0 +1,27 @@
using LINGYUN.Abp.WeChat.Work;
using LINGYUN.Abp.WeChat.Work.Localization;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Localization;
using Volo.Abp.Modularity;
namespace LINGYUN.Abp.Account.WeChat.Work;
[DependsOn(
typeof(AbpWeChatWorkModule),
typeof(AbpAspNetCoreMvcModule))]
public class AbpAccountWeChatWorkModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
PreConfigure<AbpMvcDataAnnotationsLocalizationOptions>(options =>
{
options.AddAssemblyResource(typeof(WeChatWorkResource), typeof(AbpAccountWeChatWorkModule).Assembly);
});
PreConfigure<IMvcBuilder>(mvcBuilder =>
{
mvcBuilder.AddApplicationPartIfNotExists(typeof(AbpAccountWeChatWorkModule).Assembly);
});
}
}

23
aspnet-core/modules/account/LINGYUN.Abp.Account.WeChat.Work/LINGYUN/Abp/Account/WeChat/Work/Controllers/WeChatWorkAccountController.cs

@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Identity;
using Volo.Abp.Users;
namespace LINGYUN.Abp.Account.WeChat.Work.Controllers;
[Authorize]
public class WeChatWorkAccountController : AbpControllerBase
{
private readonly IdentityUserManager identityUserManager;
/// <summary>
/// 绑定用户
/// </summary>
/// <param name="code"></param>
/// <returns></returns>
public async virtual Task BindAsync(string code)
{
var user = await identityUserManager.GetByIdAsync(CurrentUser.GetId());
}
}

1
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/AbpAccountAuthenticationTypes.cs

@ -3,4 +3,5 @@
public static class AbpAccountAuthenticationTypes public static class AbpAccountAuthenticationTypes
{ {
public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword"; public const string ShouldChangePassword = "Abp.Account.ShouldChangePassword";
public const string ConfirmUserScheme = "Abp.Account.ConfirmUser";
} }

111
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Areas/Account/Controllers/AccountController.cs

@ -0,0 +1,111 @@
using LINGYUN.Abp.Account.Web.ExternalProviders;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.Account;
using Volo.Abp.Account.Localization;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.Users;
namespace LINGYUN.Abp.Account.Web.Areas.Account.Controllers;
[Controller]
[Area(AccountRemoteServiceConsts.ModuleName)]
[Route($"api/{AccountRemoteServiceConsts.ModuleName}")]
[RemoteService(Name = AccountRemoteServiceConsts.RemoteServiceName)]
public class AccountController : AbpController
{
protected AbpSignInManager SignInManager => LazyServiceProvider.LazyGetRequiredService<AbpSignInManager>();
protected IdentityUserManager UserManager => LazyServiceProvider.LazyGetRequiredService<IdentityUserManager>();
protected IExternalProviderService ExternalProviderService => LazyServiceProvider.LazyGetRequiredService<IExternalProviderService>();
public AccountController()
{
LocalizationResource = typeof(AccountResource);
}
[HttpGet]
[Authorize]
[Route("external-logins")]
public async virtual Task<ExternalLoginResultDto> GetExternalLoginsAsync()
{
var currentUser = await UserManager.GetByIdAsync(CurrentUser.GetId());
var userLogins = await UserManager.GetLoginsAsync(currentUser);
var externalProviders = await ExternalProviderService.GetAllAsync();
return new ExternalLoginResultDto
{
UserLogins = userLogins.Select(x => new UserLoginInfoDto
{
ProviderDisplayName = x.ProviderDisplayName,
ProviderKey = x.ProviderKey,
LoginProvider = x.LoginProvider,
}).ToList(),
ExternalLogins = externalProviders.Select(x =>
{
return new ExternalLoginInfoDto
{
Name = x.Name,
DisplayName = x.DisplayName,
};
}).ToList(),
};
}
[HttpDelete]
[Authorize]
[Route("external-logins/remove")]
public async virtual Task RemoveExternalLoginAsync(RemoveExternalLoginInput input)
{
var currentUser = await UserManager.GetByIdAsync(CurrentUser.GetId());
var identityResult = await UserManager.RemoveLoginAsync(
currentUser,
input.LoginProvider,
input.ProviderKey);
if (!identityResult.Succeeded)
{
throw new UserFriendlyException("Operation failed: " + identityResult.Errors.Select(e => $"[{e.Code}] {e.Description}").JoinAsString(", "));
}
// 解绑的是当前身份认证方案则退出登录
var amr = CurrentUser.FindClaimValue(ClaimTypes.AuthenticationMethod);
if (!amr.IsNullOrWhiteSpace() && string.Equals(amr, input.LoginProvider, StringComparison.InvariantCultureIgnoreCase))
{
await SignInManager.SignOutAsync();
}
}
[HttpGet]
[Authorize]
[Route("external-logins/bind")]
public virtual async Task<IActionResult> ExternalLoginBindAsync(string provider, string returnUrl)
{
if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(returnUrl))
{
Logger.LogWarning("The parameter is incorrect");
return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary<string, string>()
{
["error"] = "The parameter is incorrect"
}));
}
var tenantId = CurrentTenant.Id;
var userId = CurrentUser.GetId();
var redirectUrl = Url.Page("/Account/ExternalLoginBind", pageHandler: "BindCallback", values: new { returnUrl, userId, tenantId });
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, userId.ToString());
properties.Items["scheme"] = provider;
return await Task.FromResult(Challenge(properties, provider));
}
}

4
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml

@ -0,0 +1,4 @@
@page
@model LINGYUN.Abp.Account.Web.Pages.Account.ExternalLoginBindModel
@{
}

117
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/ExternalLoginBind.cshtml.cs

@ -0,0 +1,117 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc.UI.RazorPages;
using Volo.Abp.Identity;
using Volo.Abp.Identity.AspNetCore;
using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace LINGYUN.Abp.Account.Web.Pages.Account;
[IgnoreAntiforgeryToken]
public class ExternalLoginBindModel : AbpPageModel
{
protected AbpSignInManager SignInManager => LazyServiceProvider.LazyGetRequiredService<AbpSignInManager>();
protected IdentityUserManager UserManager => LazyServiceProvider.LazyGetRequiredService<IdentityUserManager>();
protected IOptions<IdentityOptions> IdentityOptions => LazyServiceProvider.LazyGetRequiredService<IOptions<IdentityOptions>>();
public virtual async Task<IActionResult> OnGetAsync()
{
return await Task.FromResult(NotFound());
}
/// <summary>
/// SPA绑定第三方绑定
/// </summary>
/// <param name="provider">第三方提供者</param>
/// <param name="returnUrl">绑定成功回调地址</param>
/// <returns>IActionResult</returns>
public virtual async Task<IActionResult> OnPostAsync(string provider, string returnUrl)
{
if (string.IsNullOrWhiteSpace(provider) || string.IsNullOrWhiteSpace(returnUrl))
{
Logger.LogWarning("The parameter is incorrect");
return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary<string, string>()
{
["error"] = "The parameter is incorrect"
}));
}
var tenantId = CurrentTenant.Id;
Logger.LogInformation("CurrentTenant:{TenantId}", tenantId);
var userId = CurrentUser.GetId();
Logger.LogInformation("CurrentUser:{UserId}", userId);
var redirectUrl = Url.Page("ExternalLoginBind", pageHandler: "BindCallback", values: new { returnUrl, userId, tenantId });
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, userId.ToString());
properties.Items["scheme"] = provider;
return await Task.FromResult(Challenge(properties, provider));
}
/// <summary>
/// SPA绑定第三方登录回调
/// </summary>
/// <param name="returnUrl">绑定成功回调地址</param>
/// <param name="userId">用户Id</param>
/// <param name="tenantId">租户Id</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
[UnitOfWork]
public virtual async Task<IActionResult> OnGetBindCallbackAsync(string returnUrl, string userId, Guid? tenantId)
{
if (string.IsNullOrWhiteSpace(returnUrl))
{
Logger.LogWarning("The returnUrl cannot be empty");
return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary<string, string>()
{
["error"] = "The returnUrl cannot be empty"
}));
}
using (CurrentTenant.Change(tenantId))
{
Logger.LogInformation("CurrentTenant:{TenantId}", tenantId);
await IdentityOptions.SetAsync();
var loginInfo = await SignInManager.GetExternalLoginInfoAsync(userId);
if (loginInfo == null)
{
Logger.LogWarning("External login info is not available");
return Redirect(QueryHelpers.AddQueryString(returnUrl, new Dictionary<string, string>()
{
["error"] = "External login info is not available."
}));
}
await SignInManager.SignOutAsync();
if (await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey) == null)
{
var externalUser = await UserManager.FindByIdAsync(userId);
CheckIdentityErrors(await UserManager.AddLoginAsync(externalUser, loginInfo));
}
return Redirect(returnUrl);
}
}
protected virtual void CheckIdentityErrors(IdentityResult identityResult)
{
if (!identityResult.Succeeded)
{
throw new UserFriendlyException("Operation failed: " + identityResult.Errors.Select(e => $"[{e.Code}] {e.Description}").JoinAsString(", "));
}
}
}

65
aspnet-core/modules/account/LINGYUN.Abp.Account.Web/Pages/Account/Login.cshtml.cs

@ -361,13 +361,15 @@ public class LoginModel : AccountPageModel
if (result.IsLockedOut) if (result.IsLockedOut)
{ {
Logger.LogWarning($"External login callback error: user is locked out!"); Logger.LogWarning($"External login callback error: user is locked out!");
throw new UserFriendlyException("Cannot proceed because user is locked out!");
return await HandleUserLockedOut();
} }
if (result.IsNotAllowed) if (result.IsNotAllowed)
{ {
Logger.LogWarning($"External login callback error: user is not allowed!"); Logger.LogWarning($"External login callback error: user is not allowed!");
throw new UserFriendlyException("Cannot proceed because user is not allowed!");
return await HandleExternalLoginNotAllowed(loginInfo);
} }
IdentityUser user; IdentityUser user;
@ -525,14 +527,7 @@ public class LoginModel : AccountPageModel
// Óû§±ØÐëÐÞ¸ÄÃÜÂë // Óû§±ØÐëÐÞ¸ÄÃÜÂë
if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser)) if (notAllowedUser.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(notAllowedUser))
{ {
var changePwdIdentity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ShouldChangePassword); await StoreChangePasswordUserAsync(notAllowedUser);
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.UserId, notAllowedUser.Id.ToString()));
if (notAllowedUser.TenantId.HasValue)
{
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.TenantId, notAllowedUser.TenantId.ToString()));
}
await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ShouldChangePassword, new ClaimsPrincipal(changePwdIdentity));
return RedirectToPage("ChangePassword", new return RedirectToPage("ChangePassword", new
{ {
@ -546,6 +541,56 @@ public class LoginModel : AccountPageModel
return Page(); return Page();
} }
protected async virtual Task<IActionResult> HandleExternalLoginNotAllowed(ExternalLoginInfo loginInfo)
{
Logger.LogWarning("External login callback error: User is Not Allowed!");
var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
if (user == null)
{
Logger.LogWarning($"External login callback error: User is Not Found!");
return RedirectToPage("./Login");
}
if (user.ShouldChangePasswordOnNextLogin || await UserManager.ShouldPeriodicallyChangePasswordAsync(user))
{
await StoreChangePasswordUserAsync(user);
return RedirectToPage("./ChangePassword", new
{
ReturnUrl,
ReturnUrlHash
});
}
Alerts.Warning(L["LoginIsNotAllowed"]);
return Page();
}
protected virtual async Task StoreChangePasswordUserAsync(IdentityUser user)
{
var changePwdIdentity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ShouldChangePassword);
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.UserId, user.Id.ToString()));
if (user.TenantId.HasValue)
{
changePwdIdentity.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString()));
}
await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ShouldChangePassword, new ClaimsPrincipal(changePwdIdentity));
}
protected virtual async Task StoreConfirmUserAsync(IdentityUser user)
{
var identity = new ClaimsIdentity(AbpAccountAuthenticationTypes.ConfirmUserScheme);
identity.AddClaim(new Claim(AbpClaimTypes.UserId, user.Id.ToString()));
if (user.TenantId.HasValue)
{
identity.AddClaim(new Claim(AbpClaimTypes.TenantId, user.TenantId.ToString()));
}
await HttpContext.SignInAsync(AbpAccountAuthenticationTypes.ConfirmUserScheme, new ClaimsPrincipal(identity));
}
protected virtual Task<IActionResult> HandleUserNameOrPasswordInvalid() protected virtual Task<IActionResult> HandleUserNameOrPasswordInvalid()
{ {
Alerts.Danger(L["InvalidUserNameOrPassword"]); Alerts.Danger(L["InvalidUserNameOrPassword"]);

2
aspnet-core/modules/realtime-notifications/LINGYUN.Abp.Notifications.WeChat.Work/LINGYUN/Abp/Notifications/WeChat/Work/WeChatWorkNotificationPublishProvider.cs

@ -24,7 +24,7 @@ public class WeChatWorkNotificationPublishProvider : NotificationPublishProvider
protected ISettingProvider SettingProvider => ServiceProvider.LazyGetRequiredService<ISettingProvider>(); protected ISettingProvider SettingProvider => ServiceProvider.LazyGetRequiredService<ISettingProvider>();
protected IStringLocalizerFactory LocalizerFactory => ServiceProvider.LazyGetRequiredService<IStringLocalizerFactory>(); protected IStringLocalizerFactory LocalizerFactory => ServiceProvider.LazyGetRequiredService<IStringLocalizerFactory>();
protected IWeChatWorkMessageSender WeChatWorkMessageSender => ServiceProvider.LazyGetRequiredService<IWeChatWorkMessageSender>(); protected IWeChatWorkMessageSender WeChatWorkMessageSender => ServiceProvider.LazyGetRequiredService<IWeChatWorkMessageSender>();
protected IWeChatWorkInternalUserFinder WeChatWorkInternalUserFinder => ServiceProvider.LazyGetRequiredService<IWeChatWorkInternalUserFinder>(); protected IWeChatWorkUserClaimProvider WeChatWorkInternalUserFinder => ServiceProvider.LazyGetRequiredService<IWeChatWorkUserClaimProvider>();
protected INotificationDefinitionManager NotificationDefinitionManager => ServiceProvider.LazyGetRequiredService<INotificationDefinitionManager>(); protected INotificationDefinitionManager NotificationDefinitionManager => ServiceProvider.LazyGetRequiredService<INotificationDefinitionManager>();
protected async override Task<bool> CanPublishAsync(NotificationInfo notification, CancellationToken cancellationToken = default) protected async override Task<bool> CanPublishAsync(NotificationInfo notification, CancellationToken cancellationToken = default)

Loading…
Cancel
Save