OpenIddict使用教程

OpenIddict是一个ASP.NET Core身份验证库,可帮助您添加OpenID Connect和OAuth 2.0支持到ASP.NET Core应用程序中。下面是OpenIddict使用教程的步骤:

  1. 安装OpenIddict,在项目中添加OpenIddict.Core和OpenIddict.EntityFrameworkCore Nuget包。
  2. 配置OpenIddict,在Startup.cs文件中添加OpenIddict服务的配置。您可以选择使用内存或EFCore进行配置。以下是使用EF Core进行配置的示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    services.AddDbContext<ApplicationDbContext>(options =>
    {
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
    options.UseOpenIddict();
    });

    services.AddCustomOpenIddictApplication();
    services.AddCustomOpenIddictAuthorization();
    services.AddCustomOpenIddictScope();
    services.AddCustomOpenIddictToken();
    services.AddCustomOpenIddictValidation();
    services.AddCustomOpenIddictUser();

    services.AddOpenIddict()
    .AddCore(options =>
    {
    options.UseEntityFrameworkCore()
    .UseDbContext<ApplicationDbContext>()
    .ReplaceDefaultEntities<ApplicationDbContext>();
    })
    .AddServer(options =>
    {
    options.UseMvc();
    options.EnableAuthorizationEndpoint("/connect/authorize")
    .EnableLogoutEndpoint("/connect/logout")
    .EnableTokenEndpoint("/connect/token")
    .EnableUserinfoEndpoint("/connect/userinfo");
    options.RegisterScopes("openid", "profile", "email", "offline_access");

    options.AllowImplicitFlow();
    options.DisableHttpsRequirement();

    options.AddSigningCertificate(File.ReadAllBytes(Configuration["Auth:Certificates:Path"]),
    Configuration["Auth:Certificates:Password"]);

    options.DisableAccessTokenEncryption();
    options.SetAccessTokenLifetime(TimeSpan.FromHours(6));
    });
  3. 添加授权策略,在Startup.cs文件添加需要的授权策略。以下是一个例子:
    1
    2
    3
    4
    5
    services.AddAuthorization(options =>
    {
    options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
    options.AddPolicy("AdministratorOnly", policy => policy.RequireRole("Administrator"));
    });
  4. 在您的应用程序中使用OpenIddict,您可以使用OpenIddict来实现您的OAuth 2.0或OpenID Connect需求。以下是一些常见的用例:

4.1 登录页面

使用OpenIddict进行身份验证,您可以使用如下代码在您的控制器中。您可以使用请求重定向到触发OpenID Connect流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[HttpGet("~/login")]
public IActionResult Login()
{
var request = HttpContext.GetOpenIddictServerRequest();

return View(new LoginViewModel
{
Nonce = RandomNumberGenerator.GetInt32(),
ReturnUrl = request.RedirectUri,
Ticket = request.GetOpenIddictServerTransactionId(),
});
}

[HttpPost("~/login")]
public IActionResult Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.FindByNameAsync(model.Username);
if (user == null)
{
ModelState.AddModelError("Username", "Username or password is incorrect.");
}
else if (!await _userManager.IsEmailConfirmedAsync(user))
{
ModelState.AddModelError("Email", "You must have a confirmed email to log in.");
}
else if (!await _userManager.CheckPasswordAsync(user, model.Password))
{
ModelState.AddModelError("Username", "Username or password is incorrect.");
}
else
{
// 创建一个新的身份验证票据.
var ticket = await CreateTicketAsync(user);

return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
}

ViewData["returnUrl"] = model.ReturnUrl;
ViewData["nonce"] = model.Nonce;
ViewData["transactionId"] = model.Ticket;
return View(model);
}

4.2 注册页面

您还可以使用OpenIddict来实现您的注册页面。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[HttpGet("~/register")]
public IActionResult Register()
{
return View();
}

[HttpPost("~/register")]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser
{
UserName = model.Email,
Email = model.Email,
FirstName = model.FirstName,
LastName = model.LastName,
};

var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
var code = await _userManager.GenerateEmailConfirmationAsync(user);
var callbackUrl= Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);

await _emailSender.SendEmailAsync(model.Email, "Confirm your email",
$"Please confirm your account by clicking this link: {callbackUrl}");

return RedirectToAction(nameof(RegisterConfirmation));
}

foreach (var error in result.Errors)
{
ModelState.AddModelError("Email", error.Description);
}
}

return View(model);
}

[HttpGet("~/register/confirmation")]
public IActionResult RegisterConfirmation()
{
return View();
}

4.3 访问受保护的资源

最后,您可以使用OpenIddict来实现访问受保护资源的身份验证和授权。以下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[HttpGet("~/manager")]
[Authorize(Roles = "Manager")]
public IActionResult ManagerDashboard()
{
return View();
}

[HttpGet("~/employee")]
[Authorize(Policy = "EmployeeOnly")]
public IActionResult EmployeeDashboard()
{
return View();
}

[HttpGet("~/administrator")]
[Authorize(Policy = "AdministratorOnly")]
public IActionResult AdministratorDashboard()
{
return View();
}
  1. 通过OpenIddict实现Token刷新

当访问受保护的API时,您可以使用OpenIddict来实现使用token刷新。以下是实现Token刷新的一个示例方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[HttpPost("~/api/token/refresh")]
public async Task<IActionResult> Refresh([FromForm]string refreshToken)
{
var info = await HttpContext.AuthenticateAsync(OpenIddictServerDefaults.AuthenticationScheme);

if (info == null)
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The refresh token is no longer valid."
});
}

var principal = info.Principal;

var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The refresh token is no longer valid."
});
}

// 确保刷新令牌没有被撤销.
if (!await _tokenManager.ValidateAsync(
principal.GetId(),
principal.GetClaim(OpenIddictConstants.Claims.JwtId)))
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The refresh token is no longer valid."
});
}

// 从数据库得到客户端应用程序详细信息
var application = await _applicationManager.FindByClientIdAsync(
principal.GetClaim(OpenIddictConstants.Claims.ClientId));
if (application == null)
{
return BadRequest(new
{
error = OpenIddictConstants.Errors.InvalidRequest,
error_description = "The client application associated with this token is no longer valid."
});
}

var identity = await _userManager.CreateIdentityAsync(user, principal.GetScopes());

var ticket = await CreateTicketAsync(application, identity, principal);

return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
  1. 通过OpenIddict实现密码恢复流程OpenIddict还可以实现忘记密码流程的重置密码,以下是一个简单的示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    [HttpPost("~/forgot-password")]
    [AllowAnonymous]
    public async Task<IActionResult> ForgotPassword([FromForm] string email)
    {
    var user = await _userManager.FindByEmailAsync(email);
    if (user == null)
    {
    // 不要显示用户不存在,懂的都懂~
    return Ok();
    }

    var code = await _userManager.GeneratePasswordResetTokenAsync(user);
    code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

    var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Reque

    await _emailSender.SendEmailAsync(
    email,
    "Password Reset",
    $"Please reset your password by clicking here: <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>link</a>.");

    return Ok();
    }

    [HttpGet("~/reset-password")]
    [AllowAnonymous]
    public IActionResult ResetPassword(string code = null, string userId = null)
    {
    return View(new ResetPasswordViewModel { Code = code, UserId = userId });
    }

    [HttpPost("~/reset-password")]
    [AllowAnonymous]
    public async Task<IActionResult> ResetPassword([FromForm] ResetPasswordViewModel model)
    {
    if (!ModelState.IsValid)
    {
    return View(model);
    }

    var user = await _userManager.FindByIdAsync(model.UserId);
    if (user == null)
    {
    // 不要显示用户不存在
    return View("ResetPasswordConfirmation");
    }

    var decodedCode = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(model.Code));
    var result = await _userManager.ResetPasswordAsync(user, decodedCode, model.Password);
    if (result.Succeeded)
    {
    return RedirectToAction(nameof(ResetPasswordConfirmation));
    }

    foreach (var error in result.Errors)
    {
    ModelState.AddModelError(string.Empty, error.Description);
    }

    return View(model);
    }

    [HttpGet("~/reset-password-confirmation")]
    [AllowAnonymous]
    public IActionResult ResetPasswordConfirmation()
    {
    return View();
    }
  2. 使用OpenIddict实现自定义Token发布方案

OpenIddict支持自定义Token发布方案,以适应各种需求。在以下示例中,我们将实现自定义发布方案来控制Token的过期时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class CustomTokenEndpointHandler : OpenIddictServerHandler<OpenIddictServerOptions>
{
public CustomTokenEndpointHandler(IServiceProvider services)
: base(services)
{
}

public override async Task HandleAsync([NotNull] OpenIddictServerHandleContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

// 从数据库检索客户机应用程序.
var application = await context.HttpContext.GetOpenIddictServerApplicationAsync();
if (application == null)
{
throw new InvalidOperationException("The client application cannot be retrieved.");
}

// 从授权服务器设置检索用户主体.
var principal = context.HttpContext.User;

// 确保允许应用程序使用指定的授权类型。
if (!await ValidateClientRedirectUriAsync(application, context.Request))
{
throw new InvalidOperationException("The grant type is not allowed for this application.");
}

//注意:这个自定义令牌终端点总是忽略“scopes”参数,并根据授予的scopes/roles自动定义声明。
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(),
OpenIddictServerDefaults.AuthenticationScheme);

// 根据请求的自定义授权类型自定义令牌生命周期.
if (string.Equals(context.Request.GrantType, "urn:custom_grant", StringComparison.OrdinalIgnoreCase))
{
// Set the token expiration to 1 hour.
ticket.Properties.ExpiresUtc = context.Options.SystemClock.UtcNow.AddHours(1);
}
else
{
// 将令牌过期时间设置为默认持续时间(5分钟)
ticket.Properties.ExpiresUtc = context.Options.SystemClock.UtcNow.Add(
context.Options.AccessTokenLifetime ?? TimeSpan.FromMinutes(5));
}

context.Logger.LogInformation("The custom token request was successfully processed.");

await context.HttpContext.SignInAsync(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);

// 将响应标记为已处理,以跳过管道的其余部分.
context.HandleRequest();
}
}

您需要将其添加到OpenIddict配置中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>{
// ...

options.Handlers.Add(new CustomTokenEndpointHandler(services));

// ...
})
.AddValidation(options =>
{
// ...
});

此时,您可以使用 urn:custom_grant 授权类型来发出过期时间为1小时的Token,这可以通过以下方式完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "http://localhost:5000/connect/token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "urn:custom_grant",
["client_id"] = "your_client_id",
["client_secret"] = "your_client_secret",
["scope"] = "your_scopes_separated_by_spaces"
});

var response = await client.SendAsync(request);
var payload = await response.Content.ReadAsStringAsync();

总结

本文介绍了如何使用OpenIddict创建一个基本的身份验证和授权服务器。当然,在实现身份验证和授权服务器时有很多细节需要考虑,例如维护安全性、处理错误、管理用户和客户端应用程序等。希望这篇文章对您有所帮助!