上一章点击https://blog.csdn.net/zpczmc/article/details/86772554 查看
在上一章中,我添加了SportsStore应用程序的管理功能,如果我按原样部署应用程序,那么任何人都无法修改产品目录。他们需要知道的是,管理功能在/ Admin / Index和/Order/List Urls可以使用。 在本章中,我将向您展示如何通过密码保护它们防止随机人员使用管理功能。 完成安全措施功能,我会告诉你如何准备和部署SportsStore应用程序到生产环境中。
ASP.NET CORE Identity系统提供身份验证和授权功能,这个功能优雅的集成在ASP.NET CORE架构和MVC应用程序中,在接下来的章节中,我会建立一个安全规则,允许用户名为Admin的用户通过身份验证并使用应用的管理功能,Asp.net core Identity提供了非常多的特性可以验证用户并授权用户访问应用数据,你可以在第27,28,29章了解的更多,怎么创建用户管理用户账户,怎么用角色和安全策略,怎么使用第三方认证,比如 微软,谷歌和脸书,推特的账号登录你的应用,在这个章节中,我们的目标是增加一个能阻止用户访问程序敏感部分的功能,使你对于MVC的验证和授权有一个初步的认识和了解.
Asp.Net Identity是支持灵活配置并支持拓展的,并支持多种用户存储数据的选项,我们采用最常规的,即使用Sql Server存储用户数据,使用Entity Framework Core访问服务器.
创建一个数据库上下文类来充当数据库和身份验证模型类之间沟通的桥梁,如清单 Listing 12-1 所示
Listing 12-1. The Contents of the AppIdentityDbContext.cs File in the Models Folder
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace SportsStore.Models {
public class AppIdentityDbContext:IdentityDbContext {
public AppIdentityDbContext(DbContextOptions options) : base(options) {
}
}
}
AppIdentityDBContext类继承自IdentityDbContext,它为实体框架提供特殊身份特性,类型参数选择IdentityUser,这个类用于表达呈现用户,在第28章,我会演示如何利用此类拓展和增加用户信息
下一步是为数据库定义连接字符串,在清单 Listing 12-2中,你可以看到我在appsettings.json中的修改,就像在第8章定义连接数据字符串一样,
Listing 12-2. Defining a Connection String in the appsettings.json File
{
"Data": {
"SportStoreProducts": {
"ConnectionString": "\" Server=(localdb)\\\\MSSQLLocalDB;Database=SportsStore;Trusted_",
"Connection=True;MultipleActiveResultSets=true\""
},
"SportStoreIdentity": {
"ConnectionString": " Server=(localdb)\\MSSQLLocalDB;Database=Identity;Trusted_
Connection=True;MultipleActiveResultSets=true"
}
}
}
请记住,连接字符串必须在appsettings中的单个连续行中定义。由于书页的固定宽度,json文件显示在列表中的多行中。该清单中的添加定义了一个名为SportsStoreIdentity的连接字符串,用于指定LocalDB数据库名为Identity。
与其他ASP.NET Core功能一样,Identity在Start类中配置。 清单12-3显示了添加内容:
Listing 12-3. Configuring Identity in the Startup.cs File
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using SportsStore.Models;
namespace SportsStore {
public class Startup {
private IConfiguration Configuration;
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services) {
services.AddMemoryCache();
services.AddSession();
services.AddDbContext(options =>
options.UseSqlServer(
Configuration["Data:SportStoreProducts:ConnectionString"]));
//--------------------------------------------------------------------------
services.AddDbContext(options =>
options.UseSqlServer(
Configuration["Data:SportStoreIdentity:ConnectionString"]));
services.AddIdentity()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
//--------------------------------------------------------------------------
services.AddTransient();
services.AddTransient();
services.AddScoped(sp => SessionCart.GetCart(sp));
services.AddSingleton();
services.AddMvc();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
app.UseDeveloperExceptionPage();
app.UseSession();
//--------------------------------------------------------------------------
app.UseAuthentication();
//--------------------------------------------------------------------------
app.UseStatusCodePages();
app.UseStaticFiles();
app.UseMvc(routes => {
routes.MapRoute(
name: null,
template: "{category}/Page{productPage:int}",
defaults: new { controller = "Product", action = "List" }
);
routes.MapRoute(
name: null,
template: "Page{productPage:int}",
defaults: new { controller = "Product", action = "List", productPage = 1 }
);
routes.MapRoute(
name: null,
template: "{category}",
defaults: new { controller = "Product", action = "List", productPage = 1 }
);
routes.MapRoute(
name: null,
template: "",
defaults: new { controller = "Product", action = "List", productPage = 1 }
);
routes.MapRoute(name: null, template: "{controller}/{action}/{id?}");
});
SeedData.EnsurePopulated(app);
}
}
}
在ConfigureServices方法中,我扩展了Entity Framework Core配置以注册上下文类并,使用AddIdentity方法使用内置类来设置Identity服务代表用户和角色。 在Configure方法中,我设置用UseAuthentication方法拦截请求和响应以实现安全策略。
基本配置已经完成,现在需用使用EF的迁移功能来定义数据库及表结构,打开新的命令提示符或PowerShell窗口,定位到SportsStore文件夹下StartUp.cs所在的目录,输入以下命令
dotnet ef migrations add Initial --context AppIdentityDbContext
与以前的数据库命令的重要区别在于我使用了-context参数指定与我要使用的数据库关联的上下文类的名称,即AppIdentityDbContext。 当您在应用程序中有多个数据库时,确保这一点非常重要,您正在使用正确的上下文类。一旦Entity Framework Core生成了初始迁移,请运行以下命令进行创建数据库并运行迁移命令:
dotnet ef database update --context APPIdentityDBContext
结果是EF为我们新建了一个名为Identity的新LocalDB数据库.
我将通过在应用程序启动时为数据库设置种子来显式创建Admin用户。 我新建了一个名为IdentitySeedData.cs的类文件到Models文件夹,并定义了清单Listing 12-4中所示的静态类。
Listing 12-4. The Contents of the IdentitySeedData.cs File in the Models Folder
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
namespace SportsStore.Models {
public static class IdentitySeedData {
private const string adminUser = "Admin";
private const string adminPassword = "Secret123$";
public static async void EnsurePopulated(IApplicationBuilder app) {
UserManager userManager = app.ApplicationServices
.GetRequiredService>();
IdentityUser user = await userManager.FindByIdAsync(adminUser);
if (user == null) {
user = new IdentityUser("Admin");
await userManager.CreateAsync(user, adminPassword);
}
}
}
}
此代码使用UserManager
注意:通常需要对管理员帐户的详细信息进行硬编码,以便您可以登录应用程序就可以管理它。 这也要求你必须记住
您创建的帐户的密码(否则你也无法管理)。 有关如何更改密码的详细信息,请参阅第28章
为了确保在应用程序启动时播种Identity数据库,我添加了清单12-5中的方法.
Listing 12-5. Seeding the Identity Database in the Startup.cs File in the SportsStore Folder
......
SeedData.EnsurePopulated(app);
//-----------------------------------------------------------
IdentitySeedData.EnsurePopulated(app);
//-----------------------------------------------------------
}
}
}
......
现在我已经配置了ASP.NET Core身份验证功能,我可以将授权策略应用于我想要保护的应用程序的一部分。 我将使用最基本的授权策略,即允许访问任何经过身份验证的用户。 虽然这在实际应用中也是一个有用的策略,当然也可以创建更细粒度授权的选项(如第28,29,30章),但由于SportsStore应用程序只有一个用户,区分匿名和经过身份验证的请求就足够了,Authorize属性用于限制对操作方法的访问,在清单Listing 12-6中,您可以看到我已使用该属性来保护对Order控制器中管理操作的访问。
Listing 12-6. Restricting Access in the OrderController.cs File
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using System.Linq;
namespace SportsStore.Controllers
{
public class OrderController : Controller
{
private IOrderRepository repository;
private Cart cart;
public OrderController(IOrderRepository repoService,Cart cartService)
{
repository = repoService;
cart = cartService;
}
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
[Authorize]
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
public ViewResult List() => View(repository.Orders.Where(o => !o.Shipped));
[HttpPost]
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
[Authorize]
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
public IActionResult MarkShipped(int orderID)
{
Order order = repository.Orders.FirstOrDefault(o => o.OrderID == orderID);
if (order != null)
{
order.Shipped = true;
repository.SaveOrder(order);
}
return RedirectToAction(nameof(List));
}
public ViewResult Checkout() => View(new Order());
[HttpPost]
public IActionResult Checkout(Order order)
{
if (cart.Lines.Count() == 0)
{
ModelState.AddModelError("", "Sorry,your cart is empty!");
}
if (ModelState.IsValid)
{
order.Lines = cart.Lines.ToArray();
repository.SaveOrder(order);
return RedirectToAction(nameof(Completed));
}
else
{
return View(order);
}
}
public ViewResult Completed()
{
cart.Clear();
return View();
}
}
}
我并不想阻止未通过认证的用户访问控制器的其他方法动作,所以把只在List和MarkShipped方法上加了[Authorize]修饰,我在Admin控制器的控制器类上用[Authorize]修饰,这样我就能保护控制器类所有的方法都需要认证用户访问,如清单 Listing 12-7 所示
Listing 12-7. Restricting Access in the AdminController.cs File
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;
using System.Linq;
namespace SportsStore.Controllers {
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓修改或增加的代码↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
[Authorize]
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑修改或增加的代码↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
public class AdminController : Controller {
private IProductRepository repository;
public AdminController(IProductRepository repo) {
repository = repo;
}
public ViewResult Index() => View(repository.Products);
public ViewResult Edit(int productID) =>
View(repository.Products.FirstOrDefault(p => p.ProductID == productID));
[HttpPost]
public IActionResult Edit(Product product) {
if (ModelState.IsValid) {
repository.SaveProduct(product);
TempData["message"] = $"{product.Name} has been saved";
return RedirectToAction("Index");
} else {
//there is something wrong with the data values
return View(product);
}
}
public ViewResult Create() => View("Edit", new Product());
[HttpPost]
public IActionResult Delete(int productID) {
Product deleteProduct = repository.DeleteProduct(productID);
if (deleteProduct != null) {
TempData["message"] = $"{deleteProduct.Name} was deleted";
}
return RedirectToAction("Index");
}
}
}
当一个未经过身份验证的用户访问需要身份验证通过者才能访问的页面,mvc会把它转向到 /Account/Login 的网址,应用程序使用此页面提示用户输入凭据,所以我在 Models/ViewModels文件夹中定义了LoginModel.cs的视图模型以便使用,如清单Listing 12-8 所示
Listing 12-8. The Contents of the LoginModel.cs File in the Models/ViewModels Folder
using System.ComponentModel.DataAnnotations;
namespace SportsStore.Models.ViewModels {
public class LoginModel {
[Required]
public string Name { get; set; }
[Required]
[UIHint("password")]
public string Password { get; set; }
public string Returnurl { get; set; } = "/";
}
}
Name和Password属性用Required修饰,这就能确保模型验证要求这两个属性必须提供值,Password属性同时也被UIHint修饰,这能保证当我在Razor视图用asp-for特性修饰Input元素时,会把inupt的type标记为password,这样屏幕上就不会直接显示出用户密码的明文,我会在第24章讲解UIHint特性,
下面我在Controllers文件夹增加了一个名为AccountController.cs的控制器文件,清单Listing 12-9 展示了文件的内容,这个控制器将相应对 /Account/Login 的网址请求.
Listing 12-9. The Contents of the AccountController.cs File in the Controllers Folder
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using SportsStore.Models.ViewModels;
namespace SportsStore.Controllers {
[Authorize]
public class AccountController : Controller {
private UserManager userManager;
private SignInManager signInManager;
public AccountController(UserManager userMgr, SignInManager signInMgr) {
userManager = userMgr;
signInManager = signInMgr;
}
[AllowAnonymous]
public ViewResult Login(string returnUrl) {
return View(new LoginModel {
Returnurl = returnUrl
});
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task Login(LoginModel loginModel) {
if (ModelState.IsValid) {
IdentityUser user = await userManager.FindByNameAsync(loginModel.Name);
if (user != null) {
await signInManager.SignOutAsync();
if((await signInManager.PasswordSignInAsync(user, loginModel.Password, false, false)).Succeeded) {
return Redirect(loginModel?.Returnurl ?? "/Admin/Index");
}
}
}
ModelState.AddModelError("", "Invalid name or password");
return View(loginModel);
}
public async Task Logout(string returnUrl = "/") {
await signInManager.SignOutAsync();
return Redirect(returnUrl);
}
}
}
当用户重定向到/Account/Login 的页面时,Login动作方法会调用视图生成登录页面,一旦用户通过验证就会重定向到用于原来期望访问的页面,
身份认证凭据通过Login方法的Post版本提交,它们通过构造函数注入的UserManager
注意:一般来说,用客户端验证是一个好办法,一方面能使用户得到即时反馈,另一方面也减轻了服务器的负担,但是你不能奢望并幻想在客户端进行身份验证,因为如果客户端能进行验证,意味着你必须把用户的有效凭据发送到客户端,(也就是你要信任客户端的操作,而实际上从安全的角度上看,我们不应该相信任何客户的输入),所以身份验证必须在服务器端进行
我们为Login提供一个视图,如清单Listing 12-10 所示
Listing 12-10. The Contents of the Login.cshtml File in the Views/Account Folder
@model LoginModel
@{
ViewBag.Title = "Login";
Layout = "_AdminLayout";
}
最后一步是在管理的母版页增加一个退出登录的按钮用于将登陆的用户注销,如清单 Listing 12-11 所示,这是一个测试应用简单又实用的做法,否则你需要清除浏览器的cookies来恢复用户未通过身份验证的状态.
Listing 12-11. Adding a Logout Button in the _AdminLayout.cshtml File
@ViewBag.Title
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓修改或增加的代码↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
@ViewBag.Title
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑修改或增加的代码↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
@if (TempData["message"] != null) {
@TempData["message"]
}
@RenderBody()
一切就绪,你可以启动应用导航到 /Admin/Index 网址测试效果,由于你是未通过验证的用户并且你要访问的是受保护的动作方法,所以浏览器会跳转到/Account/Login ,输入用户名 Admin 和密码 Secret123$ ,账户控制器将会把您输入的信息与之前通过种子写入数据库的信息进行比对,一致的话就会跳转到您要访问的网址,如图Figure 12-1 所示
Figure 12-1. The administration authentication/authorization process
SportsStore应用程序的所有特性和功能都已到位,因此是时候准备将应用程序部署到生产中。 ASP.NET Core MVC提供了许多应用程序托管选项,我在本章中使用的是Microsoft Azure平台,因为它来自微软,因为它提供免费帐户,这意味着即使您不想将Azure用于您自己的项目,也可以一直保留着SportsStore示例并随时关注它。
注意:此部分需要Azure帐户。 如果您没有的话,需要在http://azure.microsoft.com创建一个免费帐户。
第一步是创建生产环境中使用的数据库,虽然你可以在部署应用程序的过程中用代码直接生成数据库,但是前提是你要提供数据库连接字符串,这就需要先提供数据库
警告:Azure门户随着Microsoft添加新功能和修改现有功能而经常更改。在我写这些内容时,本节中的说明是准确的,但所需的步骤可能略有改变,当你读这篇文章的时候。 基本方法应该仍然是相同的,但数据字段的名称和确切的步骤顺序可能需要一些实验才能获得正确的结果。
用你的账户登录http://portal.azure.com手动创建数据库,等你登录成功后,选择Sql 数据库的类别然后点击添加按钮 创建数据库,
第一步是输入数据库的名字,单击“配置所需设置”链接,然后单击“创建新服务器”链接。输入一个新的服务器名称 - 在Azure中必须是唯一的 - 并选择一个数据库,输入了服务器名称sportsstorecore2db,输入管理员用户名和密码sportsstoreadmin和Secret123 $。由于服务器名称是惟一的,你将不得不使用另外的服务器名称,我建议您使用更强大的密码。选择数据库的位置;点击选择按钮以关闭选项,然后选择“创建”按钮以创建数据库本身。 Azure将需要几分钟执行创建过程,之后它将出现在SQL数据库资源类别中。
创建另一个SQL服务器,这次输入名称标识。您可以使用该数据库服务器而不是创造一个新的。结果是由两个Azure托管的SQL Server数据库,其详细信息如表12-1所示.(现在申请Azure sql数据库需要提供ViSA信用卡,由于我没有这种卡,所以就翻译原文,大家将就看吧)
Table 12-1. The Azure Databases for the SportsStore Application
我需要填充数据库,最简单的方法是打开Azure防火墙访问权限,以便我可以从我的开发机器运行Entity Framework Core命令。
选择SQL数据库资源类别中的任一数据库,单击工具按钮,然后单击“在Visual Studio中打开”链接。 现在单击“配置您的防火墙”链接,单击“添加客户端IP”按钮,然后单击“保存”。 这允许您当前的IP地址到达数据库服务器并执行配置命令。 (您可以通过单击“在Visual Studio中打开”来检查数据库架构按钮,它将打开Visual Studio并使用SQL Server对象资源管理器来检查数据库。)
稍后我需求获得新数据库的连接字符串,当你点击sql数据库中的 显示数据库连接字符串的链接后 Azure会提供这个字符串,它会针对不同的开发框架提供连接字符串,.net 应用需要的是ADO.NET 字符串,下面是Azure提供的字符串
Server=tcp:sportsstorecore2db.database.windows.net,1433;Initial Catalog=products;Persist
Security Info=False;User ID={your_username};Password={your_password};MultipleActiveResultS
ets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;
你将会得到一个与上面不完全相同的字符串,这取决于你你的配置,并且你要注意到两个占位符,分别是用户名和密码,这需要替换成你在创建数据库时提供给Azure的值,
在部署之前,我有一些基本的准备工作要做,第一个是要更改网站出现错误的显示方式,第二个是设置数据库的连接字符串
之前,我们用的错误页是对开发者比较友好的错误提示页面,但这些信息对于终端用户是不允许看到的,我已我在控制器类Controllers文件夹下增加了ErrorController.cs,如清单Listing 12-12 所示
Listing 12-12. The Contents of the ErrorController.cs File in the Controllers Folder
using Microsoft.AspNetCore.Mvc;
namespace SportsStore.Controllers {
public class ErrorController : Controller {
public ViewResult Error() => View();
}
}
这个控制器定义了一个Error动作方法去调用视图,我们创建视图如下Listing 12-13 所示
Listing 12-13. The Contents of the Error.cshtml File in the Views/Error Folder
@{
Layout = null;
}
Error
Error.
An error occurred while processing your request.
这种错误页面是最后的选择,最好尽量保持简单,不要依赖在共享视图,视图组件或其他丰富功能上。 在这种情况下,我已禁用共享布局并定义了一个简单的HTML文档,告诉用户存在错误,但是没有提供任何错误信息来说明错误是如何发生的。
下一步是创建一个文件,为应用程序提供其数据库连接字符串。 我添加了一个名为appsettings.production.json的新ASP.NET配置文件到SportsStore项目,并添加了清单Listing 12-14中所示的内容。
提示:解决方案资源管理器将此文件嵌套在文件列表中的appsettings.json中,如果要稍后再次编辑文件,你需要展开它
Listing 12-14. The Contents of the appsettings.production.json File
{
"Data": {
"SportStoreProducts": {
"ConnectionString": "Server=tcp:sportsstorecore2db.database.windows.net,1433;Initial Catalog=products;Persist Security Info=False;User ID={your_username};Password={your_password};MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
},
"SportStoreIdentity": {
"ConnectionString": "Server=tcp:sportsstorecore2db.database.windows.net,1433;Initial Catalog=identity;Persist Security Info=False;User ID={your_username};Password={your_password};MultipleActiveResultSets=True;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
}
}
}
此文件很难读取,因为连接字符串不能跨多行分割。 此文件复制了appsettings.json文件的连接字符串部分,但使用Azure
连接字符串。 (请记住替换用户名和密码占位符。)我也设置了MultipleActiveResultSets属性为True,允许多个并发查询并避免在执行应用程序数据的复杂LINQ查询时出现的常见错误。
注意:将用户名和密码插入连接时,请删除大括号字符串,您最终使用Password = MyPassword而不是Password = {MyPassword}。
现在我更改Startup类以便应用程序产生针对开发和生产做出不同行为。清单Listing 12-15显示了我所做的更改。
Listing 12-15. Configuring the Application in the Startup.cs File
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓修改或增加的代码↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseStatusCodePages();
} else {
app.UseExceptionHandler("/Error");
}
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑修改或增加的代码↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
app.UseStaticFiles();
app.UseSession();
app.UseAuthentication();
app.UseMvc(routes => {
routes.MapRoute(
name: null,
template: "{category}/Page{productPage:int}",
defaults: new { controller = "Product", action = "List" }
);
routes.MapRoute(
name: null,
template: "Page{productPage:int}",
defaults: new { controller = "Product", action = "List", productPage = 1 }
);
routes.MapRoute(
name: null,
template: "{category}",
defaults: new { controller = "Product", action = "List", productPage = 1 }
);
routes.MapRoute(
name: null,
template: "",
defaults: new { controller = "Product", action = "List", productPage = 1 }
);
routes.MapRoute(name: null, template: "{controller}/{action}/{id?}");
});
//↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓修改或增加的代码↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
//SeedData.EnsurePopulated(app);
//IdentitySeedData.EnsurePopulated(app);
//↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑修改或增加的代码↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
}
}
}
IHostingEnvironment接口用于应用程序正在运行时提供有关其中的环境的信息,例如开发或生产, 托管环境设置为
生产时,然后ASP.NET Core将加载appsettings.production.json文件及其内容以覆盖appsettings.json文件中的设置,这意味着Entity Framework Core将连接到Azure数据库而不是LocalDB。 有很多选项可用于不同的环境下定制配置应用,我将在第14章中解释。我还注释种子数据库生成,我在“管理”中说明了这些部分的作用,显然真正的生产环境不需要这部分。
待续2019年2月10日11:29:28