using System.Text;
using Dpz.Core.Authenticator;
using Dpz.Core.Public.Entity.Auth;
using Dpz.Core.Public.ViewModel.WebAuthn;
using Dpz.Core.Service.Mediator.Features.Security.Commands;
using Microsoft.AspNetCore.Authentication;
using OpenIddict.Abstractions;
using OpenIddict.Core;
namespace Dpz.Core.Auth.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthApiController(
ILogger<AuthApiController> logger,
IAccountLoginHistoryService accountLoginHistoryService,
IUserTwoFactorService userTwoFactorService,
IUserMfaVerificationService mfaVerificationService,
IUserPasskeySessionService passkeySessionService,
SignInManager<VmUserInfo> signInManager,
IMediator mediator,
OpenIddictAuthorizationManager<DpzAuthorization> authorizationManager,
IOpenIddictTokenManager tokenManager
) : ControllerBase
{
[Authorize]
[HttpGet("security-bindings")]
public async Task<ResponseResult<UserSecurityBindingStatusResponse>> SecurityBindings(
CancellationToken cancellationToken = default
)
{
var account = User.NameIdentifier;
if (string.IsNullOrWhiteSpace(account))
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("用户未登录");
}
var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
return ResponseResult<UserSecurityBindingStatusResponse>.Ok(status);
}
[Authorize]
[HttpGet("two-factor/setup")]
public async Task<ResponseResult<UserTwoFactorSetupResponse>> TwoFactorSetup(
CancellationToken cancellationToken = default
)
{
var account = User.NameIdentifier;
if (string.IsNullOrWhiteSpace(account))
{
return ResponseResult<UserTwoFactorSetupResponse>.Fail("用户未登录");
}
var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
if (status.HasTwoFactor)
{
return ResponseResult<UserTwoFactorSetupResponse>.Fail("您已绑定双因素认证");
}
var challenge = await userTwoFactorService.BeginSetupAsync(account, cancellationToken);
if (challenge == null)
{
logger.LogWarning("获取用户密钥失败,账号:{Account}", account);
return ResponseResult<UserTwoFactorSetupResponse>.Fail("获取双因素密钥失败");
}
var twoFactorAuthenticator = new TwoFactorAuthenticator();
var setupInfo = twoFactorAuthenticator.GenerateSetupCode(
issuer: "叫我阿胖",
accountTitleNoSpaces: account,
accountSecretKey: challenge.Key,
secretIsBase32: false
);
return ResponseResult<UserTwoFactorSetupResponse>.Ok(
new UserTwoFactorSetupResponse
{
SetupToken = challenge.SetupToken,
Account = setupInfo.Account,
ManualEntryKey = setupInfo.ManualEntryKey,
QrCodeSetupImageUrl = setupInfo.QrCodeSetupImageUrl,
SetupUrl = setupInfo.SetupUrl,
}
);
}
[Authorize]
[ValidateAntiForgeryToken]
[HttpPost("two-factor/bind")]
public async Task<ResponseResult<UserSecurityBindingStatusResponse>> BindTwoFactor(
[FromBody] BindTwoFactorRequest? request,
CancellationToken cancellationToken = default
)
{
if (request == null || string.IsNullOrWhiteSpace(request.Value.PinCode))
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("请输入双因素验证码");
}
if (string.IsNullOrWhiteSpace(request.Value.SetupToken))
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("双因素绑定请求已过期");
}
var account = User.NameIdentifier;
if (string.IsNullOrWhiteSpace(account))
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("用户未登录");
}
var currentStatus = await userTwoFactorService.GetBindingStatusAsync(
account,
cancellationToken
);
if (currentStatus.HasTwoFactor)
{
return ResponseResult<UserSecurityBindingStatusResponse>.Ok(
currentStatus,
"您已绑定双因素认证"
);
}
var key = await userTwoFactorService.GetSetupKeyAsync(
account,
request.Value.SetupToken,
cancellationToken
);
if (string.IsNullOrWhiteSpace(key))
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail(
"双因素绑定请求已过期,请重新获取二维码"
);
}
var twoFactorAuthenticator = new TwoFactorAuthenticator();
var keyBuffer = Encoding.UTF8.GetBytes(key);
var keyBase32 = Base32Encoding.ToString(keyBuffer);
var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(
keyBase32,
request.Value.PinCode,
true
);
if (!twoFactorResult)
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("PIN码验证失败");
}
await userTwoFactorService.BindAsync(account, request.Value.SetupToken, cancellationToken);
currentStatus = await userTwoFactorService.GetBindingStatusAsync(
account,
cancellationToken
);
return ResponseResult<UserSecurityBindingStatusResponse>.Ok(
currentStatus,
"双因素认证绑定成功"
);
}
[Authorize]
[ValidateAntiForgeryToken]
[HttpPost("two-factor/unbind")]
public async Task<ResponseResult<UserSecurityBindingStatusResponse>> UnbindTwoFactor(
[FromBody] BindTwoFactorRequest? request,
CancellationToken cancellationToken = default
)
{
if (request == null || string.IsNullOrWhiteSpace(request.Value.PinCode))
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("请输入双因素验证码");
}
var account = User.NameIdentifier;
if (string.IsNullOrWhiteSpace(account))
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("用户未登录");
}
var key = await userTwoFactorService.GetBoundKeyAsync(account, cancellationToken);
if (string.IsNullOrWhiteSpace(key))
{
var currentStatus = await userTwoFactorService.GetBindingStatusAsync(
account,
cancellationToken
);
return ResponseResult<UserSecurityBindingStatusResponse>.Ok(
currentStatus,
"您未绑定双因素认证"
);
}
var twoFactorAuthenticator = new TwoFactorAuthenticator();
var keyBuffer = Encoding.UTF8.GetBytes(key);
var keyBase32 = Base32Encoding.ToString(keyBuffer);
var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(
keyBase32,
request.Value.PinCode,
true
);
if (!twoFactorResult)
{
return ResponseResult<UserSecurityBindingStatusResponse>.Fail("PIN码验证失败");
}
await userTwoFactorService.UnbindAsync(account, cancellationToken);
var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
return ResponseResult<UserSecurityBindingStatusResponse>.Ok(status, "双因素认证已解绑");
}
[ValidateAntiForgeryToken]
[HttpPost("sign-in")]
public async Task<ResponseResult<WebAuthnAssertionResult>> SignIn(
[FromBody] AuthSignInRequest? request,
CancellationToken cancellationToken = default
)
{
if (request == null)
{
return ResponseResult<WebAuthnAssertionResult>.Fail("登录请求不能为空");
}
var account = request.Account?.Trim();
if (string.IsNullOrWhiteSpace(account))
{
return ResponseResult<WebAuthnAssertionResult>.Fail("请输入账号");
}
if (string.IsNullOrWhiteSpace(request.Password))
{
return ResponseResult<WebAuthnAssertionResult>.Fail("请输入密码");
}
var returnUrl = NormalizeReturnUrl(request.ReturnUrl);
HttpContext.SetSessionMetadata(request.SessionInfo);
if (await accountLoginHistoryService.LockedAsync(account))
{
await accountLoginHistoryService.AccountLockedAsync(account);
return ResponseResult<WebAuthnAssertionResult>.Fail(
"短时间内多次登录失败,账户已被锁定!"
);
}
var pinValidationResult = await mfaVerificationService.ValidateOptionalTwoFactorAsync(
account,
request.PinCode,
cancellationToken
);
if (!pinValidationResult.Success)
{
await accountLoginHistoryService.PinCodeErrorAsync(account);
return ResponseResult<WebAuthnAssertionResult>.Fail(pinValidationResult.Message);
}
var result = await signInManager.PasswordSignInAsync(
account,
request.Password,
request.Remember,
false
);
if (!result.Succeeded)
{
if (HttpContext.HasAccountDisabledFlag())
{
await accountLoginHistoryService.AccountDisabledAsync(account);
return ResponseResult<WebAuthnAssertionResult>.Fail(
"您的账户已被禁用,无法登录!如有疑问,请联系管理员。"
);
}
await accountLoginHistoryService.LoginFailAsync(account);
return ResponseResult<WebAuthnAssertionResult>.Fail("用户名或密码错误!");
}
await accountLoginHistoryService.SuccessAsync(account);
passkeySessionService.ClearCurrentCredentialId();
returnUrl = await ResolvePostSignInRedirectUrlAsync(account, returnUrl, cancellationToken);
return ResponseResult<WebAuthnAssertionResult>.Ok(
new WebAuthnAssertionResult { RedirectUrl = returnUrl }
);
}
[Authorize]
[ValidateAntiForgeryToken]
[HttpPost("change-password")]
public async Task<ResponseResult<WebAuthnAssertionResult>> ChangePassword(
ChangePasswordModel model,
CancellationToken cancellationToken = default
)
{
var account = User.NameIdentifier;
if (string.IsNullOrWhiteSpace(account))
{
return ResponseResult<WebAuthnAssertionResult>.Fail("用户身份验证失败,请重新登录");
}
var pinValidationResult = await mfaVerificationService.ValidateOptionalTwoFactorAsync(
account,
model.PinCode,
cancellationToken
);
if (!pinValidationResult.Success)
{
await accountLoginHistoryService.PinCodeErrorAsync(account);
return ResponseResult<WebAuthnAssertionResult>.Fail(pinValidationResult.Message);
}
var result = await mediator.Send(
new ChangeAccountPasswordRequest
{
Account = account,
Password = model.Password!,
NewPassword = model.NewPassword!,
},
HttpContext.RequestAborted
);
if (!result.Success)
{
return ResponseResult<WebAuthnAssertionResult>.Fail(result.Message ?? "密码修改失败");
}
try
{
await tokenManager.RevokeAsync(account, null, null, null, cancellationToken);
await authorizationManager.RevokeAsync(account, null, null, null, cancellationToken);
}
catch (Exception e)
{
logger.LogError(e, "撤销用户授权和令牌失败");
}
await HttpContext.SignOutAsync();
return ResponseResult<WebAuthnAssertionResult>.Ok(
new WebAuthnAssertionResult { RedirectUrl = "/sign-in" },
"密码修改成功!为了您的账户安全,已撤销所有授权,请重新登录"
);
}
private string NormalizeReturnUrl(string? returnUrl)
{
if (string.IsNullOrWhiteSpace(returnUrl))
{
return Url.Action("Index", "Home") ?? "/";
}
return Url.IsLocalUrl(returnUrl) ? returnUrl : "/";
}
private async Task<string> ResolvePostSignInRedirectUrlAsync(
string account,
string returnUrl,
CancellationToken cancellationToken
)
{
var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
if (status.HasRequiredSecurity)
{
return returnUrl;
}
return $"/security-onboarding.html?returnUrl={Uri.EscapeDataString(returnUrl)}";
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个 ASP.NET Core 的认证 API 控制器(AuthApiController),主要负责用户认证和安全相关的功能。以下是详细解释:
主要功能模块
1. 双因素认证(2FA)管理
SecurityBindings - 获取安全绑定状态
- 查询当前登录用户的安全绑定状态(是否绑定了双因素认证)
TwoFactorSetup - 开始设置双因素认证
- 生成双因素认证的密钥和二维码
- 返回设置所需的信息:
SetupToken:设置令牌- 二维码图片URL
- 手动输入密钥
- 设置URL
BindTwoFactor - 绑定双因素认证
- 验证用户输入的 PIN 码
- 将密钥转换为 Base32 格式并验证
- 验证成功后绑定双因素认证
- 需要 CSRF 保护(
ValidateAntiForgeryToken)
UnbindTwoFactor - 解绑双因素认证
- 验证 PIN 码后解除双因素认证绑定
- 同样需要 CSRF 保护
2. 登录功能
SignIn - 用户登录
执行流程:
- 验证账号和密码是否为空
- 记录会话元数据
- 检查账户是否被锁定
- 验证可选的双因素认证码
- 执行密码登录
- 处理登录结果:
- 账户被禁用
- 密码错误(记录失败次数)
- 登录成功
- 清除 Passkey 会话
- 确定登录后的重定向 URL(如果未完成必要的安全设置,会引导至安全设置页面)
3. 修改密码
ChangePassword - 修改密码
流程:
- 验证用户身份
- 验证可选的双因素认证
- 调用中介者模式执行密码修改
- 修改成功后:
- 撤销所有现有的授权和令牌(提高安全性)
- 退出当前登录
- 重定向到登录页面
技术要点
依赖注入
控制器通过主构造函数注入了多个服务:
- 日志服务
- 登录历史服务
- 双因素认证服务
- MFA 验证服务
- Passkey 会话服务
- 登录管理器
- 中介者(Mediator)
- OpenIddict 授权和令牌管理器
安全特性
- 防 CSRF 攻击:使用
ValidateAntiForgeryToken保护状态变更操作 - 账户锁定:多次登录失败后锁定账户
- 会话管理:登录历史记录和会话元数据跟踪
- 令牌撤销:密码修改后撤销所有现有令牌
辅助方法
NormalizeReturnUrl:标准化返回 URL,防止开放重定向攻击ResolvePostSignInRedirectUrlAsync:根据安全设置状态决定登录后跳转目标
返回值
所有 API 都返回统一的 ResponseResult<T> 格式,包含成功/失败状态和相应的数据或错误消息。
这是一个设计良好的认证控制器,充分考虑了安全性、可扩展性和用户体验。
AI 正在分析代码…
评论加载中...