using System.Text;
using Dpz.Core.Authenticator;
namespace Dpz.Core.Auth.Controllers;
public class TwoFactorController(
ILogger<HomeController> logger,
IUserTwoFactorService userTwoFactorService
) : Controller
{
/// <summary>
/// 绑定双因素验证
/// </summary>
/// <returns></returns>
[HttpGet]
[Authorize]
[Route("two-factor.html")]
public async Task<IActionResult> BindTwoFactor(
string returnUrl = "/",
CancellationToken cancellationToken = default
)
{
var account = User.NameIdentifier;
if (User.Identity?.IsAuthenticated != true || string.IsNullOrWhiteSpace(account))
{
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
if (status.HasTwoFactor)
{
return Redirect(returnUrl);
}
var model = await BuildBindModelAsync(
account,
returnUrl,
cancellationToken: cancellationToken
);
return model == null ? Redirect(returnUrl) : this.ShellView(model);
}
[HttpGet]
[Authorize]
[Route("unbind-two-factor.html")]
public async Task<IActionResult> UnBindTwoFactor(CancellationToken cancellationToken = default)
{
var account = User.NameIdentifier;
var key = await userTwoFactorService.GetBoundKeyAsync(account, cancellationToken);
return this.ShellView(
BuildUnbindModel(string.IsNullOrWhiteSpace(key) ? "您未绑定双因素验证" : null)
);
}
[ValidateAntiForgeryToken]
[Authorize]
[HttpPost]
[Route("handel-bind-two-factor")]
public async Task<IActionResult> HandleBindTwoFactor(
string pinCode,
string setupToken,
string returnUrl = "/",
CancellationToken cancellationToken = default
)
{
var account = User.NameIdentifier;
if (User.Identity?.IsAuthenticated != true || string.IsNullOrWhiteSpace(account))
{
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
var status = await userTwoFactorService.GetBindingStatusAsync(account, cancellationToken);
if (status.HasTwoFactor)
{
return Redirect(returnUrl);
}
var key = await userTwoFactorService.GetSetupKeyAsync(
account,
setupToken,
cancellationToken
);
if (string.IsNullOrWhiteSpace(key))
{
logger.LogWarning("获取用户密钥失败,账号:{Account}", account);
var model = await BuildBindModelAsync(
account,
returnUrl,
"双因素绑定请求已过期,请重新获取二维码",
cancellationToken
);
return model == null ? Redirect(returnUrl) : this.ShellView(model);
}
var twoFactorAuthenticator = new TwoFactorAuthenticator();
var keyBuffer = Encoding.UTF8.GetBytes(key);
var keyBase32 = Base32Encoding.ToString(keyBuffer);
var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(keyBase32, pinCode, true);
if (!twoFactorResult)
{
var model = await BuildBindModelAsync(
account,
returnUrl,
"PIN码验证失败",
cancellationToken
);
return model == null ? Redirect(returnUrl) : this.ShellView(model);
}
await userTwoFactorService.BindAsync(account, setupToken, cancellationToken);
return Redirect(returnUrl);
}
[HttpPost]
[Authorize]
[Route("unbind-two-factor")]
public async Task<IActionResult> HandleUnbindTwoFactor(
[FromQuery] string returnUrl,
string pinCode,
CancellationToken cancellationToken = default
)
{
var account = User.NameIdentifier;
if (User.Identity?.IsAuthenticated != true || string.IsNullOrWhiteSpace(account))
{
return Challenge(authenticationSchemes: ConstValues.DefaultScheme);
}
var key = await userTwoFactorService.GetBoundKeyAsync(account, cancellationToken);
if (string.IsNullOrWhiteSpace(key))
{
return this.ShellView(BuildUnbindModel("您未绑定双因素验证"));
}
var twoFactorAuthenticator = new TwoFactorAuthenticator();
var keyBuffer = Encoding.UTF8.GetBytes(key);
var keyBase32 = Base32Encoding.ToString(keyBuffer);
var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(keyBase32, pinCode, true);
if (!twoFactorResult)
{
return this.ShellView(BuildUnbindModel("PIN码验证失败"));
}
await userTwoFactorService.UnbindAsync(account, cancellationToken);
return Redirect(returnUrl);
}
/// <summary>
/// 构建 2FA 绑定页面模型,包括生成 setup token、TOTP 密钥和二维码
/// </summary>
private async Task<AuthShellViewModel<TwoFactorSetupShellPayload>?> BuildBindModelAsync(
string account,
string returnUrl,
string? message = null,
CancellationToken cancellationToken = default
)
{
var challenge = await userTwoFactorService.BeginSetupAsync(account, cancellationToken);
if (challenge == null)
{
logger.LogWarning("获取用户密钥失败,账号:{Account}", account);
return null;
}
var twoFactorAuthenticator = new TwoFactorAuthenticator();
var setupInfo = twoFactorAuthenticator.GenerateSetupCode(
issuer: "叫我阿胖",
accountTitleNoSpaces: account,
accountSecretKey: challenge.Key,
secretIsBase32: false
);
return new AuthShellViewModel<TwoFactorSetupShellPayload>
{
EntryName = "identity",
PageName = "bind-two-factor",
ReturnUrl = returnUrl,
Payload = new TwoFactorSetupShellPayload
{
SetupToken = challenge.SetupToken,
Account = setupInfo.Account,
ManualEntryKey = setupInfo.ManualEntryKey,
QrCodeSetupImageUrl = setupInfo.QrCodeSetupImageUrl,
SetupUrl = setupInfo.SetupUrl,
},
Message = message,
};
}
/// <summary>
/// 构建 2FA 解绑页面模型
/// </summary>
private static AuthShellViewModel<EmptyAuthShellPayload> BuildUnbindModel(
string? message = null
) =>
new()
{
EntryName = "identity",
PageName = "unbind-two-factor",
Message = message,
};
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释
这是一个 ASP.NET Core MVC 控制器,用于处理**双因素认证(2FA)**的绑定和解绑功能。以下是详细解释:
整体架构
控制器定义
public class TwoFactorController(
ILogger<HomeController> logger,
IUserTwoFactorService userTwoFactorService
) : Controller
- 使用主构造函数语法(C# 12)注入依赖
- 注入了日志服务和双因素认证业务服务
核心功能
1. 绑定双因素验证页面 (BindTwoFactor)
[HttpGet]
[Authorize]
[Route("two-factor.html")]
流程:
- 验证用户是否已登录
- 检查用户是否已绑定 2FA
- 已绑定 → 重定向到返回 URL
- 未绑定 → 生成绑定页面模型
- 调用
BuildBindModelAsync生成包含以下信息的页面:- Setup Token(临时令牌)
- TOTP 密钥
- 二维码 URL
2. 处理绑定请求 (HandleBindTwoFactor)
[HttpPost]
[ValidateAntiForgeryToken]
[Route("handel-bind-two-factor")]
验证流程:
- 验证用户身份和绑定状态
- 使用
setupToken获取之前生成的密钥 - 验证 PIN 码:
var twoFactorAuthenticator = new TwoFactorAuthenticator(); var keyBuffer = Encoding.UTF8.GetBytes(key); var keyBase32 = Base32Encoding.ToString(keyBuffer); var twoFactorResult = twoFactorAuthenticator.ValidateTwoFactorPIN(keyBase32, pinCode, true);- 将密钥转为 Base32 编码
- 验证用户输入的 6 位 PIN 码
- 验证成功后调用
BindAsync完成绑定
3. 解绑双因素验证页面 (UnBindTwoFactor)
[HttpGet]
[Authorize]
[Route("unbind-two-factor.html")]
- 检查用户是否已绑定 2FA
- 返回解绑页面视图
4. 处理解绑请求 (HandleUnbindTwoFactor)
[HttpPost]
[Authorize]
[Route("unbind-two-factor")]
流程:
- 获取用户已绑定的密钥
- 要求用户输入当前有效的 PIN 码进行验证
- 验证通过后调用
UnbindAsync解除绑定
辅助方法
BuildBindModelAsync
生成绑定页面所需的完整数据模型:
var setupInfo = twoFactorAuthenticator.GenerateSetupCode(
issuer: "叫我阿胖", // 显示在验证器应用中的名称
accountTitleNoSpaces: account,
accountSecretKey: challenge.Key,
secretIsBase32: false
);
返回包含以下内容的视图模型:
SetupToken:临时令牌用于验证绑定请求QrCodeSetupImageUrl:二维码图片 URLManualEntryKey:手动输入密钥(当无法扫码时)
BuildUnbindModel
生成解绑页面的简单模型,主要用于显示错误信息。
安全特性
- 防 CSRF 攻击:绑定操作使用
[ValidateAntiForgeryToken] - 授权验证:所有操作都需要
[Authorize]认证 - 状态检查:每次操作前验证用户当前绑定状态
- 时效性控制:Setup Token 有过期机制
- PIN 码验证:使用 TOTP 算法验证动态 PIN 码
典型使用场景
首次绑定:
- 访问
/two-factor.html→ 扫描二维码 → 输入 PIN 码 → 绑定成功
- 访问
解绑操作:
- 访问
/unbind-two-factor.html→ 输入当前有效 PIN 码 → 解绑成功
- 访问
已绑定用户访问绑定页:
- 自动重定向到原页面,防止重复绑定
AI 正在分析代码…
评论加载中...