OpenIddict使用教程
@@openiddict password 模式 流程
OpenIddict是一个ASP.NET Core身份验证库,可帮助您添加OpenID Connect和OAuth 2.0支持到ASP.NET Core应用程序中。下面是OpenIddict使用教程的步骤:
-
安装OpenIddict,在项目中添加OpenIddict.Core和OpenIddict.EntityFrameworkCore Nuget包。
-
配置OpenIddict,在Startup.cs文件中添加OpenIddict服务的配置。您可以选择使用内存或EFCore进行配置。以下是使用EF Core进行配置的示例:
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)); });
- 添加授权策略,在Startup.cs文件添加需要的授权策略。以下是一个例子:
services.AddAuthorization(options => { options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber")); options.AddPolicy("AdministratorOnly", policy => policy.RequireRole("Administrator")); });
- 在您的应用程序中使用OpenIddict,您可以使用OpenIddict来实现您的OAuth 2.0或OpenID Connect需求。以下是一些常见的用例:
4.1 登录页面
使用OpenIddict进行身份验证,您可以使用如下代码在您的控制器中。您可以使用请求重定向到触发OpenID Connect流:
[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来实现您的注册页面。以下是一个例子:
[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来实现访问受保护资源的身份验证和授权。以下是一个例子:
[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(); }
- 通过OpenIddict实现Token刷新
当访问受保护的API时,您可以使用OpenIddict来实现使用token刷新。以下是实现Token刷新的一个示例方法:
[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); }
- 通过OpenIddict实现密码恢复流程OpenIddict还可以实现忘记密码流程的重置密码,以下是一个简单的示例:
[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(); }
- 使用OpenIddict实现自定义Token发布方案
OpenIddict支持自定义Token发布方案,以适应各种需求。在以下示例中,我们将实现自定义发布方案来控制Token的过期时间:
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配置中:
services.AddOpenIddict() .AddCore(options => { // ... }) .AddServer(options =>{ // ... options.Handlers.Add(new CustomTokenEndpointHandler(services)); // ... }) .AddValidation(options => { // ... });
此时,您可以使用urn:custom_grant
授权类型来发出过期时间为1小时的Token,这可以通过以下方式完成:
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创建一个基本的身份验证和授权服务器。当然,在实现身份验证和授权服务器时有很多细节需要考虑,例如维护安全性、处理错误、管理用户和客户端应用程序等。希望这篇文章对您有所帮助!