九、保护 ASP.NET Core 2.0 应用

在当今这个数字犯罪和互联网欺诈不断增加的世界里,所有现代 web 应用都需要实现强大的安全机制来防止攻击和用户身份盗用。

到目前为止,我们一直专注于理解如何构建高效的 ASP。 净核心 2.0 web 应用,而不考虑用户身份验证、用户授权、或数据保护,但由于井字应用变得越来越复杂,我们必须解决安全问题,最后将它部署到公众。

构建一个 web 应用而不考虑安全性将是一个巨大的失败,甚至可能导致最伟大和最著名的网站崩溃。 在安全漏洞和个人数据盗窃的情况下,负面的声誉和用户信心影响可能是巨大的,没有人愿意与这些应用和更麻烦的公司一起工作。

这是一个需要认真对待的话题。 您应该与安全公司合作进行代码验证和入侵测试,以确保您符合最佳实践和高安全性标准(例如,OWASP10)。

幸运的是,ASP.NET Core 2.0 包含了帮助您处理这个复杂但重要的主题所需的一切。 大多数内置功能甚至不需要高级编程或安全技能。 您将看到,通过使用 ASP 来理解和实现安全应用是非常容易的.NET Core 2.0 身份框架。

在本章中,我们将涵盖以下主题:

  • 添加基本的用户表单验证
  • 添加外部提供者身份验证
  • 增加忘记密码和密码重置机制
  • 使用双因素身份验证
  • 实现授权

实现身份验证

身份验证允许应用识别特定的用户。 它既不用于管理用户访问权限(这是授权角色),也不用于保护数据(这是数据保护角色)。

有几种验证应用用户的方法,例如:

  • 基本用户表单身份验证,使用带有登录框和密码框的登录表单
  • 单点登录(SSO)身份验证,其中用户仅对其公司上下文中的所有应用进行一次身份验证
  • 社交网络外部提供商身份验证(如 Facebook 和 LinkedIn)
  • 证书或公钥基础设施(PKI)认证

ASP.NET Core 2.0 支持所有这些方法,但在本章中,我们将集中讨论使用用户登录和密码进行表单身份验证,以及通过 Facebook 进行外部提供商身份验证。

在下面的示例中,您将看到如何使用这些方法对应用用户进行身份验证,以及一些更高级的特性,如电子邮件确认和密码重置机制。

最后(但并非最不重要),您将看到如何使用内置的 ASP 实现双因素身份验证。 最关键的应用的 NET Core 2.0 身份验证特性。

让我们准备为Tic-Tac-Toe应用实现不同的身份验证机制:

  1. 更新Startup类中的UserServiceGameInvitationServiceGameSessionService的生命周期:
        services.AddTransient<IUserService, UserService>(); 
        services.AddScoped<IGameInvitationService,
         GameInvitationService>(); 
        services.AddScoped<IGameSessionService, GameSessionService>(); 
  1. 更新Startup类中的Configure方法,并在静态文件中间件之后直接调用认证中间件:
        app.UseStaticFiles(); 
        app.UseAuthentication(); 
  1. 更新UserModel使其与内置的 ASP. php 一起使用.NET Core 2.0 身份验证功能,并删除IdentityUser类已经提供的IdEmail属性:
        public class UserModel : IdentityUser<Guid> 
        { 
          [Display(Name = "FirstName")] 
          [Required(ErrorMessage = "FirstNameRequired")] 
          public string FirstName { get; set; } 
          [Display(Name = "LastName")] 
          [Required(ErrorMessage = "LastNameRequired")] 
          public string LastName { get; set; }     
          [Display(Name = "Password")] 
          [Required(ErrorMessage = "PasswordRequired"),
           DataType(DataType.Password)] 
          public string Password { get; set; } 
          [NotMapped] 
          public bool IsEmailConfirmed 
          { 
            get { return EmailConfirmed; } 
          } 
          public System.DateTime? EmailConfirmationDate { get; set; } 
          public int Score { get; set; } 
        }

Note that in the real world, we would advise also removing the Password property. However, we will keep it in the example for clarity and learning purposes.

  1. 添加一个新的文件夹Managers,并在文件夹ApplicationUserManager中添加一个新的管理器:
        public class ApplicationUserManager : UserManager<UserModel> 
        { 
          private IUserStore<UserModel> _store; 
          DbContextOptions<GameDbContext> _dbContextOptions; 
          public ApplicationUserManager(
           DbContextOptions<GameDbContext> dbContextOptions,
           IUserStore<UserModel> store, IOptions<IdentityOptions>
           optionsAccessor, IPasswordHasher<UserModel> passwordHasher, 
           IEnumerable<IUserValidator<UserModel>> userValidators,
           IEnumerable<IPasswordValidator<UserModel>> 
           passwordValidators, ILookupNormalizer keyNormalizer,
           IdentityErrorDescriber errors, IServiceProvider services,
           ILogger<UserManager<UserModel>> logger) : 
            base(store, optionsAccessor, passwordHasher,
             userValidators, passwordValidators, keyNormalizer,
             errors, services, logger) 
          { 
            _store = store; 
            _dbContextOptions = dbContextOptions; 
          } 

          public override async Task<UserModel> FindByEmailAsync(
           string email) 
          { 
            using (var dbContext = new GameDbContext(_dbContextOptions)) 
            { 
              return await dbContext.Set<UserModel>().FirstOrDefaultAsync(
               x => x.Email == email); 
            } 
          } 

          public override async Task<UserModel> FindByIdAsync(
           string userId) 
          { 
            using (var dbContext = new GameDbContext(_dbContextOptions)) 
            { 
              Guid id = Guid.Parse(userId); 
              return await dbContext.Set<UserModel>().FirstOrDefaultAsync(
               x => x.Id == id); 
            } 
          } 

          public override async Task<IdentityResult>
           UpdateAsync(UserModel user) 
          { 
            using (var dbContext = new GameDbContext(_dbContextOptions)) 
            { 
              var current =
                await dbContext.Set<UserModel>().FirstOrDefaultAsync(
                x => x.Id == user.Id); 
              current.AccessFailedCount = user.AccessFailedCount; 
              current.ConcurrencyStamp = user.ConcurrencyStamp; 
              current.Email = user.Email; 
              current.EmailConfirmationDate = user.EmailConfirmationDate; 
              current.EmailConfirmed = user.EmailConfirmed; 
              current.FirstName = user.FirstName; 
              current.LastName = user.LastName; 
              current.LockoutEnabled = user.LockoutEnabled; 
              current.NormalizedEmail = user.NormalizedEmail; 
              current.NormalizedUserName = user.NormalizedUserName; 
              current.PhoneNumber = user.PhoneNumber; 
              current.PhoneNumberConfirmed = user.PhoneNumberConfirmed; 
              current.Score = user.Score; 
              current.SecurityStamp = user.SecurityStamp; 
              current.TwoFactorEnabled = user.TwoFactorEnabled; 
              current.UserName = user.UserName; 
              await dbContext.SaveChangesAsync(); 
              return IdentityResult.Success; 
            } 
          } 

          public override async Task<IdentityResult> 
           ConfirmEmailAsync(UserModel user, string token) 
          { 
            var isValide = await base.VerifyUserTokenAsync(user,
             Options.Tokens.EmailConfirmationTokenProvider,
             ConfirmEmailTokenPurpose, token); 
            if (isValide) 
            { 
              using (var dbContext =
                new GameDbContext(_dbContextOptions)) 
              { 
                var current =
                  await dbContext.UserModels.FindAsync(user.Id); 
                current.EmailConfirmationDate = DateTime.Now; 
                current.EmailConfirmed = true; 
                await dbContext.SaveChangesAsync(); 
                return IdentityResult.Success; 
              } 
            } 
            return IdentityResult.Failed(); 
          } 
        }
  1. 更新Startup类,并注册ApplicationUserManager:
        services.AddTransient<ApplicationUserManager>(); 
  1. 更新UserService以与 ApplicationUser Manager 一起工作,添加两个新方法GetEmailConfirmationCodeConfirmEmail,并更新用户服务接口:
        public class UserService 
        { 
          private ILogger<UserService> _logger; 
          private ApplicationUserManager _userManager; 
          public UserService(ApplicationUserManager userManager,
           ILogger<UserService> logger) 
          { 
            _userManager = userManager; 
            _logger = logger; 

            var emailTokenProvider = new EmailTokenProvider<UserModel>(); 
            _userManager.RegisterTokenProvider("Default", 
             emailTokenProvider); 
          } 

          public async Task<bool> ConfirmEmail(string email, string code) 
          { 
            var start = DateTime.Now; 
            _logger.LogTrace($"Confirm email for user {email}"); 

            var stopwatch = new Stopwatch(); 
            stopwatch.Start(); 

            try 
            { 
              var user = await _userManager.FindByEmailAsync(email); 

              if (user == null) 
                return false; 

              var result = await _userManager.ConfirmEmailAsync(
               user, code); 
              return result.Succeeded; 
            } 
            catch (Exception ex) 
            { 
              _logger.LogError($"Cannot confirm email for user
               {email} - {ex}"); 
              return false; 
            } 
            finally 
            { 
              stopwatch.Stop(); 
              _logger.LogTrace($"Confirm email for user finished in
              {stopwatch.Elapsed}"); 
            } 
          } 

          public async Task<string> GetEmailConfirmationCode(
           UserModel user) 
          { 
            return
             await _userManager.GenerateEmailConfirmationTokenAsync(user); 
          } 

          public async Task<bool> RegisterUser(UserModel userModel) 
          { 
            var start = DateTime.Now; 
            _logger.LogTrace($"Start register user
             {userModel.Email} - {start}"); 

            var stopwatch = new Stopwatch(); 
            stopwatch.Start(); 

            try 
            { 
              userModel.UserName = userModel.Email; 
              var result = await _userManager.CreateAsync(userModel,
               userModel.Password); 
              return result == IdentityResult.Success; 
            } 
            catch (Exception ex) 
            { 
              _logger.LogError($"Cannot register user 
               {userModel.Email} - {ex}"); 
              return false; 
            } 
            finally 
            { 
              stopwatch.Stop(); 
              _logger.LogTrace($"Start register user {userModel.Email}
               finished at {DateTime.Now} - elapsed 
               {stopwatch.Elapsed.TotalSeconds} second(s)"); 
            } 
          } 

          public async Task<UserModel> GetUserByEmail(string email) 
          { 
            return await _userManager.FindByEmailAsync(email); 
          } 

          public async Task<bool> IsUserExisting(string email) 
          { 
            return (await _userManager.FindByEmailAsync(email)) != null; 
          } 

          public async Task<IEnumerable<UserModel>> GetTopUsers(
           int numberOfUsers) 
          { 
            return await _userManager.Users.OrderByDescending(
             x => x.Score).ToListAsync(); 
          } 

          public async Task UpdateUser(UserModel userModel) 
          { 
            await _userManager.UpdateAsync(userModel); 
          } 
        } 

Note that you should also update the UserServiceTest class to work with the new constructor. For that, you will also have to create a mock for the UserManager class and pass it to the constructor. For the moment, you can just comment the test out and update it later. But don't forget to do it!

  1. 更新UserRegistrationController中的EmailConfirmation方法,并使用之前添加的GetEmailConfirmationCode方法检索邮件代码:
        var urlAction = new UrlActionContext 
        { 
          Action = "ConfirmEmail", 
          Controller = "UserRegistration", 
          Values = new { email, code =
           await _userService.GetEmailConfirmationCode(user) }, 
          Protocol = Request.Scheme, 
          Host = Request.Host.ToString() 
        };
  1. 更新UserRegistrationController中的ConfirmEmail方法; 需要调用UserService中的ConfirmEmail方法来完成邮件确认:
        [HttpGet] 
        public async Task<IActionResult> ConfirmEmail(string email,
         string code) 
        { 
          var confirmed = await _userService.ConfirmEmail(email, code); 

          if (!confirmed) 
            return BadRequest(); 

          return RedirectToAction("Index", "Home"); 
        } 
  1. Models文件夹中添加一个名为RoleModel的新类,并使其继承自IdentityRole<long>,因为它将被内置的 ASP. php 使用.NET Core 2.0 身份验证功能:
        public class RoleModel : IdentityRole<Guid> 
        { 
          public RoleModel() 
          { 
          } 

          public RoleModel(string roleName) : base(roleName) 
          { 
          } 
        } 
  1. 更新游戏数据库上下文,并添加一个新的角色模型 DbSet:
        public DbSet<RoleModel> RoleModels { get; set; } 
  1. Startup类中注册身份验证服务和身份验证服务,然后使用之前添加的新角色模型:
        services.AddIdentity<UserModel, RoleModel>(options => 
        { 
          options.Password.RequiredLength = 1; 
          options.Password.RequiredUniqueChars = 0; 
          options.Password.RequireNonAlphanumeric = false; 
          options.Password.RequireUppercase = false; 
          options.SignIn.RequireConfirmedEmail = false; 
        }).AddEntityFrameworkStores<GameDbContext>() 
        .AddDefaultTokenProviders(); 

        services.AddAuthentication(options => { 
          options.DefaultScheme =
            CookieAuthenticationDefaults.AuthenticationScheme; 
          options.DefaultSignInScheme =
            CookieAuthenticationDefaults.AuthenticationScheme; 
          options.DefaultAuthenticateScheme = 
            CookieAuthenticationDefaults.AuthenticationScheme; 
        }).AddCookie();  
  1. 更新通信中间件,从类中删除_userService私有成员,并相应地更新构造函数:
        public CommunicationMiddleware(RequestDelegate next) 
        { 
          _next = next; 
        } 
  1. 更新通信中间件中的两个ProcessEmailConfirmation方法,因为它们必须是异步的才能与 ASP 一起工作.NET 2.0 的身份:
        private async Task ProcessEmailConfirmation(HttpContext 
         context, WebSocket currentSocket, CancellationToken ct,
         string email) 
        { 
          var userService = 
            context.RequestServices.GetRequiredService<IUserService>(); 
          var user = await userService.GetUserByEmail(email); 
          while (!ct.IsCancellationRequested && 
                 !currentSocket.CloseStatus.HasValue &&
                  user?.IsEmailConfirmed == false) 
          { 
            await SendStringAsync(currentSocket,
             "WaitEmailConfirmation", ct); 
            await Task.Delay(500); 
            user = await userService.GetUserByEmail(email); 
          } 

          if (user.IsEmailConfirmed) 
          { 
            await SendStringAsync(currentSocket, "OK", ct); 
          } 
        } 

        private async Task ProcessEmailConfirmation(HttpContext context) 
        { 
          var userService =
            context.RequestServices.GetRequiredService<IUserService>(); 
          var email = context.Request.Query["email"]; 

          UserModel user = await userService.GetUserByEmail(email); 

          if (string.IsNullOrEmpty(email)) 
          { 
            await context.Response.WriteAsync("BadRequest:Email is
             required"); 
          } 
          else if ((await 
           userService.GetUserByEmail(email)).IsEmailConfirmed) 
          { 
            await context.Response.WriteAsync("OK"); 
          } 
        } 
  1. 更新GameInvitationService,并将公共构造函数设置为 static。
  2. Startup类中删除以下DbContextOptions注册; 它将在下一步被另一个替换:
        var dbContextOptionsbuilder = 
          new DbContextOptionsBuilder<GameDbContext>() 
           .UseSqlServer(connectionString); 
        services.AddSingleton(dbContextOptionsbuilder.Options); 
  1. 更新Startup类,并添加一个新的DbContextOptions注册:
        services.AddScoped(typeof(DbContextOptions<GameDbContext>),
        (serviceProvider) => 
        { 
          return new DbContextOptionsBuilder<GameDbContext>() 
           .UseSqlServer(connectionString).Options; 
        }); 
  1. 更新Startup类中的Configure方法,然后替换方法末尾执行数据库迁移的代码:
        var provider = app.ApplicationServices; 
        var scopeFactory =
          provider.GetRequiredService<IServiceScopeFactory>(); 
        using (var scope = scopeFactory.CreateScope()) 
        using (var context =
          scope.ServiceProvider.GetRequiredService<GameDbContext>()) 
        { 
          context.Database.Migrate(); 
        }
  1. 更新GameInvitationController中的Index方法:
        ... 
        var invitation =
          gameInvitationService.Add(gameInvitationModel).Result; 
        return RedirectToAction("GameInvitationConfirmation",
         new { id = invitation.Id }); 
        ... 
  1. 更新GameInvitationController中的ConfirmGameInvitation方法,为现有用户注册添加额外字段:
        await _userService.RegisterUser(new UserModel 
        { 
          Email = gameInvitation.EmailTo, 
          EmailConfirmationDate = DateTime.Now, 
          EmailConfirmed = true, 
          FirstName = "", 
          LastName = "", 
          Password = "Azerty123!", 
          UserName = gameInvitation.EmailTo 
        }); 

Note that the automatic creation and registration of the invited user is only a temporary workaround that we have added to simplify the example application. In the real world, you will need to handle this case differently and replace the temporary workaround with a real solution.

  1. 更新GameSessionService中的CreateGameSessionAddTurn方法,重新提取游戏会话服务接口:
        public async Task<GameSessionModel> CreateGameSession(
         Guid invitationId, UserModel invitedBy,
         UserModel invitedPlayer) 
        { 
          var session = new GameSessionModel 
          { 
            User1 = invitedBy, 
            User2 = invitedPlayer, 
            Id = invitationId, 
            ActiveUser = invitedBy 
          }; 
          _sessions.Add(session); 
          return session; 
        } 

        public async Task<GameSessionModel> AddTurn(Guid id,
         UserModel user, int x, int y) 
        { 
          List<Models.TurnModel> turns; 
          var gameSession = _sessions.FirstOrDefault(session =>
           session.Id == id); 
          if (gameSession.Turns != null && gameSession.Turns.Any()) 
            turns = new List<Models.TurnModel>(gameSession.Turns); 
          else 
            turns = new List<TurnModel>(); 

          turns.Add(new TurnModel 
          { 
            User = user, 
            X = x, 
            Y = y, 
            IconNumber = user.Email == gameSession.User1?.Email ? "1" : "2" 
          }); 

          gameSession.Turns = turns; 
          gameSession.TurnNumber = gameSession.TurnNumber + 1; 
          if (gameSession.User1?.Email == user.Email) 
            gameSession.ActiveUser = gameSession.User2; 
          else 
            gameSession.ActiveUser = gameSession.User1; 

          gameSession.TurnFinished = true; 
          _sessions = new ConcurrentBag<GameSessionModel>
           (_sessions.Where(u => u.Id != id)) 
          { 
            gameSession 
          }; 
          return gameSession; 
        } 
  1. 更新GameSessionController中的Index方法:
        public async Task<IActionResult> Index(Guid id) 
        { 
          var session = await _gameSessionService.GetGameSession(id); 
          var userService =
            HttpContext.RequestServices.GetService<IUserService>(); 

          if (session == null) 
          { 
            var gameInvitationService = 
              Request.HttpContext.RequestServices.GetService
              <IGameInvitationService>(); 
            var invitation = await gameInvitationService.Get(id); 

            var invitedPlayer =
              await userService.GetUserByEmail(invitation.EmailTo); 
            var invitedBy =
              await userService.GetUserByEmail(invitation.InvitedBy); 

            session =
              await _gameSessionService.CreateGameSession(
               invitation.Id, invitedBy, invitedPlayer); 
          } 
          return View(session); 
        } 
  1. 更新GameSessionController中的SetPosition方法,传递一个turn.User而不是turn.User.Email:
        gameSession = await _gameSessionService.AddTurn(gameSession.Id,
         turn.User, turn.X, turn.Y); 
  1. 更新 Game Db Context 中的OnModelCreating方法,并添加一个WinnerId外键:
        ... 
        modelBuilder.Entity(typeof(GameSessionModel)) 
         .HasOne(typeof(UserModel), "Winner") 
         .WithMany() 
         .HasForeignKey("WinnerId").OnDelete(DeleteBehavior.Restrict); 
        ... 
  1. 更新GameInvitationController中的GameInvitationConfirmation方法; 它必须是异步的工作与 ASP.NET Core 2.0 标识:
        [HttpGet] 
        public async Task<IActionResult> GameInvitationConfirmation(
         Guid id, [FromServices]IGameInvitationService
         gameInvitationService) 
        { 
          return await Task.Run(() => 
          { 
            var gameInvitation = gameInvitationService.Get(id).Result; 
            return View(gameInvitation); 
          }); 
        }
  1. 更新HomeController中的IndexSetCulture方法; 它们必须是异步的工作与 ASP.NET Core 2.0 标识:
        public async Task<IActionResult> Index() 
        { 
          return await Task.Run(() => 
          { 
            var culture =
              Request.HttpContext.Session.GetString("culture"); 
            ViewBag.Language = culture; 
            return View(); 
          }); 
        } 

        public async Task<IActionResult> SetCulture(string culture) 
        { 
          return await Task.Run(() => 
          { 
            Request.HttpContext.Session.SetString("culture", culture); 
            return RedirectToAction("Index"); 
          }); 
        } 
  1. 更新UserRegistrationController中的Index方法; 它必须是异步的工作与 ASP.NET 2.0 的身份:
        public async Task<IActionResult> Index() 
        { 
          return await Task.Run(() => 
          { 
            return View(); 
          }); 
        } 
  1. 打开包管理器控制台并执行Add-Migration IdentityDb命令。
  2. 通过在包管理器控制台中执行Update-Database命令来更新数据库。

  3. 启动应用并注册一个新用户,然后验证一切是否仍按预期工作。

Note that you have to use a complex password, such as Azerty123!, to be able to finish the user registration successfully now, since you have implemented the integrated features of ASP.NET Core 2.0 Identity in this section, which require complex passwords.

添加基本的用户表单验证

太棒了! 您已经注册了身份验证中间件并准备了数据库。 在下一步中,您将为Tic-Tac-Toe应用实现基本的用户身份验证。

下面的示例演示了如何修改用户注册,并添加一个简单的登录表单,其中包含用户登录和密码文本框,用于验证用户:

  1. 添加一个名为LoginModel的新模型到Models文件夹:
        public class LoginModel 
        { 
          [Required] 
          public string UserName { get; set; } 
          [Required] 
          public string Password { get; set; } 
          public string ReturnUrl { get; set; } 
        } 
  1. Views文件夹中添加一个名为Account的新文件夹,并在此新文件夹中添加一个名为Login.cshtml的新文件; 它将包含登录视图:
        @model TicTacToe.Models.LoginModel 
        <div class="container"> 
          <div id="loginbox" style="margin-top:50px;" class="mainbox 
           col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2"> 
            <div class="panel panel-info"> 
              <div class="panel-heading"> 
                <div class="panel-title">Sign In</div> 
              </div> 
              <div style="padding-top:30px" class="panel-body"> 
                <div style="display:none" id="login-alert" 
                 class="alert alert-danger col-sm-12"></div> 
                <form id="loginform" class="form-horizontal"
                 role="form" asp-action="Login" asp-controller="Account"> 
                  <input type="hidden" asp-for="ReturnUrl" /> 
                  <div asp-validation-summary="ModelOnly"
                   class="text-danger"></div> 
                  <div style="margin-bottom: 25px" class="input-group"> 
                    <span class="input-group-addon"><i class="glyphicon
                     glyphicon-user"></i></span> 
                    <input type="text" class="form-control" 
                     asp-for="UserName" value="" placeholder="username 
                     or email"> 
                  </div> 
                  <div style="margin-bottom: 25px" class="input-group"> 
                    <span class="input-group-addon"><i class="glyphicon
                     glyphicon-lock"></i></span> 
                    <input type="password" class="form-control" 
                     asp-for="Password" placeholder="password"> 
                  </div> 
                  <div style="margin-top:10px" class="form-group"> 
                    <div class="col-sm-12 controls"> 
                      <button type="submit" id="btn-login" href="#" 
                       class="btn btn-success">Login</button> 
                    </div> 
                  </div> 
                  <div class="form-group"> 
                    <div class="col-md-12 control"> 
                      <div style="border-top: 1px solid#888; 
                       padding-top:15px; font-size:85%"> 
                        Don't have an account? 
                        <a asp-action="Index" 
                         asp-controller="UserRegistration">Sign Up Here
                        </a> 
                      </div> 
                    </div> 
                  </div> 
                </form> 
              </div> 
            </div> 
          </div> 
        </div> 
  1. 更新UserService,添加SignInManager私有字段,并更新构造函数:
        ... 
        private SignInManager<UserModel> _signInManager; 
        public UserService(ApplicationUserManager userManager,
         ILogger<UserService> logger, SignInManager<UserModel>
         signInManager) 
        { 
          ... 
          _signInManager = signInManager; 
          ... 
        } 
        ...
  1. UserService中添加两个新方法SignInUserSignOutUser,并更新用户服务接口:
        public async Task<SignInResult> SignInUser(
         LoginModel loginModel, HttpContext httpContext) 
        { 
          var start = DateTime.Now; 
          _logger.LogTrace($"signin user {loginModel.UserName}"); 

          var stopwatch = new Stopwatch(); 
          stopwatch.Start(); 

          try 
          { 
            var user =
              await _userManager.FindByNameAsync(loginModel.UserName); 
            var isValid =
              await _signInManager.CheckPasswordSignInAsync(user,
               loginModel.Password, true); 
            if (!isValid.Succeeded) 
            { 
              return SignInResult.Failed; 
            } 

            if (!await _userManager.IsEmailConfirmedAsync(user)) 
            { 
              return SignInResult.NotAllowed; 
            } 

            var identity = new ClaimsIdentity(
              CookieAuthenticationDefaults.AuthenticationScheme); 
            identity.AddClaim(new Claim(
              ClaimTypes.Name, loginModel.UserName)); 
            identity.AddClaim(new Claim(
              ClaimTypes.GivenName, user.FirstName)); 
            identity.AddClaim(new Claim(
              ClaimTypes.Surname, user.LastName)); 
            identity.AddClaim(new Claim(
              "displayName", $"{user.FirstName} {user.LastName}")); 

            if (!string.IsNullOrEmpty(user.PhoneNumber)) 
            { 
              identity.AddClaim(new Claim(ClaimTypes.HomePhone,
               user.PhoneNumber)); 
            } 

            identity.AddClaim(new Claim("Score",
             user.Score.ToString())); 

            await httpContext.SignInAsync(
             CookieAuthenticationDefaults.AuthenticationScheme, 
             new ClaimsPrincipal(identity),
             new AuthenticationProperties { IsPersistent = false }); 

            return isValid; 
          } 
          catch (Exception ex) 
          { 
            _logger.LogError($"can not sigin user
             {loginModel.UserName} - {ex}"); 
            throw ex; 
          } 
          finally 
          { 
            stopwatch.Stop(); 
            _logger.LogTrace($"sigin user {loginModel.UserName} 
            finished in {stopwatch.Elapsed}"); 
          } 
        } 

        public async Task SignOutUser(HttpContext httpContext) 
        { 
          await _signInManager.SignOutAsync(); 
          await httpContext.SignOutAsync(new AuthenticationProperties {
           IsPersistent = false }); 
          return; 
        } 
  1. Controllers文件夹中添加名为AccountController的新控制器,并实现三种处理用户身份验证的新方法:
        public class AccountController : Controller 
        { 
          private IUserService _userService; 
          public AccountController(IUserService userService) 
          { 
            _userService = userService; 
          } 

          public async Task<IActionResult> Login(string returnUrl) 
          { 
            return await Task.Run(() => 
            { 
              var loginModel = new LoginModel { ReturnUrl = returnUrl }; 
              return View(loginModel); 
            }); 
          } 

          [HttpPost] 
          public async Task<IActionResult> Login(LoginModel loginModel) 
          { 
            if (ModelState.IsValid) 
            { 
              var result = await _userService.SignInUser(loginModel,
               HttpContext); 

              if (result.Succeeded) 
              { 
                if (!string.IsNullOrEmpty(loginModel.ReturnUrl)) 
                    return Redirect(loginModel.ReturnUrl); 
                else 
                    return RedirectToAction("Index", "Home"); 
              } 
              else 
                ModelState.AddModelError("", result.IsLockedOut ?
                 "User is locked" : "User is not allowed"); 
            } 
            return View(); 
          } 

          public IActionResult Logout() 
          { 
            _userService.SignOutUser(HttpContext).Wait(); 
            HttpContext.Session.Clear(); 
            return RedirectToAction("Index", "Home"); 
          } 
        } 
  1. 更新GameSessionController中的CheckGameSessionIsFinished方法:
        [HttpGet("/restapi/v1/CheckGameSessionIsFinished/{sessionId}")] 
        public async Task<IActionResult> CheckGameSessionIsFinished(
         Guid sessionId) 
        { 
          if (sessionId != Guid.Empty) 
          { 
            var session =
              await _gameSessionService.GetGameSession(sessionId); 
            if (session != null) 
            { 
              if (session.Turns.Count() == 9) 
                return Ok("The game was a draw."); 

              var userTurns = session.Turns.Where(
                x => x.User.Id == session.User1.Id).ToList(); 
              var user1Won = CheckIfUserHasWon(session.User1?.Email,
                userTurns); 
              if (user1Won) 
              { 
                return Ok($"{session.User1.Email} has won the game."); 
              } 
              else 
              { 
                userTurns = session.Turns.Where(
                  x => x.User.Id == session.User2.Id).ToList(); 
                var user2Won = CheckIfUserHasWon(session.User2?.Email,
                 userTurns); 

                if (user2Won) 
                   return Ok($"{session.User2.Email} has won
                    the game."); 
                else 
                   return Ok(""); 
              } 
            } 
            else 
            { 
              return NotFound($"Cannot find session {sessionId}."); 
            } 
          } 
          else 
          { 
            return BadRequest("SessionId is null."); 
          } 
        }
  1. 更新Views/Shared/_Menu.cshtml文件,并替换方法顶部的现有代码块:
        @using Microsoft.AspNetCore.Http; 
        @{ 
          var email = User?.Identity?.Name ??
           Context.Session.GetString("email"); 
          var displayName = User.Claims.FirstOrDefault(
           x => x.Type == "displayName")?.Value ??  
           Context.Session.GetString("displayName"); 
        }
  1. 更新Views/Shared/_Menu.cshtml文件,为已经通过身份验证的用户显示 display Name 元素,或为通过身份验证的用户显示 Login 元素; 为此,替换最后一个<li>元素:
        <li> 
          @if (!string.IsNullOrEmpty(email)) 
          { 
            Html.RenderPartial("_Account",
             new TicTacToe.Models.AccountModel { Email = email,
             DisplayName = displayName }); 
          } 
          else 
          { 
            <a asp-area="" asp-controller="Account" 
             asp-action="Login">Login</a> 
          } 
        </li> 
  1. 更新Views/Shared/_Account.cshtml文件,并替换注销和查看详细信息链接:
        <a class="btn btn-danger btn-block" asp-controller="Account"
         asp-action="Logout" asp-area="">Log Off</a> 
        <a class="btn btn-default btn-block" asp-action="Index"
         asp-controller="Home" asp-area="Account">View Details</a> 
  1. 转到Views\Shared\Components\GameSession文件夹,更新default.cshtml文件,以改善视觉表现:
        @using Microsoft.AspNetCore.Http 
        @model TicTacToe.Models.GameSessionModel 
        @{ 
          var email = Context.Session.GetString("email"); 
        } 
        <div id="gameBoard"> 
          <table> 
            @for (int rows = 0; rows < 3; rows++) 
            { 
              <tr style="height:150px;"> 
                @for (int columns = 0; columns < 3; columns++) 
                { 
                  <td style="width:150px; border:1px solid #808080;
                   text-align:center; vertical-align:middle"
                   id="@($"c_{rows}_{columns}")"> 
                    @{ 
                      var position = Model.Turns?.FirstOrDefault(
                       turn => turn.X == columns && turn.Y == rows); 
                      if (position != null) 
                      { 
                        if (position.User == Model.User1) 
                        { 
                          <i class="glyphicon glyphicon-unchecked"></i> 
                        } 
                        else 
                        { 
                          <i class="glyphicon glyphicon-remove-circle"></i> 
                        } 
                      } 
                      else 
                      { 
                        <a class="btn btn-default btn-SetPosition" 
                         style="width:150px; min-height:150px;"
                         data-X="@columns" data-Y="@rows"> 
                          &nbsp; 
                         </a> 
                       } 
                    } 
                  </td> 
                } 
              </tr> 
            } 
          </table> 
        </div> 
        <div class="alert" id="divAlertWaitTurn"> 
          <i class="glyphicon glyphicon-alert">Please wait until the 
             other user has finished his turn.</i> 
        </div>
  1. 启动应用,单击顶部菜单中的 Login 元素,并以现有用户的身份登录(或注册为用户,如果你之前没有这样做):

  1. 单击“注销”按钮。 你应该注销并被重定向回主页:

添加外部提供者身份验证

在下一节中,我们将通过使用 Facebook 作为身份验证提供者来展示外部提供者身份验证。

下面是本例中控制流的概述:

  1. 用户单击专用的外部提供者登录按钮。
  2. 相应的控制器接收指示需要哪个提供者的请求,然后向外部提供者发起挑战。
  3. 外部提供者发送一个 HTTP 回调(POSTGET),其中包含提供者名称、一个键和应用的一些用户声明。
  4. 索赔与内部应用用户匹配。
  5. 如果没有内部用户可以与索赔匹配,则用户要么被重定向到特定的注册表单,要么被拒绝。

Note that the implementation steps are the same for all external providers if they support OWIN and ASP.NET Core 2.0 Identity, and that you may even create your own providers and integrate them in the same way.

我们现在要通过 Facebook 实现外部提供者认证:

  1. 更新登录表单,并在标准登录按钮之后直接添加一个名为Login with Facebook的按钮:
        <a id="btn-fblogin" asp-action="ExternalLogin"
         asp-controller="Account" asp-route-Provider="Facebook" 
         class="btn btn-primary">Login with Facebook</a>
  1. 更新UserService和用户服务界面,然后添加三个新方法GetExternalAuthenticationPropertiesGetExternalLoginInfoAsyncExternalLoginSignInAsync:
        public async Task<AuthenticationProperties>
         GetExternalAuthenticationProperties(string provider,
         string redirectUrl) 
        { 
          return await Task.FromResult(
           _signInManager.ConfigureExternalAuthenticationProperties(
           provider, redirectUrl)); 
        } 

        public async Task<ExternalLoginInfo> GetExternalLoginInfoAsync() 
        { 
          return await _signInManager.GetExternalLoginInfoAsync(); 
        } 

        public async Task<SignInResult> ExternalLoginSignInAsync(
         string loginProvider, string providerKey, bool isPersistent) 
        { 
          _logger.LogInformation($"Sign in user with external login
           {loginProvider} - {providerKey}"); 
          return await _signInManager.ExternalLoginSignInAsync(
           loginProvider, providerKey, isPersistent); 
        } 
  1. 更新AccountController,并添加两个新方法ExternalLoginExternalLoginCallBack:
        [AllowAnonymous] 
        public async Task<ActionResult> ExternalLogin(string provider,
         string ReturnUrl) 
        { 
          var redirectUrl = Url.Action(nameof(ExternalLoginCallBack),
           "Account", new { ReturnUrl = ReturnUrl }, Request.Scheme,
            Request.Host.ToString()); 
          var properties =
            await _userService.GetExternalAuthenticationProperties(
             provider, redirectUrl); 
          ViewBag.ReturnUrl = redirectUrl; 
          return Challenge(properties, provider); 
        }  

        [AllowAnonymous] 
        public async Task<IActionResult> ExternalLoginCallBack(
         string returnUrl, string remoteError = null) 
        { 
          if (remoteError != null) 
          { 
            ModelState.AddModelError(string.Empty, $"Error from 
             external provider: {remoteError}"); 
            ViewBag.ReturnUrl = returnUrl; 
            return View("Login"); 
          } 

          var info = await _userService.GetExternalLoginInfoAsync(); 
          if (info == null) 
          { 
            return RedirectToAction("Login",
             new { ReturnUrl = returnUrl }); 
          } 

          var result =
           await _userService.ExternalLoginSignInAsync(
            info.LoginProvider, info.ProviderKey, isPersistent: false); 
          if (result.Succeeded) 
          { 
            if (!string.IsNullOrEmpty(returnUrl)) 
              return Redirect(returnUrl); 
            else 
              return RedirectToAction("Index", "Home"); 
          } 
          if (result.IsLockedOut) 
          { 
            return View("Lockout"); 
          } 
          else 
          {                 
            return View("NotFound"); 
          } 
        } 
  1. Startup类中注册 Facebook 中间件:
        services.AddAuthentication(options => { 
          options.DefaultScheme =
           CookieAuthenticationDefaults.AuthenticationScheme; 
          options.DefaultSignInScheme =
           CookieAuthenticationDefaults.AuthenticationScheme; 
          options.DefaultAuthenticateScheme =
           CookieAuthenticationDefaults.AuthenticationScheme; 
        }).AddCookie().AddFacebook(facebook => 
        { 
          facebook.AppId = "123"; 
          facebook.AppSecret = "123"; 
          facebook.ClientId = "123"; 
          facebook.ClientSecret = "123"; 
        }); 

Note that you must update the Facebook Middleware configuration and register your application in the Facebook developer portal before being able to authenticate logins with a Facebook account.

Please go to http://developer.facebook.com for more information.

  1. 启动应用,点击登录与 Facebook 按钮,登录与你的 Facebook 凭证,并验证一切是预期的工作:

使用双因素身份验证

前面看到的标准安全机制只需要一个简单的用户名和密码,这使得网络犯罪分子越来越容易访问机密数据,比如个人和财务细节,他们可以通过破解密码或截取用户凭据(电子邮件、网络监听等)。 然后,这些数据可以被用于实施财务欺诈和身份盗窃。

双因素身份验证增加了额外的安全层,因为它不仅需要用户名和密码,还需要只有用户才能提供的双因素代码(物理设备、软件生成的等等)。 这使得潜在的入侵者更难获得访问权限,从而有助于防止身份和数据盗窃。

所有主流网站都提供双因素认证选项,所以让我们将其添加到Tic-Tac-Toe应用中:

  1. 添加一个名为TwoFactorCodeModel的新模型到Models文件夹:
        public class TwoFactorCodeModel 
        { 
          [Key] 
          public long Id { get; set; } 
          public Guid UserId { get; set; } 
          [ForeignKey("UserId")] 
          public UserModel User { get; set; } 
          public string TokenProvider { get; set; } 
          public string TokenCode { get; set; } 
        } 
  1. 添加一个名为TwoFactorEmailModel的新模型到Models文件夹:
        public class TwoFactorEmailModel 
        { 
          public string DisplayName { get; set; } 
          public string Email { get; set; } 
          public string ActionUrl { get; set; } 
        } 
  1. 通过添加相应的DbSet在 Game Db 上下文中注册TwoFactorCodeModel:
        public DbSet<TwoFactorCodeModel> TwoFactorCodeModels { get; set; } 
  1. 打开 NuGet Package Manager Console 并执行Add-Migration AddTwoFactorCode命令,然后通过执行Update-Database命令更新数据库。
  2. 更新应用用户管理器,然后添加三个新方法SetTwoFactorEnabledAsyncGenerateTwoFactorTokenAsyncVerifyTwoFactorTokenAsync:
        public override async Task<IdentityResult>
         SetTwoFactorEnabledAsync(UserModel user, bool enabled) 
        { 
          try 
          { 
            using (var db = new GameDbContext(_dbContextOptions)) 
            { 
              var current = await db.UserModels.FindAsync(user.Id); 
              current.TwoFactorEnabled = enabled; 
              await db.SaveChangesAsync(); 
              return IdentityResult.Success; 
            } 
          } 
          catch (Exception ex) 
          { 
            return IdentityResult.Failed(new IdentityError {
             Description = ex.ToString() }); 
          } 
        } 

        public override async Task<string>
         GenerateTwoFactorTokenAsync(UserModel user,
         string tokenProvider) 
        { 
          using (var dbContext = new GameDbContext(_dbContextOptions)) 
          { 
            var emailTokenProvider = new EmailTokenProvider<UserModel>(); 
            var token = await emailTokenProvider.GenerateAsync(
             "TwoFactor", this, user); 
            dbContext.TwoFactorCodeModels.Add(new TwoFactorCodeModel 
            { 
              TokenCode = token, 
              TokenProvider = tokenProvider, 
              UserId = user.Id 
            }); 

            if (dbContext.ChangeTracker.HasChanges()) 
              await dbContext.SaveChangesAsync(); 

            return token; 
          } 
        } 

        public override async Task<bool>
         VerifyTwoFactorTokenAsync(UserModel user,
         string tokenProvider, string token) 
        { 
          using (var dbContext = new GameDbContext(_dbContextOptions)) 
          { 
            return await dbContext.TwoFactorCodeModels.AnyAsync(
             x => x.TokenProvider == tokenProvider &&
             x.TokenCode == token && x.UserId == user.Id); 
          } 
        }
  1. 转到Areas/Account/Views/Home文件夹,更新索引视图:
        @model TicTacToe.Models.UserModel 
        @using Microsoft.AspNetCore.Identity 
        @inject UserManager<TicTacToe.Models.UserModel> UserManager 
        @{ 
          var isTwoFactor =
            UserManager.GetTwoFactorEnabledAsync(Model).Result; 
          ViewData["Title"] = "Index"; 
          Layout = "~/Views/Shared/_Layout.cshtml"; 
        } 
        <h3>Account Details</h3> 
        <div class="container"> 
          <div class="row"> 
            <div class="col-xs-12 col-sm-6 col-md-6"> 
              <div class="well well-sm"> 
                <div class="row"> 
                  <div class="col-sm-6 col-md-4"> 
                    <Gravatar email="@Model.Email"></Gravatar> 
                  </div> 
                  <div class="col-sm-6 col-md-8"> 
                    <h4>@($"{Model.FirstName} {Model.LastName}")</h4> 
                     <p> 
                       <i class="glyphicon glyphicon-envelope">
                       </i>&nbsp;<a href="mailto:@Model.Email">
                        @Model.Email</a> 
                     </p> 
                     <p> 
                       <i class="glyphicon glyphicon-calendar">
                       </i>&nbsp;@Model.EmailConfirmationDate 
                     </p> 
                     <p> 
                       <i class="glyphicon glyphicon-star">
                       </i>&nbsp;@Model.Score 
                     </p> 
                     <p> 
                       <i class="glyphicon glyphicon-check"></i>
                        <text>Two Factor Authentication&nbsp;</text> 
                       @if (Model.TwoFactorEnabled) 
                       { 
                         <a asp-action="DisableTwoFactor">Disable</a> 
                       } 
                       else 
                       { 
                         <a asp-action="EnableTwoFactor">Enable</a> 
                       } 
                     </p> 
                  </div> 
                </div> 
              </div> 
            </div> 
          </div> 
        </div> 
  1. Areas/Account/Views文件夹中添加一个名为_ViewImports.cshtml的新文件:
        @using TicTacToe 
        @using Microsoft.AspNetCore.Mvc.Localization 
        @inject IViewLocalizer Localizer 
        @addTagHelper *, TicTacToe 
        @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 
  1. 更新UserService和用户服务接口,并添加两个新方法EnableTwoFactorGetTwoFactorCode:
        public async Task<IdentityResult> EnableTwoFactor(string name,
         bool enabled) 
        { 
          try 
          { 
            var user = await _userManager.FindByEmailAsync(name); 
            user.TwoFactorEnabled = true; 
            await _userManager.SetTwoFactorEnabledAsync(user, enabled); 
            return IdentityResult.Success; 
          } 
          catch (Exception ex) 
          { 
            throw; 
          } 
        } 

        public async Task<string> GetTwoFactorCode(string userName,
         string tokenProvider) 
        { 
          var user = await GetUserByEmail(userName); 
          return await _userManager.GenerateTwoFactorTokenAsync(user,
           tokenProvider); 
        }
  1. 更新UserService中的SignInUser方法来支持双因素认证,如果启用:
        public async Task<SignInResult> SignInUser(LoginModel
         loginModel, HttpContext httpContext) 
        { 
          var start = DateTime.Now; 
          _logger.LogTrace($"Signin user {loginModel.UserName}"); 
          var stopwatch = new Stopwatch(); 
          stopwatch.Start(); 

          try 
          { 
            var user =
              await _userManager.FindByNameAsync(loginModel.UserName); 
            var isValid =
              await _signInManager.CheckPasswordSignInAsync(user,
               loginModel.Password, true); 

            if (!isValid.Succeeded) 
            { 
              return SignInResult.Failed; 
            } 

            if (!await _userManager.IsEmailConfirmedAsync(user)) 
            { 
              return SignInResult.NotAllowed; 
            } 

            if (await _userManager.GetTwoFactorEnabledAsync(user)) 
            { 
              return SignInResult.TwoFactorRequired; 
            } 

            var identity = new ClaimsIdentity(
              CookieAuthenticationDefaults.AuthenticationScheme); 
            identity.AddClaim(new Claim(ClaimTypes.Name,
              loginModel.UserName)); 
            identity.AddClaim(new Claim(ClaimTypes.GivenName,
              user.FirstName)); 
            identity.AddClaim(new Claim(ClaimTypes.Surname,
              user.LastName)); 
            identity.AddClaim(new Claim("displayName",
              $"{user.FirstName} {user.LastName}")); 

            if (!string.IsNullOrEmpty(user.PhoneNumber)) 
            { 
              identity.AddClaim(new Claim(ClaimTypes.HomePhone,
               user.PhoneNumber)); 
            } 
            identity.AddClaim(new Claim("Score",
             user.Score.ToString())); 

            await httpContext.SignInAsync(
             CookieAuthenticationDefaults.AuthenticationScheme, 
             new ClaimsPrincipal(identity),
             new AuthenticationProperties { IsPersistent = false }); 

            return isValid; 
          } 
          catch (Exception ex) 
          { 
            _logger.LogError($"Ca not sigin user
             {loginModel.UserName} - {ex}"); 
            throw ex; 
          } 
          finally 
          { 
            stopwatch.Stop(); 
            _logger.LogTrace($"Sigin user {loginModel.UserName}
             finished in {stopwatch.Elapsed}"); 
          } 
        } 
  1. 转到Areas/Account/Controllers文件夹,并更新HomeController。 更新Index方法,添加两个新方法EnableTwoFactorDisableTwoFactor:
        [Authorize] 
        public async Task<IActionResult> Index() 
        { 
          var user =
            await _userService.GetUserByEmail(User.Identity.Name); 
          return View(user); 
        } 

        [Authorize] 
        public IActionResult EnableTwoFactor() 
        { 
          _userService.EnableTwoFactor(User.Identity.Name, true); 
          return RedirectToAction("Index"); 
        } 

        [Authorize] 
        public IActionResult DisableTwoFactor() 
        { 
          _userService.EnableTwoFactor(User.Identity.Name, false); 
          return RedirectToAction("Index"); 
        } 

Note that we will explain the [Authorize] decorator later in this chapter. It is used to add access restrictions to resources.

  1. 添加一个名为ValidateTwoFactorModel的新模型到Models文件夹:
        public class ValidateTwoFactorModel 
        { 
          public string UserName { get; set; } 
          public string Code { get; set; } 
        } 
  1. 更新AccountController,并添加一个新的方法SendEmailTwoFactor:
        private async Task SendEmailTwoFactor(string UserName) 
        { 
          var user = await _userService.GetUserByEmail(UserName); 
          var urlAction = new UrlActionContext 
          { 
            Action = "ValidateTwoFactor", 
            Controller = "Account", 
            Values = new { email = UserName,
            code = await _userService.GetTwoFactorCode(
             user.UserName, "Email") }, 
            Protocol = Request.Scheme, 
            Host = Request.Host.ToString() 
          }; 

          var TwoFactorEmailModel = new TwoFactorEmailModel 
          { 
            DisplayName = $"{user.FirstName} {user.LastName}", 
             Email = UserName, 
             ActionUrl = Url.Action(urlAction) 
          }; 
          var emailRenderService = 
            HttpContext.RequestServices.GetService
             <IEmailTemplateRenderService>(); 
          var emailService = 
            HttpContext.RequestServices.GetService
             <IEmailService>(); 
          var message =
            await emailRenderService.RenderTemplate(
             "EmailTemplates/TwoFactorEmail", TwoFactorEmailModel,
              Request.Host.ToString()); 
          try 
          { 
            emailService.SendEmail(UserName, "Tic-Tac-Toe Two Factor
             Code", message).Wait(); 
          } 
          catch 
          { 
          } 
        } 

Note that for calling RequestServices.GetService<T>();, you must also add using Microsoft.Extensions.DependencyInjection; as you have done before in other examples.

  1. 更新AccountController中的Login方法:
        [HttpPost] 
        public async Task<IActionResult> Login(LoginModel loginModel) 
        { 
          if (ModelState.IsValid) 
          { 
            var result = await _userService.SignInUser(loginModel,
             HttpContext); 
            if (result.Succeeded) 
            { 
              if (!string.IsNullOrEmpty(loginModel.ReturnUrl)) 
                return Redirect(loginModel.ReturnUrl); 
              else 
                return RedirectToAction("Index", "Home"); 
            } 
            else if (result.RequiresTwoFactor) 
            { 
              await SendEmailTwoFactor(loginModel.UserName); 
              return RedirectToAction("ValidateTwoFactor"); 
            } 
            else 
              ModelState.AddModelError("", result.IsLockedOut ? "User 
               is locked" : "User is not allowed"); 
          } 

          return View(); 
        }
  1. 添加一个名为ValidateTwoFactor的新视图到Views/Account文件夹:
        @model TicTacToe.Models.ValidateTwoFactorModel 
        @{ 
          ViewData["Title"] = "Validate Two Factor"; 
          Layout = "~/Views/Shared/_Layout.cshtml"; 
        } 
        <div class="container"> 
          <div id="loginbox" style="margin-top:50px;" class="mainbox
           col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2"> 
            <div class="panel panel-info"> 
              <div class="panel-heading"> 
                <div class="panel-title">Validate Two Factor Code</div> 
              </div> 
              <div style="padding-top:30px" class="panel-body"> 
                <div class="text-center"> 
                  <form asp-controller="Account"
                   asp-action="ValidateTwoFactor" method="post"> 
                    <div asp-validation-summary="All"></div> 
                    <div style="margin-bottom: 25px" class="input-group"> 
                      <span class="input-group-addon"><i
                       class="glyphicon glyphicon-envelope
                       color-blue"></i></span> 
                      <input id="email" asp-for="UserName" 
                       placeholder="email address" 
                       class="form-control" type="email"> 
                    </div> 
                    <div style="margin-bottom: 25px" class="input-group"> 
                      <span class="input-group-addon"><i 
                       class="glyphicon glyphicon-lock
                       color-blue"></i></span> 
                      <input id="Code" asp-for="Code" 
                       placeholder="Enter your code" class="form-control"> 
                    </div> 
                    <div style="margin-bottom: 25px" class="input-group"> 
                      <input name="submit" 
                       class="btn btn-lg btn-primary btn-block" 
                       value="Validate your code" type="submit"> 
                    </div> 
                  </form> 
                </div> 
              </div> 
            </div> 
          </div> 
        </div>
  1. 添加一个名为TwoFactorEmail的新视图到Views/EmailTemplates文件夹:
        @model TicTacToe.Models.TwoFactorEmailModel 
        @{ 
          ViewData["Title"] = "View"; 
          Layout = "_LayoutEmail"; 
        } 
        <h1>Welcome @Model.DisplayName</h1> 
        You have requested a two factor code, please click <a
         href="@Model.ActionUrl">here</a> to continue. 
  1. 更新UserService和用户服务接口,并添加一个新方法ValidateTwoFactor:
        public async Task<bool> ValidateTwoFactor(string userName,
         string tokenProvider, string token, HttpContext httpContext) 
        { 
          var user = await GetUserByEmail(userName); 
          if (await _userManager.VerifyTwoFactorTokenAsync(user,
           tokenProvider, token)) 
          { 
            var identity =
              new ClaimsIdentity(
               CookieAuthenticationDefaults.AuthenticationScheme); 
            identity.AddClaim(new Claim(ClaimTypes.Name,
             user.UserName)); 
            identity.AddClaim(new Claim(ClaimTypes.GivenName,
             user.FirstName)); 
            identity.AddClaim(new Claim(ClaimTypes.Surname,
             user.LastName)); 
            identity.AddClaim(new Claim("displayName",
             $"{user.FirstName} {user.LastName}")); 

            if (!string.IsNullOrEmpty(user.PhoneNumber)) 
            { 
              identity.AddClaim(new Claim(ClaimTypes.HomePhone,
               user.PhoneNumber)); 
            } 

            identity.AddClaim(new Claim("Score",
             user.Score.ToString())); 
            await httpContext.SignInAsync(
             CookieAuthenticationDefaults.AuthenticationScheme, 
             new ClaimsPrincipal(identity),
             new AuthenticationProperties { IsPersistent = false }); 

            return true; 
          } 
          return false; 
        } 
  1. 更新AccountController,并添加两种双因素身份验证的新方法:
        public async Task<IActionResult> ValidateTwoFactor(
         string email, string code) 
        { 
          return await Task.Run(() => 
          { 
            return View(new ValidateTwoFactorModel { Code = code, 
             UserName = email }); 
          }); 
        } 

        [HttpPost] 
        public async Task<IActionResult> ValidateTwoFactor(
         ValidateTwoFactorModel validateTwoFactorModel) 
        { 
          if (ModelState.IsValid) 
          { 
            await _userService.ValidateTwoFactor(
             validateTwoFactorModel.UserName, "Email",
             validateTwoFactorModel.Code, HttpContext); 
            return RedirectToAction("Index", "Home"); 
          } 

          return View(); 
        }
  1. 启动应用,以现有用户身份登录,然后转到 Account Details 页面。 启用双因素身份验证(在此步骤之前,您可能需要重新创建数据库并注册一个新用户):

  1. 以用户身份登录,进入登录页面,然后再次登录。 这次您将被要求输入一个双因素认证码:

  1. 您将收到一封带有双重认证码的电子邮件:

  1. 点击电子邮件中的链接,一切都会自动为你填写。 登录并验证一切是否正常工作:

增加忘记密码和密码重置机制

现在您已经了解了如何向应用添加身份验证,您必须考虑如何帮助用户重置他们忘记的密码。 用户会忘记他们的密码,这是可能发生的,所以您需要一些适当的机制。

处理这类请求的标准方法是向用户发送一个电子邮件重置链接。 然后用户可以更新他们的密码,而不用冒着通过电子邮件发送明文密码的风险。 将用户密码直接发送到用户电子邮件是不安全的,应该不惜一切代价避免。

您现在将看到如何添加重置密码功能到井字策略应用:

  1. 更新登录表单,并在 Sign Up Here 链接之后直接添加一个名为Reset Password Here的新链接:
        <div class="col-md-12 control"> 
          <div style="border-top: 1px solid#888; padding-top:15px;
           font-size:85%"> 
           Don't have an account? 
           <a asp-action="Index"
            asp-controller="UserRegistration">Sign Up Here</a> 
        </div> 
        <div style="font-size: 85%;"> 
          Forgot your password? 
          <a asp-action="ForgotPassword">Reset Password Here</a></div> 
        </div> 
  1. 添加一个名为ResetPasswordEmailModel的新模型到Models文件夹:
        public class ResetPasswordEmailModel 
        { 
          public string DisplayName { get; set; } 
          public string Email { get; set; } 
          public string ActionUrl { get; set; } 
        }
  1. 更新AccountController,并添加一个新的方法ForgotPassword:
        [HttpGet] 
        public async Task<IActionResult> ForgotPassword() 
        { 
          return await Task.Run(() => 
          { 
            return View(); 
          }); 
        } 
  1. 添加一个名为ResetPasswordModel的新模型到Models文件夹:
        public class ResetPasswordModel 
        { 
          public string Token { get; set; } 
          public string UserName { get; set; } 
          public string Password { get; set; } 
          public string ConfirmPassword { get; set; } 
        } 
  1. 添加一个名为ForgotPassword的新视图到Views/Account文件夹:
        @model TicTacToe.Models.ResetPasswordModel 
        @{ 
          ViewData["Title"] = "GameInvitationConfirmation"; 
          Layout = "~/Views/Shared/_Layout.cshtml"; 
        } 
        <div class="form-gap"></div> 
        <div class="container"> 
          <div class="row"> 
            <div class="col-md-4 col-md-offset-4"> 
              <div class="panel panel-default"> 
                <div class="panel-body"> 
                  <div class="text-center"> 
                    <h3><i class="fa fa-lock fa-4x"></i></h3> 
                    <h2 class="text-center">Forgot Password?</h2> 
                    <p>You can reset your password here.</p> 
                    <div class="panel-body"> 
                      <form id="register-form" role="form"
                       autocomplete="off" class="form"
                       method="post" asp-controller="Account"
                       asp-action="SendResetPassword"> 
                        <div class="form-group"> 
                          <div class="input-group"> 
                            <span class="input-group-addon"><i
                             class="glyphicon glyphicon-envelope
                             color-blue"></i></span> 
                            <input id="email" name="UserName" 
                             placeholder="email address" 
                             class="form-control" type="email"> 
                          </div> 
                        </div> 
                        <div class="form-group"> 
                          <input name="recover-submit"
                           class="btn btn-lg btn-primary btn-block" 
                           value="Reset Password" type="submit"> 
                        </div> 
                        <input type="hidden" class="hide" 
                         name="token" id="token" value=""> 
                      </form> 

                    </div> 
                  </div> 
                </div> 
              </div> 
            </div> 
          </div> 
        </div> 
  1. 更新UserService和用户服务接口,并添加一个新方法GetResetPasswordCode:
        public async Task<string> GetResetPasswordCode(UserModel user) 
        { 
          return await _userManager.GeneratePasswordResetTokenAsync(user); 
        } 
  1. View/EmailTemplates文件夹中添加一个名为ResetPasswordEmail的新视图:
        @model TicTacToe.Models.ResetPasswordEmailModel 
        @{ 
          ViewData["Title"] = "View"; 
          Layout = "_LayoutEmail"; 
        } 
        <h1>Welcome @Model.DisplayName</h1> 
        You have requested a password reset, please click <a 
         href="@Model.ActionUrl">here</a> to continue. 
  1. 更新AccountController,并添加一个新的方法SendResetPassword:
        [HttpPost] 
        public async Task<IActionResult> SendResetPassword(
         string UserName) 
        { 
          var user = await _userService.GetUserByEmail(UserName); 
          var urlAction = new UrlActionContext 
          { 
            Action = "ResetPassword", 
            Controller = "Account", 
            Values = new { email = UserName,
             code = await _userService.GetResetPasswordCode(user) }, 
            Protocol = Request.Scheme, 
            Host = Request.Host.ToString() 
          }; 

          var resetPasswordEmailModel = new ResetPasswordEmailModel 
          { 
            DisplayName = $"{user.FirstName} {user.LastName}", 
            Email = UserName, 
            ActionUrl = Url.Action(urlAction) 
          }; 

          var emailRenderService = 
            HttpContext.RequestServices.GetService
             <IEmailTemplateRenderService>(); 
          var emailService =
            HttpContext.RequestServices.GetService<IEmailService>(); 
          var message =
            await emailRenderService.RenderTemplate(
             "EmailTemplates/ResetPasswordEmail",
              resetPasswordEmailModel, 
          Request.Host.ToString()); 

          try 
          { 
            emailService.SendEmail(UserName,
             "Tic-Tac-Toe Reset Password", message).Wait(); 
          } 
          catch 
          { 

          } 

          return View("ConfirmResetPasswordRequest",
           resetPasswordEmailModel); 
        }
  1. 添加一个名为ConfirmResetPasswordRequest的新视图到Views/Account文件夹:
        @model TicTacToe.Models.ResetPasswordEmailModel 
        @{ 
          ViewData["Title"] = "ConfirmResetPasswordRequest"; 
          Layout = "~/Views/Shared/_Layout.cshtml"; 
        } 
        @section Desktop{<h2>@Localizer["DesktopTitle"]</h2>} 
        @section Mobile {<h2>@Localizer["MobileTitle"]</h2>} 
        <h1>@Localizer["You have requested to reset your password,
         an email has been sent to {0}, please click on the provided 
         link to continue.", Model.Email]</h1> 
  1. 更新AccountController,并添加一个新的方法ResetPassword:
        public async Task<IActionResult> ResetPassword(string email,
         string code) 
        { 
          var user = await _userService.GetUserByEmail(email); 
          ViewBag.Code = code; 
          return View(new ResetPasswordModel { Token = code,
           UserName = email }); 
        } 
  1. Views/Account文件夹中添加一个名为SendResetPassword的新视图:
        @model TicTacToe.Models.ResetPasswordEmailModel 
        @{ 
          ViewData["Title"] = "SendResetPassword"; 
          Layout = "~/Views/Shared/_Layout.cshtml"; 
        } 
        @section Desktop{<h2>@Localizer["DesktopTitle"]</h2>} 
        @section Mobile {<h2>@Localizer["MobileTitle"]</h2>} 
        <h1>@Localizer["You have requested a password reset, an email
         has been sent to {0}, please click on the link to continue.",
         Model.Email]</h1> 
  1. 添加一个名为ResetPassword的新视图到Views/Account文件夹:
        @model TicTacToe.Models.ResetPasswordModel 
        @{ 
          ViewData["Title"] = "ResetPassword"; 
          Layout = "~/Views/Shared/_Layout.cshtml"; 
        } 
        <div class="container"> 
          <div id="loginbox" style="margin-top:50px;" class="mainbox 
           col-md-6 col-md-offset-3 col-sm-8 col-sm-offset-2"> 
            <div class="panel panel-info"> 
              <div class="panel-heading"> 
                <div class="panel-title">Reset your Password</div> 
              </div> 
              <div style="padding-top:30px" class="panel-body"> 
                <div class="text-center"> 
                  <form asp-controller="Account" 
                   asp-action="ResetPassword" method="post"> 
                    <input type="hidden" asp-for="Token" /> 
                    <div asp-validation-summary="All"></div> 
                    <div style="margin-bottom: 25px" class="input-group"> 
                      <span class="input-group-addon"><i 
                       class="glyphicon glyphicon-envelope 
                       color-blue"></i></span> 
                      <input id="email" asp-for="UserName" 
                       placeholder="email address" 
                       class="form-control" type="email"> 
                    </div> 
                    <div style="margin-bottom: 25px" class="input-group"> 
                      <span class="input-group-addon"><i 
                       class="glyphicon glyphicon-lock 
                       color-blue"></i></span> 
                      <input id="password" asp-for="Password" 
                       placeholder="Password" 
                       class="form-control" type="password"> 
                    </div> 
                    <div style="margin-bottom: 25px" class="input-group"> 
                      <span class="input-group-addon"><i 
                       class="glyphicon glyphicon-lock 
                       color-blue"></i></span> 
                      <input id="confirmpassword" 
                       asp-for="ConfirmPassword" 
                       placeholder="Confirm your Password" 
                       class="form-control" type="password"> 
                    </div> 
                    <div style="margin-bottom: 25px" class="input-group"> 
                      <input name="submit" 
                       class="btn btn-lg btn-primary btn-block" 
                       value="Reset Password" type="submit"> 
                    </div> 
                  </form> 
                </div> 
              </div> 
            </div> 
          </div> 
        </div>
  1. 更新UserService和用户服务接口,并添加一个名为ResetPassword的新方法:
        public async Task<IdentityResult> ResetPassword(
         string userName, string password, string token) 
        { 
          var start = DateTime.Now; 
          _logger.LogTrace($"Reset user password {userName}"); 

          var stopwatch = new Stopwatch(); 
          stopwatch.Start(); 

          try 
          { 
            var user = await _userManager.FindByNameAsync(userName); 
            var result = await _userManager.ResetPasswordAsync(user,
             token, password); 
            return result; 
          } 
          catch (Exception ex) 
          { 
            _logger.LogError($"Cannot reset user password
             {userName} - {ex}"); 
            throw ex; 
          } 
          finally 
          { 
            stopwatch.Stop(); 
            _logger.LogTrace($"Reset user password {userName}
             finished in {stopwatch.Elapsed}"); 
          } 
        } 
  1. 更新AccountController,并添加一个新的方法ResetPassword:
        [HttpPost] 
        public async Task<IActionResult> ResetPassword(
         ResetPasswordModel reset) 
        { 
          if (ModelState.IsValid) 
          { 
            var result =
              await _userService.ResetPassword(reset.UserName,
               reset.Password, reset.Token); 

            if (result.Succeeded) 
              return RedirectToAction("Login"); 
            else 
              ModelState.AddModelError("", "Cannot reset your password"); 
          } 
          return View(); 
        } 
  1. 启动应用,进入登录页面,点击重置密码在这里链接:

  1. 输入一个现有的用户电子邮件忘记密码? 页面; 这将向用户发送一封电子邮件:

  1. 打开密码重置邮箱,点击提供的链接:

  1. 在“重置密码”界面,输入新密码,单击“重置密码”。 你应该自动重定向到登录页面,所以登录与新密码:

实现授权

在本章的第一部分中,您了解了如何处理用户身份验证以及如何处理用户登录。 在下一部分中,您将看到如何管理用户访问,这将允许您微调谁有权访问什么。

最简单的授权方法是使用[Authorize]元装饰器,它完全禁用匿名访问。 在这种情况下,用户需要登录才能访问受限制的资源。

让我们看看如何在Tic-Tac-Toe应用中实现它:

  1. HomeController中添加一个名为SecuredPage的新方法,并通过添加[Authorize]装饰器来删除对它的匿名访问:
        [Authorize] 
        public async Task<IActionResult> SecuredPage() 
        { 
          return await Task.Run(() => 
          { 
            ViewBag.SecureWord = "Secured Page"; 
            return View("SecuredPage"); 
          }); 
        }  
  1. 添加一个名为SecuredPage的新视图到Views/Home文件夹:
        @{ 
          ViewData["Title"] = "Secured Page"; 
        } 
        @section Desktop {<h2>@Localizer["DesktopTitle"]</h2>} 
        @section Mobile {<h2>@Localizer["MobileTitle"]</h2>} 
        <div class="row"> 
          <div class="col-lg-12"> 
            <h2>Tic-Tac-Toe @ViewBag.SecureWord</h2> 
          </div> 
        </div> 
  1. 尝试通过手动输入 URLhttp://<host>/Home/SecuredPage访问安全页面,但未登录; 您将自动重定向到登录页面:

  1. 输入有效的用户凭证并登录; 你应该会自动重定向到安全页面,现在就可以看到它:

另一种比较流行的方法是使用基于角色的安全性,它提供了一些更高级的特性。 这是保护您的 ASP 的推荐方法之一.NET Core 2.0 web 应用。

下面的例子解释了如何使用它:

  1. Models文件夹中添加一个名为UserRoleModel的新类,并使其继承IdentityUserRole<long>; 它将被内置的 ASP.NET Core 2.0 身份验证特性:
        public class UserRoleModel : IdentityUserRole<Guid> 
        { 
          [Key] 
          public long Id { get; set; } 
        } 
  1. 更新游戏数据库上下文中的OnModelCreating方法:
        protected override void OnModelCreating(ModelBuilder modelBuilder) 
        { 
          ... 
          modelBuilder.Entity<IdentityUserRole<Guid>>() 
           .ToTable("UserRoleModel") 
           .HasKey(x => new { x.UserId, x.RoleId }); 
        } 
  1. 打开 NuGet 包管理控制台,执行Add-Migration IdentityDb2命令,然后执行Update-Database命令。
  2. 更新UserService,并修改构造器来创建两个名为PlayerAdministrator的角色,如果它们还不存在:
        public UserService(RoleManager<RoleModel> roleManager,
         ApplicationUserManager userManager, ILogger<UserService> 
         logger, SignInManager<UserModel> signInManager) 
        { 
          ... 
          if (!roleManager.RoleExistsAsync("Player").Result) 
            roleManager.CreateAsync(new RoleModel {
            Name = "Player" }).Wait(); 

          if (!roleManager.RoleExistsAsync("Administrator").Result)
            roleManager.CreateAsync(new RoleModel { 
            Name = "Administrator" }).Wait(); 
        }
  1. 更新UserService中的RegisterUser方法,并在用户注册时将用户添加到Player角色或Administrator角色:
        ... 
        try 
        { 
          userModel.UserName = userModel.Email; 
          var result = await _userManager.CreateAsync(userModel,
           userModel.Password); 
          if (result == IdentityResult.Success) 
          { 
            if(userModel.FirstName == "Jason") 
              await _userManager.AddToRoleAsync(userModel,
               "Administrator"); 
            else 
              await _userManager.AddToRoleAsync(userModel, "Player"); 
          } 

          return result == IdentityResult.Success; 
        } 
        ... 

Note that in the example, the code to identify whether a user has the administrator role is intentionally very basic. You should implement something more sophisticated in your applications.

  1. 启动应用并注册一个新用户,在 SQL Server Object Explorer 中打开RoleModel表,并分析其内容:

  1. 在 SQL Server Object Explorer 中打开UserRoleModel表并分析其内容:

  1. 更新UserService中的SignInUser方法以映射角色与索赔:
        ... 
        identity.AddClaim(new Claim("Score", user.Score.ToString())); 
        var roles = await _userManager.GetRolesAsync(user); 
        identity.AddClaims(roles?.Select(r => new
         Claim(ClaimTypes.Role, r))); 

        await httpContext.SignInAsync(
         CookieAuthenticationDefaults.AuthenticationScheme, 
          new ClaimsPrincipal(identity),
          new AuthenticationProperties { IsPersistent = false }); 
        ... 
  1. 更新HomeController中的SecuredPage方法,并使用管理员角色来保护访问,并替换Authorize装饰器:
        [Authorize(Roles = "Administrator")] 
  1. 启动应用。 如果您试图在未登录的情况下访问http://<host>/Home/SecuredPage,您将被重定向到登录页面。 以具有玩家角色的用户登录,您将被重定向到访问拒绝页面(不存在,因此出现404错误),因为该用户不具有管理员角色:

  1. 注销,然后作为具有管理员角色的用户登录; 你现在应该看到安全页面,因为用户有必要的角色:

在下面的示例中,您将看到如何作为已注册用户自动登录,以及如何激活基于索赔和基于策略的身份验证:

  1. 更新SignInUser方法,在UserService中添加一个名为SignIn的新方法:
        public async Task<SignInResult> SignInUser(LoginModel 
         loginModel, HttpContext httpContext) 
        { 
          var start = DateTime.Now; 
          _logger.LogTrace($"Signin user {loginModel.UserName}"); 
          var stopwatch = new Stopwatch(); 
          stopwatch.Start(); 

          try 
          { 
            var user =
              await _userManager.FindByNameAsync(loginModel.UserName); 
            var isValid =
              await _signInManager.CheckPasswordSignInAsync(user,
               loginModel.Password, true); 

            if (!isValid.Succeeded) 
            { 
              return SignInResult.Failed; 
            } 

            if (!await _userManager.IsEmailConfirmedAsync(user)) 
            { 
              return SignInResult.NotAllowed; 
            } 

            if (await _userManager.GetTwoFactorEnabledAsync(user)) 
            { 
              return SignInResult.TwoFactorRequired; 
            } 

            await SignIn(httpContext, user); 

            return isValid; 
          } 
          catch (Exception ex) 
          { 
            _logger.LogError($"Ca not sigin user 
             {loginModel.UserName} - {ex}"); 
            throw ex; 
          } 
          finally 
          { 
            stopwatch.Stop(); 
            _logger.LogTrace($"Sigin user {loginModel.UserName} 
             finished in {stopwatch.Elapsed}"); 
          } 
        } 

        private async Task SignIn(HttpContext httpContext, UserModel user) 
        { 
          var identity = new ClaimsIdentity(
            CookieAuthenticationDefaults.AuthenticationScheme); 
          identity.AddClaim(new Claim(ClaimTypes.Name, user.UserName)); 
          identity.AddClaim(new Claim(ClaimTypes.GivenName,
            user.FirstName)); 
          identity.AddClaim(new Claim(ClaimTypes.Surname,
            user.LastName)); 
          identity.AddClaim(new Claim("displayName",
            $"{user.FirstName} {user.LastName}")); 

          if (!string.IsNullOrEmpty(user.PhoneNumber)) 
          { 
            identity.AddClaim(new Claim(ClaimTypes.HomePhone,
             user.PhoneNumber)); 
          } 
          identity.AddClaim(new Claim("Score", user.Score.ToString())); 

          var roles = await _userManager.GetRolesAsync(user); 
          identity.AddClaims(roles?.Select(r => 
           new Claim(ClaimTypes.Role, r))); 

          if (user.FirstName == "Jason") 
          identity.AddClaim(new Claim("AccessLevel", "Administrator")); 

          await httpContext.SignInAsync(
           CookieAuthenticationDefaults.AuthenticationScheme,
           new ClaimsPrincipal(identity),
           new AuthenticationProperties { IsPersistent = false }); 
        } 

Note that, in the example, the code to identify whether a user has administrator privileges is intentionally very basic. You should implement something more sophisticated in your applications.

  1. 更新UserService中的RegisterUser方法,添加一个新参数,注册用户后自动签到,重新提取用户服务接口:
        public async Task<bool> RegisterUser(UserModel userModel,
         bool isOnline = false) 
        { 
          ... 
          if (result == IdentityResult.Success) 
          { 
            ... 
            if (isOnline) 
            { 
              HttpContext httpContext =
                new HttpContextAccessor().HttpContext; 
              await Signin(httpContext, userModel); 
            }     
          } 
          ... 
        }
  1. 更新UserRegistrationController中的Index方法,自动注册新注册的用户:
        ... 
        await _userService.RegisterUser(userModel, true); 
        ... 
  1. 更新GameInvitationController中的ConfirmGameInvitation方法,自动注册被邀请用户:
        ... 
        await _userService.RegisterUser(new UserModel 
        { 
          Email = gameInvitation.EmailTo, 
          EmailConfirmationDate = DateTime.Now, 
          EmailConfirmed = true, 
          FirstName = "", 
          LastName = "", 
          Password = "Azerty123!", 
          UserName = gameInvitation.EmailTo 
        }, true); 
        ... 
  1. Startup类中添加一个名为AdministratorAccessLevelPolicy的新策略,就在 MVC 中间件配置之后:
        services.AddAuthorization(options => 
        { 
          options.AddPolicy("AdministratorAccessLevelPolicy",
           policy => policy.RequireClaim("AccessLevel",
           "Administrator")); 
        }); 
  1. 更新HomeController中的SecuredPage方法,使用Policy代替Role以确保访问安全,并替换Authorize装饰器:
        [Authorize(Policy = "AdministratorAccessLevelPolicy")]

Note that it can be required to limit access to only one specific middleware, since several kinds of Authentication Middleware can be used with ASP.NET Core 2.0 (Cookie, Bearer, and more) at the same time.

For this case, the Authorize decorator you have seen before allows you to define which middleware can authenticate a user.

Here is an example to allow Cookie and Bearer:

        [Authorize(AuthenticationSchemes = "Cookie,Bearer",
         Policy = "AdministratorAccessLevelPolicy")] 
  1. 启动应用,注册具有Administrator访问级别的新用户,登录并访问http://<host>/Home/SecuredPage。 一切都应该像以前一样工作。

Note that you might need to clear your cookies and log in again to create a new authentication token with the required claims.

  1. 尝试以没有所需访问级别的用户访问安全页面; 和以前一样,您应该被重定向到http://<host>/Account/AccessDenied?ReturnUrl=%2FHome%2FSecuredPage:

  1. 注销,然后作为具有Administrator角色的用户登录; 现在您应该看到安全页面,因为用户具有必要的角色。

总结

在本章中,您已经学习了如何保护 ASP.NET Core 2.0 应用,包括管理应用用户的身份验证和授权。

您已经通过 Facebook 向示例应用添加了基本表单身份验证和更高级的外部提供者身份验证。 这将为您提供一些关于如何在自己的应用中处理这些重要主题的好想法。

此外,您还学习了如何添加标准的重置密码机制,因为用户总是会忘记他们的密码,您需要尽可能安全地响应这种类型的请求。

我们甚至还讨论了双因素身份验证,它可以为关键应用提供更高的安全级别。

最后,您了解了如何以多种方式(基本、角色、策略)处理授权,因此您可以决定哪种方法最适合您的特定用例。

在下一章中,我们将讨论在托管和部署您的 ASP 时您将拥有的不同选择.NET Core 2.0 web 应用。