ASP.NET

入门

使用我们的 NuGet 包将简化与 ASP.NET Identity 的集成过程,带来更顺畅、更高效的集成体验。

dotnet add package Passwordless.AspNetCore

引导现在是一个简单的过程,其中将 Passwordless 添加到 IdentityBuilder 成为必要步骤。

下面的示例假设您的 appsettings.json 将在关键 Passwordless 下添加所有配置设置。

builder.Services
    .AddIdentity<IdentityUser, IdentityRole>()
    .AddEntityFrameworkStores<PasswordlessContext>()
    .AddPasswordless(builder.Configuration.GetSection("Passwordless"));

您至少需要提供 ApiKey(公钥)和 ApiSecret(私钥)。

{
  "Passwordless": {
    "ApiKey": "***:public:***",
    "ApiSecret": "***:secret:***",
    "Register": {
      "Discoverable": true
    }
  }
}

现在,我们需要修改 WebApplication 对象以添加 Passwordless 路由,以便更轻松地注册、登录和管理凭据。

端点记录在本页后面的部分中。

app.MapPasswordless(enableRegisterEndpoint: true);

现在我们将在 /Account/Register 创建注册页面。当我们加载页面时,我们将向用户呈现一个表单。

当我们单击 Register 按钮时,将调用 OnPostAsync。当表单有效时,我们将设置标志,该标志将允许我们的 Javascript 代码运行以允许注册令牌。

调用 POST /passwordless-register 将创建我们的 IdentityUser 并在其响应中返回注册令牌。然后我们将能够使用该令牌来创建我们的通行密钥。

@page
@using Passwordless.Net
@using Microsoft.Extensions.Options
@using Passwordless.AspNetCore
@model RegisterModel
@inject IOptions<PasswordlessAspNetCoreOptions> PasswordlessOptions;

@{
    ViewData["Title"] = "Register";
}

<h1>@ViewData["Title"]</h1>

@{
    var canAddPasskeys = ViewData["CanAddPasskeys"] is true;
}

<div class="row">
    <div class="col-12">
        <form class="needs-validation" action="" method="POST">
            <div class="mb-3">
                <label asp-for="Form.Username" class="form-label">Username</label>
                <input
                        placeholder="Jane Doe"
                        type="text"
                        asp-for="Form.Username"
                        class="form-control"
                        id="username"/>
                <span class="text-danger" asp-validation-for="Form.Username"></span>
            </div>
            <div class="mb-3">
                <label asp-for="Form.Email" class="form-label">Email</label>
                <input
                        placeholder="janedoe@example.org"
                        type="text"
                        asp-for="Form.Email"
                        class="form-control"
                        id="email"/>
                <span class="text-danger" asp-validation-for="Form.Email"></span>
            </div>
            <div class="text-danger" asp-validation-summary="ModelOnly"></div>
            <div>
                <button type="submit" class="btn-primary">Register</button>
            </div>
        </form>
    </div>
</div>

@if (canAddPasskeys)
{
<script src="https://cdn.passwordless.dev/dist/1.1.0/umd/passwordless.umd.js"></script>
<script>
    async function register() {
        const username = document.getElementById('username').value;
        const email = document.getElementById('email').value;
        const registrationRequest = {
            email: email,
            username: username,
            displayName: username,
            aliases: [email]
        };

        const registrationResponse = await fetch('/passwordless-register', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(registrationRequest)
        });

        // 如果没有错误,则进行反序列化,然后使用返回的令牌创建我们的通行密钥
        if (registrationResponse.ok) {
            const registrationResponseJson = await registrationResponse.json();
            const token = registrationResponseJson.token;

            // 我们需要使用从 https://cdn.passwordless.dev/dist/1.1.0/umd/passwordless.umd.js 导入的客户端。
            const p = new Passwordless.Client({
                apiKey: '@PasswordlessOptions.Value.ApiKey',
                apiUrl: '@PasswordlessOptions.Value.ApiUrl'
            });
            const registeredPasskeyResponse = await p.register(token, email);
        }
    }

    register();
</script>
}
public IActionResult OnGet()
{
    if (HttpContext.User.Identity is { IsAuthenticated: true })
    {
        return LocalRedirect("/");
    }

    return Page();
}

public async Task OnPostAsync(FormModel form, CancellationToken cancellationToken)
{
    if (!ModelState.IsValid) return;
    _logger.LogInformation("Registering user {username}", form.Username);
    ViewData["CanAddPasskeys"] = true;
}

public FormModel Form { get; init; } = new();

关于登录页面 /Account/Login,单击「Login」按钮将再次触发一个类似的标志设置,从而执行我们的 JavaScript 代码的执行。

您可以采取两种方法。您可以请求一个「alias」来验证哪些通行密钥与别名对应,或者如果您有可发现的凭据,可能就不需要输入表单了。

登录过程与注册过程不同。在登录过程中,您将向身份验证器请求有效的通行密钥,以获取一个身份验证令牌。随后,该身份验证令牌将被传输到您的后端以启动会话。

使用「Passwordless ASP.NET Identity SDK」,您可以通过简单调用 POST /passwordless-login 来简化此过程,并且 SDK 将为您处理所有必要的步骤。

身份验证成功后,我们的示例应用程序将自动将您重定向到 /Authorized/HelloWorld 页面,该页面需要您登录才能访问。

@page
@model LoginModel
@using Microsoft.Extensions.Options
@using Passwordless.AspNetCore
@inject IOptions<PasswordlessAspNetCoreOptions> PasswordlessOptions;
@{
    ViewData["Title"] = "Login";
}

<h1>@ViewData["Title"]</h1>

@{
    var canLogin = ViewData["CanLogin"] != null && (bool)ViewData["CanLogin"];
}

<div class="row">
    <div class="col-12">
        <form class="needs-validation" action="" method="POST">
            <div class="mb-3">
                <label asp-for="Form.Email" class="form-label">Email</label>
                <input
                        placeholder="janedoe@example.org"
                        type="text"
                        asp-for="Form.Email"
                        class="form-control"
                        id="email"/>
                <span class="text-danger" asp-validation-for="Form.Email"></span>
            </div>
            <div class="text-danger" asp-validation-summary="ModelOnly"></div>
            <div>
                <button type="submit" class="btn-primary">Login</button>
            </div>
        </form>
    </div>
</div>

@if (canLogin)
{
<script src="https://cdn.passwordless.dev/dist/1.1.0/umd/passwordless.umd.js"></script>
<script>
    async function login() {
        const alias = document.getElementById('email').value;
        const p = new Passwordless.Client({
            apiKey: '@PasswordlessOptions.Value.ApiKey',
            apiUrl: '@PasswordlessOptions.Value.ApiUrl'
        });
        const loginPasskeyResponse = await p.signinWithAlias(alias);
        if (!loginPasskeyResponse) {
            return;
        }
        const loginRequest = {
            token: loginPasskeyResponse.token
        };
        const loginResponse = await fetch('/passwordless-login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(loginRequest)
        });

        if (loginResponse.ok) {
            console.log('login successful: ' + (await loginResponse.text()));

            // Redirect to authorized page /Authorized/HelloWorld
            window.location.href = '/Authorized/HelloWorld';
        }
    }

    login();
</script>
}

如果我们在已通过身份验证的情况下访问登录页面,我们希望重定向到其他地方。

public IActionResult OnGet()
{
    if (HttpContext.User.Identity is { IsAuthenticated: true })
    {
        return LocalRedirect("/");
    }
    return Page();
}

public async Task OnPostAsync(LoginForm form, CancellationToken cancellationToken)
{
    if (!ModelState.IsValid) return;
    _logger.LogInformation("Logging in user {email}", form.Email);
    ViewData["CanLogin"] = true;
}

public LoginForm Form { get; } = new();

然后,您可以从 HttpContext 访问有关登录用户的信息:

public class HelloWorldModel : PageModel
{
    private readonly ILogger<HelloWorldModel> _logger;
    
    public HelloWorldModel(ILogger<HelloWorldModel> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public void OnGet()
    {
        var identity = HttpContext.User.Identity ;
        var email = User.FindFirstValue(ClaimTypes.Email);
        AuthenticatedUser = new AuthenticatedUserModel(identity.Name, email);
    }
    
    public AuthenticatedUserModel AuthenticatedUser { get; private set; }
}

public record AuthenticatedUserModel(string Username, string Email);

配置

密钥默认值描述

ApiKey

您的公共 API 密钥。

ApiSecret

您的私有 API 密钥。

ApiUrl

https://v4.passwordless.dev

您的 Passwordless.dev 实例运行的位置。

SignInScheme

Identity.Application

控制用于处理登录的方案。

UserIdClaimType

控制用于从已经过身份验证的用户查找用户 ID 的声明类型,请参阅 ClaimsPrincipal。如果为空,则将从 IdentityOptions.ClaimsIdentity 回退到 ClaimsIdentityOptions.UserIdClaimType;如果又为空,则回退到 ClaimTypes.NameIdentifier

Register__AliasHashing

true

[false/true] 当设置为 true 时,别名仅以其哈希形式存储。

Register__Attestation

None

[None/Direct/Indirect] 值为 None 表示服务器不关心验证。值为 Indirect 表示服务器将允许匿名验证数据。Direct 表示服务器希望接收来自身份验证器的验证数据。

Register__AuthenticationType

any

Register__Expiration

"00:02:00"

["hh:mm:ss"]

Register__Discoverable

true

[false/true] 可发现凭证将私钥和关相关数据存储在身份验证器的内存中,而不是服务器上。这样,服务器就无需将证书发送到验证器进行解密,从而减少了身份验证对用户名和密码的依赖。

Register__UserVerification

Preferred

[Discouraged/Preferred/Required] WebAuthn 依赖方可能在某些操作中需要用户验证,而在其他操作中则不需要,因此可以使用这种类型来表达自己的需求。

高级

如果您需要更大的灵活性,我们邀请您探索我们的 JavaScript 客户端库和 .NET SDK。如果您希望对 ASP.NET Identity 框架进行更精细的控制,或者希望进行完全自定义的实施,则此选项特别合适。

最后更新于