Category Archives: C#

ASP.NET Core in Action 31 Advanced configuration of ASP.NET Core

31 Advanced configuration of ASP.NET Core‌

This chapter covers

• Building custom middleware
• Using dependency injection (DI) services in IOptions configuration
• Replacing the built-in DI container with a third-party container

When you’re building apps with ASP.NET Core, most of your creativity and specialization go into the services and models that make up your business logic and the Razor Pages and APIs that expose them. Eventually, however, you’re likely to find that you can’t quite achieve a desired feature using the components that come out of the box. At that point, you may need to look to more complex uses of the built- in features.

This chapter shows some of the ways you can customize cross-cutting parts of your application, such as your DI container or your middleware pipeline. These approaches are particularly useful if you’re coming from a legacy application or are working on an existing project, and you want to continue to use the patterns and libraries you’re familiar with.

We’ll start by looking at the middleware pipeline. You saw how to build pipelines by piecing together existing

middleware in chapter 4, but in this chapter you’ll create your own custom middleware. You’ll explore the basic middleware constructs of the Map, Use, and Run methods and learn how to create standalone middleware classes.‌

You’ll use these to build middleware components that can add headers to all your responses as well as middleware that returns responses. Finally, you’ll learn how to turn your custom middleware into a simple endpoint, using endpoint routing.

In chapter 10 you learned about strongly typed configuration using the IOptions<T> pattern, and in section 31.2 you’ll learn how to take this further. You’ll learn how to use the OptionsBuilder<T> type to fluently build your IOptions<T> object with the builder pattern. You’ll also see how to use services from DI when configuring your IOptions objects—something that’s not possible using the methods you’ve seen so far.

We stick with DI in section 31.3, where I’ll show you how to replace the built-in DI container with a third-party alternative. The built-in container is fine for most small apps, but your ConfigureServices function can quickly get bloated as your app grows and you register more services.

I’ll show you how to integrate the third-party Lamar library into an existing app, so you can use extra features such as automatic service registration by convention.

The components and techniques shown in this chapter are more advanced than most features you’ve seen so far. You likely won’t need them in every ASP.NET Core project, but they’re good to have in your back pocket should the need arise!

31.1 Customizing your middleware pipeline‌

In this section you’ll learn how to create custom middleware. You’ll learn how to use the Map, Run, and Use extension methods to create simple middleware using lambda expressions. You’ll then see how to create equivalent middleware components using dedicated classes. You’ll also learn how to split the middleware pipeline into branches, and you’ll find out when this is useful.

The middleware pipeline is one of the building blocks of ASP.NET Core apps, so we covered it in depth in chapter 4. Every request passes through the middleware pipeline, and each middleware component in turn gets an opportunity to modify the request or to handle it and return a response.

ASP.NET Core includes middleware for handling common scenarios out of the box. You’ll find middleware for serving static files, handling errors, authentication, and many more tasks.

You’ll spend most of your time during development working with Razor Pages, minimal API endpoints, or web API controllers. These are exposed as the endpoints for most of your app’s business logic, and they call methods on your app’s various business services and models. However, you’ve also seen middleware like the Swagger middleware and the WelcomePageMiddleware that returns a response without using the endpoint routing system. The various improvements to the routing system in .NET 7 mean I rarely find the need to create “terminal” middleware like this, as endpoint routing is easy to work with and extensible.Nevertheless, it may occasionally be preferable to create small, custom, terminal middleware components like these.

At other times, you might have requirements that lie outside the remit of Razor Pages or minimal API endpoints. For example, you might want to ensure that all responses generated by your app include a specific header. This sort of cross-cutting concern is a perfect fit for custom middleware. You could add the custom middleware early in your middleware pipeline to ensure that every response from your app includes the required header, whether it comes from the static-file middleware, the error handling middleware, or a Razor Page.

In this section I show three ways to create custom middleware components, as well as how to create branches in your middleware pipeline where a request can flow down either one branch or another. By combining the methods demonstrated in this section, you’ll be able to create custom solutions to handle your specific requirements.

We start by creating a middleware component that returns the current time as plain text whenever the app receives a request. From there we’ll look at branching the pipeline, creating general-purpose middleware components, and encapsulating your middleware into standalone classes.

Finally, in section 31.1.5 you’ll see how to turn your custom middleware component into an endpoint and integrate it with the endpoint routing system.

31.1.1 Creating simple apps with the Run extension‌

As you’ve seen in previous chapters, you define the middleware pipeline for your app in Program.cs by adding middleware to a WebApplication object, typically using extension methods, as in this example:‌

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseExceptionHandler();
app.UseStaticFiles();
app.Run();

When your app receives a request, the request passes through each middleware component, each of which gets a chance to modify the request or handle it by generating a response. If a middleware component generates a response, it effectively short-circuits the pipeline; no subsequent middleware in the pipeline sees the request. The response passes back through the earlier middleware components on its way back to the browser.

You can use the Run extension method to build a simple middleware component that always generates a response. This extension takes a single lambda function that runs whenever a request reaches the component. The Run extension always generates a response, so no middleware placed after it ever executes. For that reason, you should always place the Run middleware last in a middleware pipeline.

TIP Remember that middleware components run in the order in which you add them to the pipeline. If a middleware component handles a request and generates a response, later middleware never sees the request.

The Run extension method provides access to the request in the form of the HttpContext object you saw in chapter 4. This contains all the details of the request in the Request property, such as the URL path, the headers, and the body of the request. It also contains a Response property you can use to return a response.‌‌‌

The following listing shows how you could build a simple middleware component that returns the current time. It uses the provided HttpContext context object and the Response property to set the Content-Type header of the response (not strictly necessary in this case, as text/plain is used if an alternative content type is not set) and writes the body of the response using WriteAsync(text).

Listing 31.1 Creating simple middleware using the Run extension

app.Run(async (HttpContext context) => ❶
{
context.Response.ContentType = "text/plain"; ❷
await context.Response.WriteAsync( ❸
DateTimeOffset.UtcNow.ToString()); ❸
});
app.UseStaticFiles(); ❹

❶ Uses the Run extension to create simple middleware that always returns a response
❷ You should set the content-type of the response you’re generating; text/plain is the default value.
❸ Returns the time as a string in the response. The 200 OK status code is used if not explicitly set.
❹ Any middleware added after the Run extension will never execute.

The Run extension is useful for two different things:

• Creating simple middleware that always generates a response

• Creating complex middleware that hijacks the whole request to build an additional framework on top of ASP.NET Core

Whether you’re using the Run extension to create basic endpoints or a complex extra framework layer, the middleware always generates some sort of response.

Therefore, you must always place it at the end of the pipeline, as no middleware placed after it will execute.

TIP Using the Run extension to unconditionally generate a response is rare these days. The endpoint routing system used by minimal APIs provides many extra niceties such as model binding, routing, integration with other middleware such as authentication and authorization, and so on.

There may be occasional situations where you want to unconditionally generate a response, but a more common scenario is where you want your middleware component to respond only to a specific URL path, such as the way the Swagger UI middleware responds only to the /swagger path. In the next section you’ll see how you can combine Run with the Map extension method to create branching middleware pipelines.

31.1.2 Branching middleware‌ pipelines with the Map extension

So far when discussing the middleware pipeline, we’ve always considered it to be a single pipeline of sequential components. Each request passes through every middleware component until one component generates a response; then the response passes back through the previous middleware.

The Map extension method lets you change that simple pipeline into a branching structure. Each branch of the pipeline is independent; a request passes through one branch or the other but not both, as shown in figure 31.1. The Map extension method looks at the path of the request’s URL. If the path starts with the required pattern, the request travels down the branch of the pipeline; otherwise, it remains on the main trunk. This lets you have completely different behavior in different branches of your middleware pipeline.

alt text

Figure 31.1 A sequential middleware pipeline compared with a branching pipeline created with the Map extension. In branching middleware, requests pass through only one of the branches at most. Middleware on the other branch never see the request and aren’t executed.

NOTE The URL-matching used by Map is conceptually similar to the routing you’ve seen throughout the book, but it is much more basic, with many limitations. For example, it uses a simple string-prefix match, and you can’t use route parameters. Generally, you should favor using endpoint routing instead of branching using Map. A similar extension, MapWhen, allows matching based on anything in HttpContext, such as headers or query string parameters.

For example, imagine you want to add a simple health-check endpoint to your existing app. This endpoint is a simple URL you can call that indicates whether your app is running correctly. You could easily create a health-check middleware using the Run extension, as you saw in listing 31.1, but then that’s all your app can do. You want the health-check to respond only to a specific URL, /ping. Your Razor Pages should handle all other requests as normal.

TIP The health-check scenario is a simple example for demonstrating the Map method, but ASP.NET Core includes built-in support for health-check endpoints, which integrate into the endpoint routing system. You should use these instead of creating your own. You can learn more about creating health checks in Microsoft’s “Health checks in ASP.NET Core” documentation: http://mng.bz/nMA2.

One solution would be to create a branch using the Map extension method and to place the health-check middleware on that branch, as shown in figure 31.1. Only those requests that match the Map pattern /ping will execute the branch; all other requests are handled by the standard routing middleware and Razor Pages on the main trunk instead, as shown in the following listing.

Listing 31.2 Using the Map extension to create branching middleware pipelines

app.UseStatusCodePages(); ❶
app.Map("/ping", (IApplicationBuilder branch) => ❷
{
branch.UseExceptionHandler(); ❸
branch.Run(async (HttpContext context) => ❹
{ ❹
context.Response.ContentType = "text/plain"; ❹
await context.Response.WriteAsync("pong"); ❹
}); ❹
});
app.UseStaticFiles(); ❺
app.UseRouting(); ❺
app.MapRazorPages(); ❺
app.Run();

❶ Every request passes through this middleware.
❷ The Map extension method branches if a request starts with /ping.
❸ This middleware runs only for requests matching the /ping branch.
❹ The Run extension always returns a response, but only on the /ping branch.
❺ The rest of the middleware pipeline run for requests that don’t match the /ping branch.

The Map middleware creates a completely new IApplicationBuilder (called branch in the listing), which you can customize as you would your main app pipeline. Middleware added to the branch builder are added only to the branch pipeline, not the main trunk pipeline.‌

TIP The WebApplication object you typically add middleware to implements the IApplicationBuilder interface. Most extension methods for adding middleware use the IApplicationBuilder interface, so you can use‌ the extension methods in branches as well as your main middleware pipeline.

In this example, you add the Run middleware to the branch, so it executes only for requests that start with /ping, such as /ping, /ping/go, and /ping?id=123. Any requests that don’t start with /ping are ignored by the Map extension. Those requests stay on the main trunk pipeline and execute the next middleware in the pipeline after Map (in this case, the StaticFilesMiddleware).

WARNING There are several Map extension method overloads. Some of these are extension methods on IApplicationBuilder and are used to branch the pipeline, as you saw in listing 31.2. Other overloads are extensions on IEndpointRouteBuilder and are used to create minimal endpoints, using the endpoint routing system. If you’re struggling to make your app compile, make sure that you’re not accidentally using the wrong Map overload!

If you need to, you can create sprawling branched pipelines using Map, where each branch is independent of every other. You could also nest calls to Map so you have branches coming off branches.

The Map extension can be useful, but if you try to get too elaborate, it can quickly get confusing. Remember that you should use middleware for implementing cross-cutting concerns or simple endpoints. The endpoint routing mechanism of minimal APIs and Razor Pages is better suited to more complex routing requirements, so always favor it over Map where possible.

One situation where Map can be useful is when you want to have two independent subapplications but don’t want the hassle of multiple deployments. You can use Map to keep these pipelines separate, with separate routing and endpoints inside each branch of the pipeline.

TIP This approach can be useful, for example, if you’re embedding an OpenID Connect server such as IdentityServer in your application. By mapping IdentityServer to a branch, you ensure that the endpoints and controllers in your main app can’t interfere with the endpoints exposed by IdentityServer.

Be aware that these branches share configuration and a DI container, so they’re independent only from the middleware pipeline’s point of view. You must also remember that WebApplication adds lots of middleware to the pipeline by default, so you may need to override these by explicitly calling UseRouting() in all your branches, for example.

NOTE Achieving truly independent branches in the same application requires a lot of effort. See Filip Wojcieszyn’s blog post, “Running multiple independent ASP.NET Core pipelines side by side in the same application,” for guidance: http://mng.bz/vzA4.

The final point you should be aware of when using the Map extension is that it modifies the effective Path seen by middleware on the branch. When it matches a URL prefix, the Map extension cuts off the matched segment from the path, as shown in figure 31.2. The removed segments are stored on a property of HttpContext called PathBase, so they’re still accessible if you need them.

alt text

Figure 31.2 When the Map extension diverts a request to a branch, it removes the matched segment from the Path property and adds it to the PathBase property.‌

NOTE ASP.NET Core’s link generator (used in Razor and minimal APIs, as discussed in chapter 6) uses PathBase to ensure that it generates URLs that include the PathBase as a prefix.

You’ve seen the Run extension, which always returns a response, and the Map extension, which creates a branch in the pipeline. The next extension we’ll look at is the general- purpose Use extension.

31.1.3 Adding to the pipeline with the Use extension‌

You can use the Use extension method to add a general- purpose piece of middleware. You can use it to view and modify requests as they arrive, to generate a response, or to pass the request on to subsequent middleware in the pipeline.

As with the Run extension, when you add the Use extension to your pipeline, you specify a lambda function that runs when a request reaches the middleware. ASP.NET Core passes two parameters to this function:

• The HttpContext representing the current request and response—You can use this to inspect the request or generate a response, as you saw with the Run extension.

• A pointer to the rest of the pipeline as a Func—By executing this task, you can execute the rest of the middleware pipeline.

By providing a pointer to the rest of the pipeline, you can use the Use extension to control exactly how and when the rest of the pipeline executes, as shown in figure 31.3. If you don’t call the provided Func at all, the rest of the pipeline doesn’t execute for the request, so you have complete control.

alt text

Figure 31.3 Three pieces of middleware, created with the Use extension. Invoking the provided Func using next() invokes the rest of the pipeline. Each middleware component can run code before and after calling the rest of the pipeline, or it can choose to not call next() to short-circuit the pipeline.

Exposing the rest of the pipeline as a Func makes it easy to conditionally short-circuit the pipeline, which enables

many scenarios. Instead of branching the pipeline to implement the health-check middleware with Map and Run, as you did in listing 31.2, you could use a single instance of the Use extension, as shown in the following listing. This provides the same required functionality as before but does so without branching the pipeline.

Listing 31.3 Using the Use extension method to create a health-check middleware

app.Use(async (HttpContext context, Func<Task> next) => ❶
{
if (context.Request.Path.StartsWithSegments("/ping")) ❷
{
context.Response.ContentType = "text/plain"; ❸
await context.Response.WriteAsync("pong"); ❸
}
else
{
await next(); ❹
}
});
app.UseStaticFiles();

❶ The Use extension takes a lambda with HttpContext (context) and Func<Task> (next) parameters.
❷ The StartsWithSegments method looks for the provided segment in the current path.
❸ If the path matches, generates a response and short-circuits the pipeline
❹ If the path doesn’t match, calls the next middleware in the pipeline—in this case UseStaticFiles()\

If the incoming request starts with the required path segment (/ping), the middleware responds and doesn’t call the rest of the pipeline. If the incoming request doesn’t start with /ping, the extension calls the next middleware in the pipeline, with no branching necessary.

With the Use extension, you have control of when and whether you call the rest of the middleware pipeline. But it’s important to note that you generally shouldn’t modify the Response object after calling next(). Calling next() runs the rest of the middleware pipeline, so subsequent middleware may start streaming the response to the browser. If you try to modify the response after executing the pipeline, you may end up corrupting the response or sending invalid data.

WARNING Don’t modify the Response object after calling next(). Also, don’t call next() if you’ve written to the Response.Body; writing to this Stream can trigger Kestrel to start streaming the response to the browser, and you could cause invalid data to be sent. You can generally read from the Response object safely, such as to inspect the final StatusCode or ContentType of the response.

Another common use for the Use extension method is to modify every request or response that passes through it. For example, you should send various HTTP headers with all your applications for security reasons. These headers often disable old, insecure legacy behaviors by browsers or restrict the features enabled by the browser. You learned about the HSTS header in chapter 28, but you can add other headers for additional security.

TIP You can test the security headers for your app at https://securityheaders.com, which also provides information about what headers you should add to your application and why.

Imagine you’ve been tasked with adding one such header— X-Content-Type-Options: nosniff, which provides added protection against cross-site scripting (XSS) attacks— to every response generated by your app. This sort of cross- cutting concern is perfect for middleware. You can use the Use extension method to intercept every request, set the response header, and then execute the rest of the middleware pipeline. No matter what response the pipeline generates, whether it’s a static file, an error, or a Razor Page, the response will always have the security header.‌

Listing 31.4 shows a robust way to achieve this. When the middleware receives a request, it registers a callback that runs before Kestrel starts sending the response back to the browser. It then calls next() to run the rest of the middleware pipeline. When the pipeline generates a response, likely in some later middleware, Kestrel executes the callback and adds the header. This approach ensures that the header isn’t accidentally removed by other middleware in the pipeline and also ensures that you don’t try to modify the headers after the response has started streaming to the browser.

Listing 31.4 Adding headers to a response with the Use extension

app.Use(async (HttpContext context, Func<Task> next) => ❶
{
context.Response.OnStarting(() => ❷
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff"; ❸
return Task.CompletedTask; ❹
});
await next(); ❺
}
app.UseStaticFiles(); ❻
app.UseRouting(); ❻
app.MapRazorPages ❻

❶ Adds the middleware at the start of the pipeline
❷ Sets a function that runs before the response is sent to the browser
❸ Adds the header to the response for added protection against XSS attacks
❹ The function passed to OnStarting must return a Task.
❺ Invokes the rest of the middleware pipeline
❻ No matter what response is generated, it’ll have the security header added.

Simple cross-cutting middleware like the security header example is common, but it can quickly clutter your Program.cs configuration and make it difficult to understand the pipeline at a glance. Instead, it’s common to encapsulate your middleware in a class that’s functionally equivalent to the Use extension but that can be easily tested and reused.

31.1.4 Building a custom middleware component‌

Creating middleware with the Use extension, as you did in listings 31.3 and 31.4, is convenient, but it’s not easy to test, and you’re somewhat limited in what you can do. For example, you can’t easily use DI to inject scoped services inside these basic middleware components. Normally, rather than call the Use extension directly, you’ll encapsulate your middleware into a class that’s functionally equivalent.

Custom middleware components don’t have to derive from a specific base class or implement an interface, but they have a certain shape, as shown in listing 31.5. ASP.NET Core uses reflection to execute the method at runtime. Middleware classes should have a constructor that takes a RequestDelegate object, which represents the rest of the middleware pipeline, and they should have an Invoke function with a signature similar to‌

public Task Invoke(HttpContext context);

The Invoke() function is equivalent to the lambda function from the Use extension, and it is called when a request is received. The following listing shows how you could convert the headers middleware from listing 31.4 into a standalone middleware class.

Listing 31.5 Adding headers to a Response using a custom middleware component


public class HeadersMiddleware
{
private readonly RequestDelegate _next; ❶
public HeadersMiddleware(RequestDelegate next) ❶
{ ❶
_next = next; ❶
} ❶
public async Task Invoke(HttpContext context) ❷
{
context.Response.OnStarting(() => ❸
{ ❸
context.Response.Headers["X-Content-Type-Options"] = ❸
"nosniff"; ❸
return Task.CompletedTask; ❸
}); ❸
await _next(context); ❹
}
}

❶ The RequestDelegate represents the rest of the middleware pipeline.
❷ The Invoke method is called with HttpContext when a request is received.
❸ Adds the header to the response as before
❹ Invokes the rest of the middleware pipeline. Note that you must pass in the
provided HttpContext.

NOTE Using this shape approach makes the middleware more flexible. In particular, it means you can easily use DI to inject services into the Invoke method. This wouldn’t be possible if the Invoke method were an overridden base class method or an interface. However, if you prefer, you can implement the IMiddleware interface, which defines the basic Invoke method.

This middleware is effectively identical to the example in listing 31.4, but it’s encapsulated in a class called HeadersMiddleware. You can add this middleware to your app in Startup.Configure by calling

app.UseMiddleware<HeadersMiddleware>();

A common pattern is to create helper extension methods to make it easy to consume your extension method from

Program.cs (so that IntelliSense reveals it as an option on the WebApplication instance). The following listing shows how you could create a simple extension method for HeadersMiddleware.

Listing 31.6 Creating an extension method to expose HeadersMiddleware

public static class MiddlewareExtensions
{
public static IApplicationBuilder UseSecurityHeaders( ❶
this IApplicationBuilder app) ❶
{
return app.UseMiddleware<HeadersMiddleware>(); ❷
}
}

❶ By convention, the extension method should return an IApplicationBuilder to allow chaining.
❷ Adds the middleware to the pipeline

With this extension method, you can now add the headers middleware to your app using

app.UseSecurityHeaders();

TIP My SecurityHeaders NuGet package makes it easy to add security headers using middleware without having to write your own. The package provides a fluent interface for adding the recommended security headers to your app. You can find instructions on how to install it at http://mng.bz/JggK.

Listing 31.5 is a simple example, but you can create middleware for many purposes. In some cases you may need to use DI to inject services and use them to handle a request. You can inject singleton services into the constructor of your middleware component, or you can inject services with any lifetime into the Invoke method of your middleware, as demonstrated in the following listing.

Listing 31.7 Using DI in middleware components

public class ExampleMiddleware
{
private readonly RequestDelegate _next;
private readonly ServiceA _a; ❶
public HeadersMiddleware(RequestDelegate next, ServiceA a) ❶
{ ❶
_next = next; ❶
_a = a; ❶
}
public async Task Invoke(
HttpContext context, ServiceB b, ServiceC c) ❷
{
// use services a, b, and c
// and/or call _next.Invoke(context);
}
}

❶ You can inject additional services in the constructor. These must be singletons.
❷ You can inject services into the Invoke method. These may have any lifetime.

WARNING ASP.NET Core creates the middleware only once for the lifetime of your app, so any dependencies injected in the constructor must be singletons. If you need to use scoped or transient dependencies, inject them into the Invoke method.

In addition to cross-cutting concerns, a good use for middleware is creating simple handlers with as few dependencies as possible that respond to a fixed URL, similar to the Use extension method you learned about in section 31.1.3. These simple handlers can be dropped into multiple applications, regardless of how the app’s routing is configured.

So-called well-known Uniform Resource Identifiers (URIs) are a good use case for these simple middleware handlers, such as the security.txt well-known URI (https://www.rfc- editor.org/rfc/rfc9116) and the OpenID Connect URIs (http://mng.bz/wvj2). These handlers always respond to a single path, so they can neatly encapsulate all the logic without risk of interfering with any other routing configuration.‌

Listing 31.8 shows a simple example of a security.txt handler implemented as middleware. It always responds to the well- known path with a fixed value and is easy to add to any application by calling app.UseMiddleware.

Listing 31.8 A Security.txt handler implemented as middleware

public class SecurityTxtHandler
{
private readonly RequestDelegate _next;
public SecurityTxtHandler(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext context)
{
var path = context.Request.Path;
if(path.StartsWithSegments("/.well-known/security.txt")) ❶
{
context.Response.ContentType = "text/plain"; ❷
return context.Response.WriteAsync( ❷
"Contact: mailto:security@example.com"); ❷
}
return _next.Invoke(context); ❸
}
}

❶ The middleware looks for a fixed, well-known path.
❷ If the path is matched, the middleware returns a response.
❸ If the path didn’t match, the next middleware in the pipeline is called.

That covers pretty much everything you need to start building your own middleware components. By encapsulating your middleware in custom classes, you can easily test their behavior or distribute them in NuGet packages, so I strongly recommend taking this approach. Apart from anything else, it will make Program.cs file less cluttered and easier to understand.

31.1.5 Converting middleware into endpoint routing endpoints‌

In this section you’ll learn how you can take the custom middleware you created in section 31.1.2 and convert it to a simple middleware endpoint that integrates into the endpoint routing system. Then you can take advantage of features such as routing and authorization.

In section 31.1.2 I described creating a simple ping-pong endpoint, using the Map and Run extension methods, that returns a plain-text pong response whenever a /ping request is received by branching the middleware pipeline.‌‌This is fine because it’s so simple, but what if you have more complex requirements?

Consider a basic enhancement of this ping-pong example. How would you add authorization to the request? The AuthorizationMiddleware looks for metadata on endpoints like Razor Pages or minimal APIs to see whether there’s any authorization metadata, but it doesn’t know how to work with the ping-pong Map extension.

Similarly, what if you wanted to use more complex routing? Maybe you want to be able to call /ping/3 and have your ping-pong middleware reply pong-pong-pong. (No, I can’t think why you would either!) You now have to try to parse that integer from the URL, make sure it’s valid, and so on.That’s sounding like a lot more work and seems to be a clear indicator you should have created a minimal API endpoint using endpoint routing!

For our simple ping-pong endpoint, that wouldn’t be hard to do, but what if you have a more complex middleware component that you don’t want to rewrite completely? Is there some way to convert the middleware to an endpoint?

Let’s imagine that you need to apply authorization to the simple ping-pong endpoint you created in section 31.1.2. This is much easier to achieve with endpoint routing than simple middleware branches like Map or Use, but let’s imagine you want to stick to using middleware instead of a traditional minimal API endpoint. The first step is creating a standalone middleware component for the functionality,using the approach you saw in section 31.1.4, as shown in the following listing.

Listing 31.9 The PingPongMiddleware implemented as a middleware component

public class PingPongMiddleware
{
public PingPongMiddleware(RequestDelegate next) ❶
{
}
public async Task Invoke(HttpContext context) ❷
{
context.Response.ContentType = "text/plain"; ❸
await context.Response.WriteAsync("pong"); ❸
}
}

❶ Even though it isn’t used in this case, you must inject a RequestDelegate in the
constructor.
❷ Invoke is called to execute the middleware.
❸ The middleware always returns a “pong” response.

Note that this middleware always returns a "pong" response regardless of the request URL; we will configure the "/ping" path later. We can use this class to convert a middleware pipeline from the branching version shown in figure 31.1, to the endpoint version shown in figure 31.4.

alt text

Figure 31.4 Endpoint routing separates the selection of an endpoint from the execution of an endpoint. The routing middleware selects an endpoint based on the incoming request and exposes metadata about the endpoint. Middleware placed before the endpoint middleware can act based on the selected endpoint, such as short-circuiting unauthorized requests. If the request is authorized, the endpoint middleware executes the selected endpoint and generates a response.

Converting the ping-pong middleware to an endpoint doesn’t require any changes to the middleware itself. Instead, you need to create a mini middleware pipeline containing only your ping-pong middleware.

TIP Converting response-generating middleware to an endpoint essentially requires converting it to its own mini pipeline, so you can even include additional middleware in the endpoint pipeline if you wish.

To create the mini pipeline, you call CreateApplicationBuilder() on IEndpointRouteBuilder instance, which creates a new IApplicationBuilder. There are two ways to access the IEndpointRouteBuilder: call UseEndpoints(endpoints =>{}) and use the endpoints variable or explicitly cast WebApplication to IEndpointRouteBuilder.‌

NOTE Although WebApplication implements IEndpointRouteBuilder, it deliberately hides the advanced CreateApplicationBuilder() method from you! This should be a good indication that you’re in advanced territory and should probably consider using minimal API endpoints instead.

In the following listing, we create a new IApplicationBuilder, add the middleware that makes up the endpoint to it, and then call Build() to create the pipeline. Once you have a pipeline, you can associate it with a given route by calling Map() on the IEndpointRouteBuilder instance and passing in a route template.

Listing 31.10 Mapping the ping-pong endpoint in UseEndpoints

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
var endpoint = ((IEndpointRouteBuilder)app) ❶
.CreateApplicationBuilder() ❷
.UseMiddleware<PingPongMiddleware>() ❸
.Build(); ❸
app.Map("/ping", endpoint); ❹
app.MapRazorPages();
app.MapHealthChecks("/healthz");
app.Run();

❶ Casts the WebApplication to IEndpointRouteBuilder so you can call
CreateApplicationBuilider
❷ Creates a miniature, standalone IApplicationBuilder to build your endpoint
❸ Adds the middleware and builds the final endpoint. This is executed when the
endpoint is executed.
❹ Maps the new endpoint with the route template “/ping”

TIP Note that the Map() function on IEndpointRouteBuilder creates a new endpoint (consisting of your mini-pipeline) with an associated route.

Although it has the same name, this is conceptually different from the Map function on IApplicationBuilder from section 31.1.2, which is used to branch the middleware pipeline. It is analogous to the MapGet (and kin) methods you use to create minimal API endpoints.

As is common with ASP.NET Core, you can extract this somewhat-verbose functionality into an extension method to make your endpoint easier to read and discover. The following listing extracts the code to create an endpoint from listing 31.10 into a separate class, taking the route template to use as a method parameter.

Listing 31.11 An extension method for using the PingPongMiddleware as an endpoint

public static class EndpointRouteBuilderExtensions
{
public static IEndpointConventionBuilder MapPingPong( ❶
this IEndpointRouteBuilder endpoints, ❶
string route) ❷
{
var pipeline = endpoints
.CreateApplicationBuilder() ❸
.UseMiddleware<PingPongMiddleware>() ❸
.Build(); ❸
return endpoints ❹
.Map(route, pipeline) ❹
.RequireAuthorization(); ❺
}
}

❶ Creates an extension method for registering the PingPongMiddleware as an endpoint
❷ Allows the caller to pass in a route template for the endpoint
❸ Creates the endpoint pipeline
❹ Adds the new endpoint to the provided endpoint collection, using the provide route template
❺ You can add additional metadata here directly, or the caller can add metadata themselves.

Now that you have an extension method, MapPingPong(), you can update your mapping code to be simpler and easier to understand:

app.MapPingPong("/ping"); 
app.MapRazorPages(); app.MapHealthChecks("/healthz");

Congratulations—you’ve created your first custom endpoint from middleware! By turning the middleware into an endpoint, you can now add extra metadata, as shown in listing 31.11. Your middleware is hooked into the endpoint routing system and benefits from everything it offers.

The example in listing 31.11 used a basic route template, "/ping", but you can also use templates that contain route parameters, such as "/ping/{count}", as you would with minimal APIs. The big difference is that you don’t get the benefits of model binding that you get from minimal APIs, and it clearly takes more effort than using minimal APIs!

TIP For examples of how to access the route data from your middleware, as well as best-practice advice, see my blog entry titled “Accessing route values in endpoint middleware in ASP.NET Core 3.0” at http://mng.bz/4ZRj.

Converting existing middleware like PingPongMiddleware to work with endpoint routing can be useful when you have already implemented that middleware, but it’s a lot of boilerplate to write if you want to create a new simple endpoint. In almost all cases you should use minimal API endpoints instead. But if you ever find yourself needing to reuse some existing middleware as an endpoint, now you know how!

In the next section we’ll move away from the middleware pipeline and look at how to handle a common configuration requirement: using DI services to build a strongly typed IOptions objects.‌

31.2 Using DI with OptionsBuilder and IConfigureOptions‌

In this section I describe how to handle a common scenario: you want to use services registered in DI to configure IOptions objects. There are several ways to achieve this, but in this section I introduce the OptionsBuilder as one possible approach and highlight some of the other features it enables.

In chapter 10 we discussed the ASP.NET Core configuration system in depth. You saw how an IConfiguration object is built from multiple layers, where subsequent layers can add to or replace configuration values from previous layers. Each layer is added by a configuration provider, which reads values from a file, from environment variables, from User Secrets, or from any number of possible locations.

A common and encouraged practice is to bind your configuration object to strongly typed IOptions objects, as you saw in chapter 10. Typically, you configure this binding in Program.cs by calling builder.Services.Configure<T>() and providing an IConfiguration object or a configuration section to bind.

For example, to bind a strongly typed object called CurrencyOptions to the "Currencies" section of an IConfiguration object, you could use the following:

builder.services.Configure<CurrencyOptions>( Configuration.GetSection("Currencies"));

TIP You can see an example of the CurrencyOptions type and the associated "Currencies" section of appsetttings.json in the source code for this chapter.

This sets the properties of the CurrencyOptions object, based on the values in the "Currencies" section of your IConfiguration object. Simple binding like this is common, but sometimes you might not want to rely on configuring your IOptions<T> objects via the configuration system; you might want to configure them in code instead.The IOptions pattern requires only that you configure a strongly typed object before it’s injected into a dependent service; it doesn’t mandate that you have to bind it to an IConfiguration section.

TIP Technically, even if you don’t configure an IOptions at all, you can still inject it into your services. In that case, the T object is simply created using the default constructor.

The Configure() method has an additional overload that takes a lambda function. The framework executes the lambda function to configure the CurrencyOptions object when it is injected using DI. The following listing shows an example that uses a lambda function to set the Currencies property on a configured CurrencyOptions object to a fixed array of strings.‌‌

Listing 31.12 Configuring an IOptions object using a lambda function

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<CurrencyOptions>( ❶
builder.Configuration.GetSection("Currencies")); ❶
builder.services.Configure<CurrencyOptions>(options => ❷
options.Currencies = new string[] { "GBP", "USD"}); ❷
WebApplication app = builder.Build();
app.MapGet("/", (IOptions<CurrencyOptions> opts) => opts.Value); ❸
app.Run();

❶ Configures the IOptions object by binding to an IConfiguration section
❷ Configures the IOptions object by executing a lambda function
❸ The injected IOptions value is built by first binding to configuration and then applying the lambda.

Each call to Configure<T>(), both the binding to IConfiguration and the lambda function, adds another configuration step to the CurrencyOptions object. When the DI container first requires an instance of IOptions, the steps run in turn, as shown in figure 31.5.

alt text

Figure 31.5 Configuring a CurrencyOptions object. When the DI container needs an IOptions<> instance of a strongly typed object, the container creates the object and then uses each of the registered Configure() methods to set the object’s properties.

In the previous code snippet, you set the Currencies property to a static array of strings in a lambda function. But what if you don’t know the correct values ahead of time? You might need to load the available currencies from a database or from some remote service, such as an ICurrencyProvider.

This situation, in which you need a configured service to configure your IOptions<T>, is potentially hard to resolve. Remember that you declared your IOptions<T> configuration as part of your app’s DI configuration. But if you need to resolve a service from DI to configure the IOptions object, you’re stuck with a chicken-and-egg problem: how can you access a service from the DI container before you’ve finished configuring the DI container?

This circular problem has several potential solutions, but the easiest approach is to use an alternative API for configuring IOptions instances, using the OptionsBuilder type. This type is effectively a wrapper around some of the core IOptions interfaces, but it often results in a terser and more convenient syntax to the approach you’ve seen so far.‌

TIP Another helpful feature of OptionsBuilder is adding validation to your IOptions objects. This ensures that your configuration is loaded and bound correctly on app startup so that you don’t have any typos in your configuration section names, for example. You can read more about adding validation to your IOptions objects on my blog at http://mng.bz/qrjJ.

The following listing shows the equivalent of listing 31.12 but using OptionsBuilder<T> instead. You create an OptionsBuilder<T> instance by calling AddOptions<T> (), and then chain additional methods such as BindConfiguration() and Configure() to configure your final IOptions<T> object, building up layers of options configuration, as shown previously in figure 31.5.‌

Listing 31.13 Configuring an IOptions<T> object using OptionsBuilder<T>

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<CurrencyOptions>() ❶
.BindConfiguration("Currencies") ❷
.Configure(opts => ❸
opts.Currencies = new string[] { "GBP", "USD"}); ❸
WebApplication app = builder.Build();
app.MapGet("/", (IOptions<CurrencyOptions> opts) => opts.Value);
app.Run();

❶ Creates an OptionsBuilder object
❷ Binds to the Currencies section of the IConfiguration
❸ Configures the IOptions object by executing a lambda function

You’ve seen the builder pattern many times throughout the book, and the pattern in this case is no different. The builder exposes methods that you can chain together fluently. One of the benefits of the builder pattern is that it’s easy to discover all the methods it exposes. In this case, if you explore the type in your integrated development environment (IDE), you may notice that OptionsBuilder<T> exposes multiple Configure overloads, such as

• Configure(Action<T,TDep> config);

• Configure<TDep1,TDep2>(Action<T, TDep1, TDep2> config);

• Configure<TDep1,TDep2,TDep3> (Action<T,TDep1,TDep2,TDep3> config);

These methods allow you to specify dependencies that are automatically retrieved from the DI container and passed to the config action when the IOptions object is fetched from DI, as shown in figure 31.6. Five overloads for Configure allow you to inject dependencies, allowing you to inject up to five dependencies with these methods.

alt text
alt text

Figure 31.6 Using OptionsBuilder to build an IOptions object. Dependencies that are requested via the Configure methods are automatically retrieved from the DI container and used to execute the lambda function.

Using this pattern, we can update the code from listing 31.13 to use the ICurrencyProvider whenever our app needs to create the CurrencyOptions object. We can register the service in the DI container and know that the DI will take

care of providing it to the lambda function at runtime, as shown in the following listing.

Listing 31.14 Using a DI service

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<CurrencyOptions>()
.BindConfiguration("Currencies")
.Configure<ICurrencyProvider>((opts, service) => ❶
opts.Currencies = service.GetCurrencies()); ❶
builder.Services.AddSingleton<ICurrencyProvider, CurrencyProvider>(); ❷
WebApplication app = builder.Build();
app.MapGet("/", (IOptions<CurrencyOptions> opts) => opts.Value); ❸
app.Run();

❶ Configures the Ioptions object using a service from DI
❷ Registers the service with the DI container
❸ Retrieves the IOptions object, which retrieves the service from DI and runs the lambda method

With the configuration in listing 31.14, when the IOptions<CurrencyOptions> is first injected into the minimal API endpoint, the IOptions<CurrencyOptions> object is built as described by the OptionsBuilder. First, the "Currencies" section of the app IConfiguration is bound to a new CurrencyOptions object. Then the ICurrencyProvider is retrieved from DI and passed to the Configure<TDep> lambda, along with the options object. Finally, the IOptions object is injected into the endpoint.

WARNING You must inject only singleton services using Configure<TDeps> methods. If you try to inject a scoped service, such as a DbContext, you will get an error in development warning you about a captive dependency. I describe how to work around this on my blog at http://mng.bz/7Dve.

The OptionsBuilder<T> is a convenient way to configure your IOptions objects using dependencies, but you can use an alternative approach: implementing the IConfigureOptions<T> interface. You implement this interface in a configuration class and use it to configure the IOptions object in any way you need, as shown in the following listing. This class can use DI, so you can easily use any other required services.

Listing 31.15 Implementing IConfigureOptions<T> to configure an options object

public class ConfigureCurrencyOptions : IConfigureOptions<CurrencyOptions>
{
private readonly ICurrencyProvider _currencyProvider; ❶
public ConfigureCurrencyOptions(ICurrencyProvider currencyProvider)
{
_currencyProvider = currencyProvider; ❶
}
public void Configure(CurrencyOptions options) ❷
{
options.Currencies = _currencyProvider.GetCurrencies(); ❸
}
}

❶ You can inject services that are available only after the DI is completely configured.
❷ Configure is called when an instance of IOptions<CurrencyOptions> is required.
❸ Uses the injected service to load the values

All that remains is to register the implementation in the DI container. As always, order is important, so if you want ConfigureCurrencyOptions to run after binding to configuration, you must add it after configuring your OptionsBuilder<T>:

builder.Services.AddOptions<CurrencyOptions>()
.BindConfiguration("Currencies");
builder.AddSingleton
<IConfigureOptions<CurrencyOptions>, ConfigureCurrencyOptions>();

TIP The order in which you configure your options matters. If you want to always run your configuration last, after all other configuration methods, you can use the PostConfigure() method on OptionsBuilder, or the IPostConfigureOptions interface. You can read more about this approach on my blog at http://mng.bz/mVj4.‌‌

With this configuration, when IOptions is injected into an endpoint or service, the CurrencyOptions object is first bound to the "Currencies" section of your IConfiguration and then configured by the ConfigureCurrencyOptions class.‌

WARNING The CurrencyConfigureOptions object is registered as a singleton, so it will capture any injected services of scoped or transient lifetimes.

Whether you use the OptionsBuilder or the IConfigureOptions approach, you need to register the ICurrencyProvider dependency with the DI container. In the sample code for this chapter, I created a simple CurrencyProvider service and registered it with the DI container using‌‌

builder.Services.AddSingleton<ICurrencyProvider, CurrencyProvider>();

As your app grows and you add extra features and services, you’ll probably find yourself writing more of these simple DI registrations, where you register a Service that implements IService. The built-in ASP.NET Core DI container requires you to explicitly register each of these services manually. If you find this requirement frustrating, it may be time to look at third-party DI containers that can take care of some of the boilerplate for you.

31.3 Using a third-party dependency injection container‌

In this section I show you how to replace the default DI container with a third-party alternative, Lamar. Third-party containers often provide additional features compared with the built-in container, such as assembly scanning, automatic service registration, and property injection. Replacing the built-in container can also be useful when you’re porting an existing app that uses a third-party DI container to ASP.NET Core.

The .NET community had used DI containers for years before ASP.NET Core decided to include a built-in one. The ASP.NET Core team wanted a way to use DI in their own framework libraries, and they wanted to create a common abstraction1 that allows you to replace the built-in container with your favorite third-party alternative, such as Autofac, StructureMap/Lamar, Ninject, Simple Injector, or Unity.

The built-in container is intentionally limited in the features it provides, and realistically, it won’t be getting many more. By contrast, third-party containers can provide a host of extra features. These are some of the features available in Lamar (https://jasperfx.github.io/lamar/guide/ioc), the spiritual successor to StructureMap (https://structuremap.github.io):

• Assembly scanning for interface/implementation pairs based on conventions

• Automatic concrete class registration Property injection and constructor selection

• Automatic Lazy/Func resolution

• Debugging/testing tools for viewing inside your container

None of these features is a requirement for getting an application up and running, so using the built-in container makes a lot of sense if you’re building a small app or are new to DI containers in general. But if at some undefined tipping point, the simplicity of the built-in container becomes too much of a burden, it may be worth replacing.

TIP A middle-of-the-road approach is to use the Scrutor NuGet package, which adds some features to the built-in DI container without replacing it. For an introduction and examples, see my blog post, “Using Scrutor to automatically register your services with the ASP.NET Core DI container” at http://mng.bz/MX7B.

In this section I show how you can configure an ASP.NET Core app to use Lamar for dependency resolution. It won’t be a complex example or an in-depth discussion of Lamar itself.Instead, I’ll cover the bare minimum to get you up and running.

Whichever third-party container you choose to install in an existing app, the overall process is pretty much the same:

  1. Install the container NuGet package.

  2. Register the third-party container with WebApplicationBuilder in Program.cs.

  3. Configure the third-party container to register your services.

Most of the major .NET DI containers include adapters and extension methods to hook easily into your ASP.NET Core app. For details, it’s worth consulting the specific guidance for the container you’re using. For Lamar, the process looks like this:

  1. Install the Lamar.Microsoft.DependencyInjection NuGet package using the NuGet package manager, by running dotnet add package

    dotnet add package Lamar.Microsoft.DependencyInjection

    or by adding a to your .csproj file:

    <PackageReference 
    Include="Lamar.Microsoft.DependencyInjection" Version="8.1.0" />
  2. Call UseLamar() on WebApplicationBuilder.Host in Program.cs:

    WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
    builder.Host.UseLamar(services => {})
    WebApplication app = builder.Build();
  3. Configure the Lamar ServiceRegistry in the lambda method passed to UseLamar(), as shown in the following listing. This is a basic configuration, but you can see a more complex example in the source code for this chapter.

Listing 31.16 Configuring Lamar as a third-party DI container

builder.Host.UseLamar(services => ❶
{
services.AddAuthorization(); ❷
services.AddControllers() ❷
.AddControllersAsServices(); ❸
services.Scan(_ => { ❹
_.AssemblyContainingType(typeof(Program)); ❹
_.WithDefaultConventions(); ❹
}); ❹
}

❶ Configures your services in UseLamar() instead of on builder.Services
❷ You can (and should) add ASP.NET Core framework services to the
ServiceRegistry, as usual.
❸ Required so that Lamar is used to build your web API controllers
❹ Lamar can automatically scan your assemblies for services to register.

In this example I’ve used the default conventions to register services. This automatically registers concrete classes and services that are named following expected conventions (for example, Service implements IService). You can change these conventions or add other registrations in the UseLamar() lambda.

The ServiceRegistry passed into UseLamar() implements IServiceCollection, which means you can use all the built-in extension methods, such as AddControllers() and AddAuthorization(), to add framework services to your container.‌

WARNING If you’re using DI in your Model-View-Controller (MVC) controllers (almost certainly!), and you register those dependencies with Lamar rather than the built-in container, you may need to call AddControllersAsServices(), as shown in listing 31.16. This is due to an implementation detail in the way your MVC controllers are created by the framework. For details, see my blog entry titled “Controller activation and dependency injection in ASP.NET Core MVC” at http://mng.bz/aogm.

With this configuration in place, whenever your app needs to create a service, it will request it from the Lamar container, which will create the dependency tree for the class and create an instance. This example doesn’t show off the power of Lamar, so be sure to check out the documentation (https://jasperfx.github.io/lamar) and the associated source code for this chapter for more examples. Even in modest-size applications, Lamar can greatly simplify your service registration code, but its party trick is showing all the services you have registered and any associated issues.

TIP Third-party containers typically add configuration approaches but don’t change any of the fundamentals of how DI works in ASP.NET Core. All the techniques you’ve seen in this book will work whether you’re using the built-in container or a third-party container, so you can use the IConfigureOptions approach in section 31.2, for example, regardless of which container you choose.

That brings us to the end of this chapter on advanced configuration. In this chapter I focused on some of the core components of any ASP.NET Core app: middleware, configuration, and DI. In the next chapter you’ll learn about more custom components, with a focus on Razor Pages and web API controllers.‌

Summary

Use the Run extension method to create middleware components that always return a response. You should always place the Run extension at the end of a middleware pipeline or branch, as middleware placed after it will never execute.

You can create branches in the middleware pipeline with the Map extension. If an incoming request matches the specified path prefix, the request will execute the pipeline branch; otherwise, it will execute the trunk.

When the Map extension matches a request path segment, it removes the segment from the request’s HttpContext.Path and moves it to the PathBase property. This ensures that routing in branches works correctly.

You can use the Use extension method to create generalized middleware components that can generate a response, modify the request, or pass the request on to subsequent middleware in the pipeline. This is useful for cross-cutting concerns, like adding a header to all responses.

You can encapsulate middleware in a reusable class. The class should take a RequestDelegate object in the constructor and should have a public Invoke() method that takes an HttpContext and returns a Task. To call the next middleware component in the pipeline, invoke the RequestDelegate with the provided HttpContext.

To create endpoints that generate a response, build a miniature pipeline containing the response- generating middleware, and call endpoints.Map(route, pipeline). Endpoint routing will be used to map incoming requests to your endpoint.

You can configure IOptions<T> objects using a fluent builder interface. Call AddOptions<T>() to create an OptionsBuilder<T> instance and then chain configuration calls.

OptionsBuilder<T> allows easy access to dependencies for configuration, as well as features such as validation.

You can also use services from the DI container to configure an IOptions<T> object by creating a separate class that implements IConfigureOptions<T>. This class can use DI in the constructor and is used to lazily build a requested IOptions<T> object at runtime.

You can replace the built-in DI container with a third-party container. Third-party containers often provide additional features, such as convention- based dependency registration, assembly scanning, and property injection.

  1. Although the promotion of DI as a core practice has been applauded, this abstraction has seen some controversy. This post, titled “What’s wrong with the ASP.NET Core DI abstraction?”, from one of the maintainers of the SimpleInjector DI library, describes many of the arguments and concerns: http://mng.bz/yYAd. You can also read more about the decisions at http://mng.bz/6DnA.

ASP.NET Core in Action 30 Building ASP.NET Core apps with the generic host and Startup

Part 5 Going further with ASP.NET Core‌

第 5 部分:进一步了解 ASP.NET Core

Parts 1 through 4 of this book touched on all the aspects of ASP.NET Core you need to learn to build an HTTP application, whether that’s server-rendered applications using Razor Pages or JavaScript Object Notation (JSON) APIs using minimal APIs. In part 5 we look at four topics that build on what you’ve learned so far: customizing ASP.NET Core to your needs, interacting with third-party HTTP APIs, background services, and testing.
本书的第 1 部分到第 4 部分介绍了构建 HTTP 应用程序需要学习的 ASP.NET Core 的所有方面,无论是使用 Razor Pages 的服务器呈现的应用程序,还是使用最少 API 的 JavaScript 对象表示法 (JSON) API。在第 5 部分中,我们将介绍基于您目前所学知识的四个主题:根据您的需求自定义 ASP.NET Core、与第三方 HTTP API 交互、后台服务和测试。

In chapter 30 we start by looking at an alternative way to bootstrap your ASP.NET Core applications, using the generic host instead of the WebApplication approach you’ve seen so far in the book. The generic host was the standard way to bootstrap apps before .NET 6 (and is the approach you’ll find in previous editions of this book), so it’s useful to recognize the pattern, but it also comes in handy for building non-HTTP applications, as you’ll see in chapter 34.
在第 30 章中,我们首先研究了一种替代方法来引导 ASP.NET Core 应用程序,使用通用主机而不是您在本书中到目前为止看到的 WebApplication 方法。在 .NET 6 之前,泛型主机是引导应用程序的标准方法(您将在本书的前几个版本中找到该方法),因此识别模式很有用,但它在构建非 HTTP 应用程序时也很方便,如第 34 章所示。

In part 1 you learned about the middleware pipeline, and you saw how it is fundamental to all ASP.NET Core applications. In chapter 31 you’ll learn how to take full advantage of the pipeline, creating branching middleware pipelines, custom middleware, and simple middleware- based endpoints. You’ll also learn how to handle some complex chicken-and-egg configuration issues that often arise in real-life applications. Finally, you’ll learn how to replace the built-in dependency injection container with a third-party alternative.
在第 1 部分中,您了解了中间件管道,并了解了它如何成为所有 ASP.NET Core 应用程序的基础。在第 31 章中,您将学习如何充分利用管道,创建分支中间件管道、自定义中间件和基于中间件的简单端点。您还将学习如何处理实际应用程序中经常出现的一些复杂的先有鸡还是先有蛋的配置问题。最后,您将学习如何将内置的依赖项注入容器替换为第三方替代方案。

In chapter 32 you’ll learn how to create custom components for working with Razor Pages and API controllers. You’ll learn how to create custom Tag Helpers and validation attributes, and I’ll introduce a new component—view components—for encapsulating logic with Razor view rendering. You’ll also learn how to replace the attribute-based validation framework used by default in ASP.NET Core with an alternative.
在第 32 章中,您将学习如何创建自定义组件以使用 Razor Pages 和 API 控制器。您将学习如何创建自定义标记帮助程序和验证属性,并且我将介绍一个新组件 — 视图组件 — 用于使用 Razor 视图渲染封装逻辑。您还将了解如何将 ASP.NET Core 中默认使用的基于属性的验证框架替换为替代框架。

Most apps you build aren’t designed to stand on their own. It’s common for your app to need to interact with APIs, whether those are APIs for sending emails, taking payments, or interacting with your own internal applications. In chapter 33 you’ll learn how to call these APIs using the IHttpClientFactory abstraction to simplify configuration, add transient fault handling, and avoid common pitfalls.
您构建的大多数应用程序都不是为了独立而构建的。您的应用通常需要与 API 交互,无论这些 API 是用于发送电子邮件、收款还是与您自己的内部应用程序交互的 API。在第 33 章中,您将学习如何使用 IHttpClientFactory 抽象调用这些 API,以简化配置、添加瞬态故障处理并避免常见陷阱。

This book deals primarily with serving HTTP traffic, both server-rendered web pages using Razor Pages and web APIs commonly used by mobile and single-page applications.
本书主要介绍提供 HTTP 流量,包括使用 Razor Pages 的服务器呈现的网页,以及移动和单页应用程序常用的 Web API。

However, many apps require long-running background tasks that execute jobs on a schedule or that process items from a queue. In chapter 34 I’ll show how you can create these long-running background tasks in your ASP.NET Core applications. I’ll also show how to create standalone services that have only background tasks, without any HTTP handling, and how to install them as a Windows Service or as a Linux systemd daemon.
但是,许多应用程序需要长时间运行的后台任务,这些任务按计划执行作业或处理队列中的项目。在第 34 章中,我将展示如何在 ASP.NET Core 应用程序中创建这些长时间运行的后台任务。我还将展示如何创建仅包含后台任务而没有任何 HTTP 处理的独立服务,以及如何将它们安装为 Windows 服务或 Linux systemd 守护程序。

Chapters 35 and 36, the final chapters, cover testing your application. The exact role of testing in application development can lead to philosophical arguments, but in these chapters I stick to the practicalities of testing your app with the xUnit test framework. You’ll see how to create unit tests for your apps, test code that’s dependent on EF Core using an in-memory database provider, and write integration tests that can test multiple aspects of your application at the same time.
第 35 章和第 36 章是最后几章,涵盖了测试您的应用程序。测试在应用程序开发中的确切作用可能会导致哲学争论,但在这些章节中,我将重点介绍使用 xUnit 测试框架测试应用程序的实用性。你将了解如何为应用创建单元测试,使用内存中数据库提供程序测试依赖于 EF Core 的代码,以及编写可以同时测试应用程序多个方面的集成测试。

In the fast-paced world of web development there’s always more to learn, but by the end of part 5 you should have everything you need to build applications with ASP.NET Core, whether they be server-rendered page-based applications, APIs, or background services.
在快节奏的 Web 开发世界中,总是有更多的东西需要学习,但在第 5 部分结束时,您应该拥有使用 ASP.NET Core 构建应用程序所需的一切,无论它们是服务器渲染的基于页面的应用程序、API 还是后台服务。

In the appendices for this book, I provide some background and resources about .NET. Appendix A describes how to prepare your development environment by installing .NET 7 and an IDE or editor. In appendix B you’ll find a list of resources I use to learn more about ASP.NET Core and to stay up to date with the latest features.
在本书的附录中,我提供了一些有关 .NET 的背景和资源。附录 A 介绍了如何通过安装 .NET 7 和 IDE 或编辑器来准备开发环境。在附录 B 中,您将找到我用来了解有关 ASP.NET Core 的更多信息并了解最新功能的资源列表。

30 Building ASP.NET Core apps with the generic host and Startup

30 使用通用主机构建 ASP.NET Core 应用程序 和 Startup

This chapter covers
本章涵盖

• Using the generic host and a Startup class to bootstrap your ASP.NET Core app
使用泛型主机和 Startup 类引导 ASP.NET Core 应用程序

• Understanding how the generic host differs from WebApplication
了解通用主机与 WebApplication 的区别

• Building a custom generic IHostBuilder
构建自定义通用 IHostBuilder

• Choosing between the generic host and minimal hosting
在通用主机和最小主机之间进行选择

Some of the biggest changes introduced in ASP.NET Core in .NET 6 were the minimal hosting APIs, namely the WebApplication and WebApplicationBuilder types you’ve seen throughout this book. These were introduced to dramatically reduce the amount of code needed to get started with ASP.NET Core and are now the default way to build ASP.NET Core apps.‌
在 .NET 6 中引入 ASP.NET Core 的一些最大变化是最小的托管 API,即您在本书中看到的 WebApplication 和 WebApplicationBuilder 类型。引入这些应用程序是为了显著减少开始使用 ASP.NET Core 所需的代码量,现在是构建 ASP.NET Core 应用程序的默认方式。

Before .NET 6, ASP.NET Core used a different approach to bootstrap your app: the generic host, IHost, IHostBuilder, and a Startup class. Even though this approach is not the default in .NET 7, it’s still valid, so it’s important that you’re aware of it, even if you don’t need to use it yourself. In this chapter I introduce the generic host and show how it relates to the minimal hosting APIs you’re already familiar with. In chapter 34 you’ll learn how to use the generic host approach to build nonweb apps too.
在 .NET 6 之前,ASP.NET Core 使用不同的方法来启动应用程序:泛型主机、IHost、IHostBuilder 和 Startup 类。即使此方法不是 .NET 7 中的默认方法,它仍然有效,因此即使您自己不需要使用它,了解它也很重要。在本章中,我将介绍通用主机,并展示它与您已经熟悉的最小托管 API 的关系。在第 34 章中,你也将学习如何使用通用的 host 方法来构建非 Web 应用程序。

I start by introducing the two main concepts: the generic host components (IHostBuilder and IHost) and the Startup class. These split your app bootstrapping code between two files, Program.cs and Startup.cs, handling different aspects of your app’s configuration. You’ll learn why this split was introduced, where each component is configured, and how it compares with minimal hosting using WebApplication.
首先,我将介绍两个主要概念:通用主机组件(IHostBuilder 和 IHost)和 Startup 类。这些选项将你的应用程序引导代码拆分为两个文件(Program.cs 和 Startup.cs),处理应用程序配置的不同方面。您将了解引入此拆分的原因、每个组件的配置位置,以及它与使用 WebApplication 的最小托管的比较。

In section 30.4 you’ll learn how the helper function Host.CreateDefaultBuilder() works and use this knowledge to customize the IHostBuilder instance. This can give you greater control than minimal hosting, which may be useful in some situations.
在第 30.4 节中,您将了解帮助程序函数 Host.CreateDefaultBuilder() 的工作原理,并利用这些知识自定义 IHostBuilder 实例。这可以为您提供比最小托管更大的控制权,这在某些情况下可能很有用。

In section 30.5 we take a step back and look at some of the drawbacks in the generic host bootstrapping code we’ve explored, particularly its apparent complexity compared to minimal hosting with WebApplication.
在 Section 30.5 中,我们退后一步,看看我们探索过的通用主机引导代码中的一些缺点,特别是与使用 WebApplication 进行最小托管相比,它明显的复杂性。

Finally, in section 30.6 I discuss some of the reasons you might nevertheless choose to use the generic host instead of minimal hosting in your .NET 7 app. In most cases I suggest using minimal hosting with WebApplication, but there are valid cases in which the generic host makes sense.
最后,在第 30.6 节中,我将讨论一些原因,您可能仍然选择在 .NET 7 应用程序中使用通用主机而不是最小托管。在大多数情况下,我建议对 WebApplication 使用最小托管,但在某些情况下,通用主机是有意义的。

30.1 Separating concerns between two files‌

30.1 在两个文件之间分离关注点

As you’ve seen throughout this book, the standard way to create an ASP.NET Core application in .NET 7 is with the WebApplicationBuilder and WebApplication classes inside Program.cs, using top-level statements. Before .NET 6, however, ASP.NET Core used a different approach, which you can still use in .NET 7 if you wish.‌‌
正如您在本书中所看到的,在 .NET 7 中创建 ASP.NET Core 应用程序的标准方法是使用顶级语句在 Program.cs 中使用 WebApplicationBuilder 和 WebApplication 类。但是,在 .NET 6 之前,ASP.NET Core 使用不同的方法,如果您愿意,您仍然可以在 .NET 7 中使用该方法。

This approach typically uses a traditional static void Main() entry point (although top-level statements are supported) and splits its bootstrapping code across two files, as shown in figure 30.1:
这种方法通常使用传统的静态 void Main() 入口点(尽管支持顶级语句),并将其引导代码拆分为两个文件,如图 30.1 所示:

• Program.cs—This contains the entry point for the application, which bootstraps a host object. This is where you configure the infrastructure of your application, such as Kestrel, integration with Internet Information Services (IIS), and configuration sources.
Program.cs - 包含应用程序的入口点,用于引导主机对象。您可以在此处配置应用程序的基础结构,例如 Kestrel、与 Internet Information Services (IIS) 的集成以及配置源。

• Startup.cs—The Startup class is where you configure your dependency injection (DI) container, your middleware pipeline, and your application’s endpoints.
Startup.cs - Startup 类用于配置依赖关系注入 (DI) 容器、中间件管道和应用程序的端点。

alt text

Figure 30.1 The different responsibilities of the Program and Startup classes in an ASP.NET Core app that uses the generic host instead of WebApplication
图 30.1 使用泛型主机而不是 WebApplication 的 ASP.NET Core 应用程序中 Program 和 Startup 类的不同职责

We’ll look at each of these files in turn in section 30.2 and 30.3 to see how they might look for a typical Razor Pages app. I discuss the generic host at the center of this setup and compare the approach with the newer WebApplication APIs you’ve used so far throughout the book.
我们将在第 30.2 节和第 30.3 节中依次查看这些文件,以了解它们在典型 Razor Pages 应用程序中的外观。我将讨论此设置中心的通用主机,并将该方法与您在本书中到目前为止使用的较新的 WebApplication API 进行比较。

30.2 The Program class: Building a Web Host‌

30.2 Program 类:构建 Web 主机

All ASP.NET Core apps are fundamentally console applications. With the Startup-based hosting model, the Main entry point builds and runs an IHost instance, as shown in the following listing, which shows a typical Program.cs file. The IHost is the core of your ASP.NET Core application: it contains the HTTP server (Kestrel) for handling requests, along with all the necessary services and configuration to generate responses.‌‌
所有 ASP.NET Core 应用程序基本上都是控制台应用程序。使用基于启动的托管模型,Main 入口点构建并运行 IHost 实例,如下面的清单所示,其中显示了一个典型的 Program.cs 文件。IHost 是 ASP.NET Core 应用程序的核心:它包含用于处理请求的 HTTP 服务器 (Kestrel),以及用于生成响应的所有必要服务和配置。

Listing 30.1 The Program.cs file configures and runs an IHost
清单 30.1 Program.cs 文件配置并运行 IHost

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args) ❶
.Build() ❷
.Run(); ❸
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) ❹
.ConfigureWebHostDefaults(webBuilder => ❺
{
webBuilder.UseStartup<Startup>(); ❻
});
}

❶ Creates an IHostBuilder using the CreateHostBuilder method
使用 CreateHostBuilder 方法创建 IHostBuilder

❷ Builds and returns an instance of IHost from the IHostBuilder
从 IHostBuilder构建并返回 IHost 的实例

❸ Runs the IHost and starts listening for requests and generating responses
运行 IHost 并开始侦听请求并生成响应

❹ Creates an IHostBuilder using the default configuration
使用默认配置创建 IHostBuilder

❺ Configures the application to use Kestrel and listen to HTTP requests
将应用程序配置为使用 Kestrel 并侦听 HTTP 请求

❻ The Startup class defines most of your application’s configuration.
Startup 类定义了应用程序的大部分配置。

The Main function contains all the basic initialization code required to create a web server and to start listening for requests. It uses an IHostBuilder, created by the call to CreateDefaultBuilder, to define how the generic IHost is configured, before instantiating the IHost with a call to Build().
Main 函数包含创建 Web 服务器和开始侦听请求所需的所有基本初始化代码。它使用通过调用 CreateDefaultBuilder 创建的 IHostBuilder 来定义泛型 IHost 的配置方式,然后再通过调用 Build() 实例化 IHost。

TIP The IHost object represents your built application. The WebApplication type you’ve used throughout the book also implements IHost.
提示:IHost 对象表示您构建的应用程序。您在本书中介绍的 WebApplication 类型也实现了 IHost。

Much of your app’s configuration takes place in the IHostBuilder created by the call to CreateDefaultBuilder, but it delegates some responsibility to a separate class, Startup. The Startup class referenced in the generic UseStartup<> method is where you configure your app’s services and define your middleware pipeline.
应用程序的大部分配置都发生在由调用 CreateDefaultBuilder 创建的 IHostBuilder 中,但它将一些责任委托给单独的类 Startup。泛型 UseStartup<> 方法中引用的 Startup 类是您配置应用程序服务和定义中间件管道的位置。

NOTE The code to build the IHostBuilder is extracted to a helper method called CreateHostBuilder. The name of this method is historically important, as it was used implicitly by tooling such as the Entity Framework Core (EF Core) tools, as I discuss in section 30.5.‌
注意:用于构建 IHostBuilder 的代码被提取到名为 CreateHostBuilder 的帮助程序方法中。此方法的名称在历史上很重要,因为它由 Entity Framework Core (EF Core) 工具等工具隐式使用,如我在第 30.5 节中讨论的那样。

You may be wondering why you need two classes for configuration: Program and Startup. Why not include all your app’s configuration in one class or the other? The idea is to separate code that changes often from code that rarely changes.
您可能想知道为什么需要两个类进行配置:Program 和 Startup。为什么不将应用程序的所有配置都包含在一个类或另一个类中呢?这个想法是将经常更改的代码与很少更改的代码分开。

The Program class for two different ASP.NET Core applications typically look similar, but the Startup classes often differ significantly (though they all follow the same basic pattern, as you’ll see in section 30.3). You’ll rarely find that you need to modify Program as your application grows, whereas you’ll normally update Startup whenever you add additional features. For example, if you add a new NuGet dependency to your project, you’ll normally need to update Startup to make use of it.
两个不同的 ASP.NET Core 应用程序的 Program 类通常看起来相似,但 Startup 类通常有很大不同(尽管它们都遵循相同的基本模式,如您将在第 30.3 节中看到的那样)。您很少会发现需要随着应用程序的增长而修改 Program,而您通常会在添加其他功能时更新 Startup。例如,如果向项目添加新的 NuGet 依赖项,则通常需要更新 Startup 才能使用它。

The Program class is where a lot of app configuration takes place, but this is mostly hidden inside the Host.CreateDefaultBuilder method.
Program 类是进行大量应用程序配置的地方,但这主要隐藏在 Host.CreateDefaultBuilder 方法中。

CreateDefaultBuilder is a static helper method that simplifies the bootstrapping of your app by creating an IHostBuilder with some common configuration. This is similar to the way you’ve used WebApplication.CreateDefaultBuilder() throughout the book.
CreateDefaultBuilder 是一种静态帮助程序方法,它通过创建具有一些常见配置的 IHostBuilder 来简化应用程序的启动。这类似于您在整本书中使用 WebApplication.CreateDefaultBuilder() 的方式。

NOTE You can create custom HostBuilder instances if you want to customize the default setup and create a completely custom IHost instance, as you’ll see in section 30.4. This is different from WebApplicationBuilder, which always uses the same defaults.
注意:如果您想自定义默认设置并创建完全自定义的 IHost 实例,则可以创建自定义 HostBuilder 实例,如第 30.4 节所示。这与 WebApplicationBuilder 不同,后者始终使用相同的默认值。

The other helper method used by default is ConfigureWebHostDefaults. This uses a WebHostBuilder object to configure Kestrel to listen for HTTP requests.‌
默认情况下使用的另一个帮助程序方法是 ConfigureWebHostDefaults。这使用 WebHostBuilder 对象将 Kestrel 配置为侦听 HTTP 请求。

Creating services with the generic host
使用通用主机创建服务

It might seem strange that you must call ConfigureWebHostDefaults as well as CreateDefaultBuilder. Couldn’t we have one method? Isn’t handling HTTP requests the whole point of ASP.NET Core?
您必须调用 ConfigureWebHostDefaults 和 CreateDefaultBuilder 似乎很奇怪。我们不能有一种方法吗?处理 HTTP 请求不是 ASP.NET Core 的全部意义所在吗?

Well, yes and no! ASP.NET Core 3.0 introduced the concept of a generic host. This allows you to use much of the same framework as ASP.NET Core applications to write non-HTTP applications. These apps can run as console apps or can be installed as Windows services (or as systemd daemons in Linux) to run background tasks or read from message queues, for example.
嗯,是的,也不是!ASP.NET Core 3.0 引入了通用主机的概念。这允许您使用与 ASP.NET Core 应用程序相同的框架来编写非 HTTP 应用程序。例如,这些应用程序可以作为控制台应用程序运行,也可以作为 Windows 服务(或 Linux 中的 systemd 守护程序)安装,以运行后台任务或从消息队列中读取数据。

Kestrel and the web framework of ASP.NET Core build on top of the generic host functionality introduced in ASP.NET Core 3.0. To configure a typical ASP.NET Core app, you configure the generic host features that are common across all apps—features such as configuration, logging, and dependency services. For web applications, you then also configure the services, such as Kestrel, that are necessary to handle web requests. In chapter 34 you’ll see how to build applications using the generic host to run scheduled tasks and build background services.
Kestrel 和 ASP.NET Core 的 Web 框架构建在 ASP.NET Core 3.0 中引入的通用主机功能之上。要配置典型的 ASP.NET Core 应用程序,您需要配置所有应用程序中通用的通用主机功能,例如配置、日志记录和依赖项服务等功能。对于 Web 应用程序,您还可以配置处理 Web 请求所需的服务,例如 Kestrel。在第 34 章中,您将看到如何使用通用主机构建应用程序来运行计划任务和构建后台服务。

Even in .NET 7, WebApplication and WebApplicationBuilder use the generic host behind the scenes. You can read more about the evolution of ASP.NET Core’s bootstrapping code and the relationship between IHost and WebApplication on my blog at https://andrewlock.net/exploring-dotnet-6-part-2-comparing-webapplicationbuilder-to-the-generic-host/.
即使在 .NET 7 中,WebApplication 和 WebApplicationBuilder 也在后台使用通用主机。您可以在我的博客 https://andrewlock.net/exploring-dotnet-6-part-2-comparing-webapplicationbuilder-to-the-generic-host/ 上阅读有关 ASP.NET Core 引导代码的演变以及 IHost 和 WebApplication 之间的关系的更多信息。

Once the configuration of the IHostBuilder is complete, the call to Build produces the IHost instance, but the application still isn’t handling HTTP requests yet. It’s the call to Run() that starts the HTTP server listening. At this point, your application is fully operational and can respond to its first request from a remote browser.
IHostBuilder 的配置完成后,对 Build 的调用将生成 IHost 实例,但应用程序仍未处理 HTTP 请求。对 Run() 的调用将启动 HTTP 服务器侦听。此时,您的应用程序已完全运行,并且可以响应来自远程浏览器的第一个请求。

30.3 The Startup class: Configuring your application‌

30.3 Startup 类:配置应用程序

As you’ve seen, Program is responsible for configuring a lot of the infrastructure for your app, but you configure some of your app’s behavior in Startup. The Startup class is responsible for configuring two main aspects of your application:
如你所见,Program 负责为应用程序配置大量基础结构,但你在 Startup 中配置应用程序的一些行为。Startup 类负责配置应用程序的两个主要方面:

• DI container service registration
DI 集装箱服务注册

• Middleware configuration and mapping of endpoints
中间件配置和端点映射

You configure each of these aspects in its own method in Startup: service registration in ConfigureServices and middleware/endpoint configuration in Configure. A typical outline of Startup is shown in the following listing.
您可以在 Startup 中在其自己的方法中配置每个方面:ConfigureServices 中的服务注册和 Configure 中的中间件/终端节点配置。下面的清单显示了 Startup 的典型轮廓。

Listing 30.2 An outline of Startup.cs showing how each aspect is configured
清单 30.2 Startup.cs概述,显示每个 aspect 是如何配置的

public class Startup
{
public void ConfigureServices(IServiceCollection services) ❶
{
// method details
}
public void Configure(IApplicationBuilder app) ❷
{
// method details
}
}

❶ Configures services by registering them with the IServiceCollection
通过在 IServiceCollection中注册服务来配置服务
❷ Configures the middleware pipeline for handling HTTP requests
配置用于处理 HTTP 请求的中间件管道

The IHostBuilder created in Program automatically calls ConfigureServices and then Configure, as shown in figure 30.2. Each call configures a different part of your application, making it available for subsequent method calls. Any services registered in the ConfigureServices method are available to the Configure method. Once configuration is complete, you create an IHost by calling Build() on the IHostBuilder.
在 Program 中创建的 IHostBuilder 会自动调用 ConfigureServices,然后调用 Configure,如图 30.2 所示。每次调用都会配置应用程序的不同部分,使其可用于后续方法调用。在 ConfigureServices 方法中注册的任何服务都可用于 Configure 方法。配置完成后,您可以通过在 IHostBuilder 上调用 Build() 来创建 IHost。

alt text

Figure 30.2 The IHostBuilder is created in Program.cs and calls methods on Startup to configure the application’s services and middleware pipeline. Once configuration is complete, the IHost is created by calling Build() on the IHostBuilder.
图 30.2 IHostBuilder 是在 Program.cs中创建的,并在启动时调用方法来配置应用程序的服务和中间件管道。配置完成后,通过在 IHostBuilder 上调用 Build() 来创建 IHost。

An interesting point about the Startup class is that it doesn’t implement an interface as such. Instead, the methods are invoked by using reflection to find methods with the predefined names of Configure and ConfigureServices. This makes the class more flexible and enables you to modify the signature of the Configure method to inject any services you registered in ConfigureServices using DI.
关于 Startup 类的一个有趣之处在于,它没有实现这样的接口。相反,通过使用反射来查找具有预定义名称 Configure 和 ConfigureServices 的方法,从而调用这些方法。这使得该类更加灵活,并使您能够修改 Configure 方法的签名,以注入您使用 DI 在 ConfigureServices 中注册的任何服务。

TIP If you’re not a fan of the flexible reflection approach, you can implement the IStartup interface or derive from the StartupBase class, which provide the method signatures shown previously in listing 30.2. If you take this approach, you won’t be able to use DI to inject services into the Configure() method.‌‌
提示:如果您不喜欢灵活的反射方法,则可以实现 IStartup 接口或从 StartupBase 类派生,这些类提供前面清单 30.2 中所示的方法签名。如果采用此方法,则无法使用 DI 将服务注入 Configure() 方法。

ConfigureServices is where you add all your required and custom services to the DI container, exactly as you do with WebApplicationBuilder.Services in a typical .NET 7 ASP.NET Core app. The following listing shows how you might configure all the services for the Razor Pages recipe app you’ve seen throughout this book. This listing also shows how you can access the IConfiguration for your app: by injecting into the Startup constructor. You’ll see how to customize your app’s configuration in section 30.4.
在 ConfigureServices 中,您可以将所有必需的自定义服务添加到 DI 容器中,就像在典型的 .NET 7 ASP.NET Core 应用程序中使用 WebApplicationBuilder.Services 一样。以下清单显示了如何为本书中介绍的 Razor Pages 配方应用程序配置所有服务。此清单还显示了如何访问应用程序的 IConfiguration:通过注入 Startup 构造函数。您将在 Section 30.4 中看到如何自定义应用程序的配置。

Listing 30.3 Registering services with DI in ConfigureServices
清单 30.3 在 ConfigureServices 中向 DI 注册服务

public class Startup
{
public IConfiguration Configuration { get; } ❶
public Startup(IConfiguration configuration) ❶
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services) ❷
{
var conn = Configuration.GetConnectionString("DefaultConnection");
services.AddDbContext<AppDbContext>(options => ❸
options.UseSqlite(conn)); ❸
services.AddDefaultIdentity<ApplicationUser>(options => ❸
options.SignIn.RequireConfirmedAccount = true) ❸
.AddEntityFrameworkStores<AppDbContext>(); ❸
services.AddScoped<RecipeService>(); ❹
services.AddRazorPages(); ❺
services.AddScoped<IAuthorizationHandler, IsRecipeOwnerHandler>();
services.AddAuthorizationBuilder()
.AddPolicy("CanManageRecipe",
p => p.AddRequirements(new IsRecipeOwnerRequirement()));
}
public void Configure(IApplicationBuilder app) => { /* Not shown */ }
}

❶ The IConfiguration for the app is injected into the constructor.
应用程序的 IConfiguration 被注入到构造函数中。

❷ You must register your services against the provided IServiceCollection.
您必须针对提供的 IServiceCollection 注册您的服务。

❸ Registers all the EF Core and ASP.NET Core Identity services
注册所有 EF Core 和 ASP.NET Core Identity 服务

❹ Registers the custom service implementations
注册自定义服务实现

❺ Registers the framework services
注册框架服务

After configuring all your services, you need to set up your middleware pipeline and map your endpoints. The process is similar to configuring your middleware pipeline using WebApplication:
配置完所有服务后,您需要设置中间件管道并映射终端节点。该过程类似于使用 WebApplication 配置中间件管道:

• You add middleware to the pipeline by calling Use extension methods on an IApplicationBuilder instance.
通过在 IApplicationBuilder 实例上调用 Use
扩展方法,将中间件添加到管道中。

• The order in which you add the middleware to the pipeline is important and defines the final pipeline order.
将中间件添加到管道的顺序非常重要,它定义了最终的管道顺序。

• You can add middleware conditionally based on the environment.
您可以根据环境有条件地添加中间件。

However, there are some important differences between the WebApplication approach you’ve seen so far and the Startup approach:
但是,到目前为止,您看到的 WebApplication 方法与 Startup 方法之间存在一些重要差异:

• The IWebHostEnvironment for your app is exposed directly on WebApplication.Environment. To access this information inside Startup, you must inject it into the constructor or the Configure method using DI.
应用程序的 IWebHostEnvironment 直接在 WebApplication.Environment 上公开。要在 Startup 中访问此信息,您必须使用 DI 将其注入到构造函数或 Configure 方法中。

• As you saw in chapter 4, WebApplication automatically adds a lot of middleware to your pipeline, such as routing middleware, endpoint middleware, and the authentication middleware. You must add this middleware manually when using the Startup approach.
如第 4 章所示,WebApplication 会自动向管道中添加大量中间件,例如路由中间件、端点中间件和身份验证中间件。使用 Startup 方法时,必须手动添加此中间件。

• WebApplication implements both IApplicationBuilder and IEndpointRouteBuilder, so you can add endpoints directly to WebApplication, by calling MapGet() or MapRazorPages(), for example.When using the Startup approach, you must call UseEndpoints() and map all your endpoints in a lambda method instead.
WebApplication 同时实现 IApplicationBuilder 和 IEndpointRouteBuilder,因此您可以通过调用 MapGet() 或 MapRazorPages() 等方式将端点直接添加到 WebApplication。使用 Startup 方法时,您必须调用 UseEndpoints() 并改为在 lambda 方法中映射所有终端节点。

• The Configure method is not async, so it’s cumbersome to do async tasks. By contrast, when using WebApplication, you’re free to use async methods between any of your general bootstrapping code.
Configure 方法不是异步的,因此执行异步任务很麻烦。相比之下,在使用 WebApplication 时,您可以在任何常规引导代码之间自由使用异步方法。

Despite these caveats, in many cases your Startup.Configure method will look almost identical to the way you configure the pipeline on WebApplication. The following listing shows how the Configure() method for the Razor Pages recipe app might look.‌尽管有这些注意事项,但在许多情况下,您的 Startup.Configure 方法看起来与您在 WebApplication 上配置管道的方式几乎相同。以下清单显示了 Razor Pages 配方应用的 Configure() 方法的外观。

Listing 30.4 Startup.Configure() for a Razor Pages application
列表 30.4 Razor Pages 应用程序的 Startup.Configure()

public class Startup
{
public void Configure(
IApplicationBuilder app, ❶
IWebHostEnvironment env) ❷
{
if (env.IsDevelopment()) ❸
{
app.UseDeveloperExceptionPage(); ❹
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting(); ❺
app.UseAuthentication();
app.UseAuthorization(); ❻
app.UseEndpoints(endpoints => ❼
{
endpoints.MapRazorPages(); ❽
});
}
}

❶ IApplicationBuilder is used to build the middleware pipeline.
IApplicationBuilder 用于构建中间件管道。

❷ Other services can be accepted as parameters.
其他服务可以作为参数接受。

❸ Different behavior when in development or production
开发或生产时的行为不同

❹ WebApplication adds this automatically. You must explicitly add it when using Startup.
WebApplication 会自动添加此内容。您必须在使用Startup 时显式添加它。

❺ Similarly, you must explicitly call UseRouting.
同样,您必须显式调用 UseRouting。

❻ Must always be placed between the call to UseRouting and UseEndpoints
必须始终放置在对 UseRouting 和 UseEndpoints的调用之间

❼ Adds the endpoint middleware, which executes the endpoints
添加执行终结点的终结点中间件

❽ Maps the Razor Pages endpoints
映射 Razor Pages 终结点

In this example, the IWebHostEnvironment object is injected into the Configure() method using DI so that you can configure the middleware pipeline differently in development and production. In this case, we add the DeveloperExceptionPageMiddleware to the pipeline when we’re running in development.‌
在此示例中,使用 DI 将 IWebHostEnvironment 对象注入到 Configure() 方法中,以便您可以在开发和生产中以不同的方式配置中间件管道。在本例中,我们在开发中运行时将 DeveloperExceptionPageMiddleware 添加到管道中。

NOTE Remember that WebApplication adds this middleware automatically, but with Startup you must add it manually. The same goes for all the other automatically added middleware.
注意:请记住,WebApplication 会自动添加此中间件,但使用 Startup 时,您必须手动添加它。所有其他自动添加的 middleware 也是如此。

After adding all the middleware to the pipeline, you come to the UseEndpoints() call, which adds the EndpointMiddleware to the pipeline. When you use WebApplication, you rarely need to call this, as WebApplication automatically adds it at the end of the pipeline, but when you use Startup, you should add it at the end of your pipeline.
将所有中间件添加到管道后,您将转到 UseEndpoints() 调用,该调用将 EndpointMiddleware 添加到管道中。当您使用 WebApplication 时,您很少需要调用它,因为 WebApplication 会自动将其添加到管道的末尾,但是当您使用 Startup 时,您应该将其添加到管道的末尾。

Note as well that the call to UseEndpoints() is where you define all the endpoints in your application. Whether they’re Razor Pages, Model-View-Controller (MVC) controllers, or minimal APIs, you must register them in the UseEndpoints() lambda.
另请注意,对 UseEndpoints() 的调用是定义应用程序中的所有终结点的位置。无论它们是 Razor Pages、Model-View-Controller (MVC) 控制器还是最小 API,都必须在 UseEndpoints() lambda 中注册它们。

NOTE Endpoints must be registered inside the call to UseEndpoints() using the IEndpointRouteBuilder instance from the lambda method.
注意:必须使用 lambda 方法中的 IEndpointRouteBuilder 实例在对 UseEndpoints() 的调用中注册终端节点。

Other than the noted differences, moving your service, middleware, and endpoint configuration between a Startup-based approach and WebApplication should be relatively simple, which may lead you to wonder whether there’s any good reason to choose the Startup approach over WebApplication. As always, the answer is “It depends,” but one possible reason is so that you can customize your IHostBuilder.
除了上述差异之外,在基于 Startup 的方法和 WebApplication 之间移动服务、中间件和端点配置应该相对简单,这可能会让您怀疑是否有任何充分的理由选择 Startup 方法而不是 WebApplication。与往常一样,答案是“视情况而定”,但一个可能的原因是您可以自定义 IHostBuilder。

30.4 Creating a custom IHostBuilder‌

30.4 创建自定义 IHostBuilder

As you saw in section 30.2, the default way to work with a Startup class in ASP.NET Core is to use the Host.CreateDefaultBuilder() method. This opinionated helper method sets up many defaults for your app. It is analogous to the WebApplication‌.CreateBuilder() method in that way.
如您在第 30.2 节中所见,在 ASP.NET Core 中使用 Startup 类的默认方法是使用 Host.CreateDefaultBuilder() 方法。这个固执己见的 helper 方法为您的应用程序设置了许多默认值。它类似于 WebApplication 。CreateBuilder() 方法。

However, you don’t have to use the CreateDefaultBuilder method to create an IHostBuilder instance: you can directly create a HostBuilder instance and customize it from scratch if you prefer. Before you start doing that, though, it’s worth seeing some of the things the CreateDefaultBuilder method gives you and what they’re used for. You may then consider customizing the default HostBuilder instance instead of creating a completely bespoke instance.‌
但是,您不必使用 CreateDefaultBuilder 方法创建 IHostBuilder 实例:如果您愿意,可以直接创建 HostBuilder 实例并从头开始自定义它。不过,在开始执行此作之前,有必要了解 CreateDefaultBuilder 方法为您提供的一些功能以及它们的用途。然后,您可以考虑自定义默认的 HostBuilder 实例,而不是创建完全定制的实例。

NOTE You can use Host.CreateDefaultBuilder() in .NET 7 even if you’re not using ASP.NET Core by installing the Microsoft.Extensions.Hosting package. You’ll learn how to create non-HTTP applications using the generic host in chapter 34.
注意:即使您没有使用 ASP.NET Core,也可以通过安装 Microsoft.Extensions.Hosting 包在 .NET 7 中使用 Host.CreateDefaultBuilder()。您将在第 34 章中学习如何使用通用主机创建非 HTTP 应用程序。

The defaults chosen by CreateDefaultBuilder are ideal when you’re initially setting up an app, but as your application grows, you may find you need to break it apart and tinker with some of the internals. The following listing shows a rough overview of the CreateDefaultBuilder method, so you can see how the HostBuilder is constructed. It’s not exhaustive or complete, but it should give you an idea of the amount of work the CreateDefaultBuilder method does for you!
CreateDefaultBuilder 选择的默认值在您最初设置应用程序时是理想的,但随着应用程序的增长,您可能会发现需要将其分解并修改一些内部结构。下面的清单显示了 CreateDefaultBuilder 方法的粗略概述,因此你可以看到 HostBuilder 是如何构造的。它并不详尽或完整,但它应该让您了解 CreateDefaultBuilder 方法为您完成的工作量!

Listing 30.5 The Host.CreateDefaultBuilder method
清单 30.5 Host.CreateDefaultBuilder 方法

public static IHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new HostBuilder() ❶
.UseContentRoot(Directory.GetCurrentDirectory()) ❷
.ConfigureHostConfiguration(IConfigurationBuilder config => ❸
{ ❸
config.AddEnvironmentVariables("DOTNET_"); ❸
config.AddCommandLine(args); ❸
}) ❸
.ConfigureAppConfiguration((hostingContext, config) => ❹
{ ❹
IHostEnvironment env = hostingContext.HostingEnvironment; ❹
config ❹
.AddJsonFile("appsettings.json") ❹
.AddJsonFile($"appsettings.{env.EnvironmentName}.json"); ❹
if (env.IsDevelopment()) ❹
{ ❹
config.AddUserSecrets(); ❹
} ❹
config ❹
.AddEnvironmentVariables() ❹
.AddCommandLine(); ❹
}) ❹
.ConfigureLogging((hostingContext, logging) => ❺
{ ❺
logging.AddConfiguration( ❺
hostingContext.Configuration.GetSection("Logging")); ❺
logging.AddConsole(); ❺
logging.AddDebug(); ❺
logging.AddEventSourceLogger(); ❺
logging.AddEventLog(); ❺
}) ❺
.UseDefaultServiceProvider((context, options) => ❻
{ ❻
var isDevelopment = context.HostingEnvironment ❻
.IsDevelopment(); ❻
options.ValidateScopes = isDevelopment; ❻
options.ValidateOnBuild = isDevelopment; ❻
}); ❻
return builder; ❼
}

❶ Creates an instance of HostBuilder
创建 HostBuilder的实例

❷ The content root defines the directory where configuration files can be found.
内容根定义可以找到配置文件的目录。

❸ Configures hosting settings such as determining the hosting environment
配置托管设置,例如确定托管环境

❹ Configures application settings
配置应用程序设置

❺ Sets up the logging infrastructure
设置日志记录基础设施

❻ Configures the DI container, optionally enabling verification settings
配置 DI 容器,可选择启用验证设置

❼ Returns HostBuilder for further configuration by calling extra methods before calling Build()
通过在调用 Build() 之前调用额外的方法返回 HostBuilder 以进行进一步配置

The first method called on HostBuilder is UseContentRoot(). This tells the application in which directory it can find any configuration or Razor files it needs later. This is typically the folder in which the application is running, hence the call to GetCurrentDirectory.
在 HostBuilder 上调用的第一个方法是 UseContentRoot()。这会告知应用程序稍后可以在哪个目录中找到所需的任何配置或 Razor 文件。这通常是运行应用程序的文件夹,因此调用 GetCurrentDirectory。

TIP Remember that ContentRoot is not where you store static files that the browser can access directly. That’s the WebRoot, typically wwwroot.
提示:请记住,ContentRoot 不是存储浏览器可以直接访问的静态文件的位置。这就是 WebRoot,通常是 wwwroot。

The ConfigureHostingConfiguration() method is where your application determines which HostingEnvironment it’s currently running in. The framework looks for environment variables that start with "DOTNET_" (such as the DOTNETENVIRONMENT variable you learned about in chapter 10) and command-line arguments to determine whether it’s running in a development or production environment. This is used to populate the IWebHostEnvironment object that’s used throughout your app.‌
ConfigureHostingConfiguration() 方法是应用程序确定它当前在哪个 HostingEnvironment 中运行的位置。框架会查找以 “DOTNET
” 开头的环境变量(例如您在第 10 章中学到的 DOTNET_ENVIRONMENT 变量)和命令行参数,以确定它是在开发环境中运行还是在生产环境中运行。这用于填充整个应用程序中使用的 IWebHostEnvironment 对象。

The ConfigureAppConfiguration() method is where you configure the main IConfiguration object for your app, populating it from appsettings.json files, environment variables, and User Secrets, for example. The default builder populates the configuration using all the sources shown in listing 30.5, which is similar to the configuration WebApplicationBuilder uses.‌
ConfigureAppConfiguration() 方法是为应用程序配置主 IConfiguration 对象的地方,例如,从 appsettings.json 文件、环境变量和用户密钥中填充它。默认构建器使用清单 30.5 中所示的所有源填充配置,这类似于 WebApplicationBuilder 使用的配置。

TIP There are some important differences in how the IConfiguration object is built using the default builder and the approach used by WebApplicationBuilder. You can read about these differences on my blog at http://mng.bz/e11V.
提示:使用默认生成器和 WebApplicationBuilder 使用的方法构建 IConfiguration 对象的方式存在一些重要差异。您可以在我的博客 http://mng.bz/e11V 上阅读这些差异。

Next up after app configuration comes ConfigureLogging(). ConfigureLogging is where you specify the logging settings and providers for your application, which you learned about in chapter 26. In addition to setting up the default ILoggerProviders, this method sets up log filtering, using the IConfiguration prepared in ConfigureAppConfiguration().
接下来,在应用程序配置之后是 ConfigureLogging()。ConfigureLogging 是指定应用程序的日志记录设置和提供程序的地方,您在第 26 章中了解了这一点。除了设置默认 ILoggerProviders 之外,此方法还使用 ConfigureAppConfiguration() 中准备的 IConfiguration 设置日志筛选。

The last method call shown in listing 30.5, UseDefaultServiceProvider, configures your app to use the built-in DI container. It also sets the ValidateScopes and ValidateOnBuild options based on the current HostingEnvironment. This ensures that when running the application in the development environment, the container automatically checks for captured dependencies, which you learned about in chapter 9.‌‌
清单 30.5 中显示的最后一个方法调用 UseDefaultServiceProvider 将您的应用程序配置为使用内置的 DI 容器。它还根据当前 HostingEnvironment 设置 ValidateScopes 和 ValidateOnBuild 选项。这可确保在开发环境中运行应用程序时,容器会自动检查捕获的依赖项,您在第 9 章中学到了这一点。

As you can see, CreateDefaultBuilder does a lot for you. In many cases, these defaults are exactly what you need, but if they’re not, the default builder is optional. You could call new HostBuilder() and start customizing it from there, but you’d need to set up everything that CreateHostBuilder does: logging, hosting configuration, and service provider configuration, as well as your app configuration.
如您所见,CreateDefaultBuilder 为您做了很多事情。在许多情况下,这些默认值正是您所需要的,但如果它们不是,则默认构建器是可选的。您可以调用新的 HostBuilder() 并从那里开始自定义它,但您需要设置 CreateHostBuilder 执行的所有作:日志记录、托管配置和服务提供商配置,以及您的应用程序配置。

An alternative approach is to layer additional configuration on top of the existing defaults. In the following listing, I show how to add a Seq logging provider to the configured providers using ConfigureLogging(), as well as how to reconfigure the app configuration to load only from the appsettings.json provider by clearing the default providers.
另一种方法是在现有默认值之上对其他配置进行分层。在下面的清单中,我将展示如何使用 ConfigureLogging() 将 Seq 日志记录提供程序添加到配置的提供程序中,以及如何通过清除默认提供程序来重新配置应用程序配置以仅从 appsettings.json 提供程序加载。

Listing 30.6 Customizing the default HostBuilder
清单 30.6 自定义默认的 HostBuilder

public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logBuilder => logBuilder.AddSeq()) ❶
.ConfigureAppConfiguration((hostContext, config) => ❷
{
config.Sources.Clear(); ❸
config.AddJsonFile("appsettings.json"); ❹
}
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

❶ Adds the Seq logging provider to the configuration
将 Seq 日志记录提供程序添加到配置中

❷ HostBuilder provides a hosting context and an instance of ConfigurationBuilder.
HostBuilder 提供托管上下文和 ConfigurationBuilder 实例。

❸ Clears the providers configured by default in CreateDefaultBuilder
清除 CreateDefaultBuilder中默认配置的提供程序

❹ Adds a JSON configuration provider, providing the filename of the configuration file
添加 JSON 配置提供程序,提供配置文件的文件名

A new HostBuilder is created in CreateDefaultBuilder() and executes all the configuration methods you saw in listing 30.5. Next, the HostBuilder invokes the extra ConfigureLogging() and ConfigureAppConfiguration() methods added in listing 30.6. You can call any of the other configuration methods on HostBuilder to further customize the instance before calling Build().‌
在 CreateDefaultBuilder() 中创建一个新的 HostBuilder,并执行您在清单 30.5 中看到的所有配置方法。接下来,HostBuilder 调用清单 30.6 中添加的额外 ConfigureLogging() 和 ConfigureAppConfiguration() 方法。在调用 Build() 之前,您可以在 HostBuilder 上调用任何其他配置方法以进一步自定义实例。

NOTE Each call to a Configure() method on HostBuilder adds an extra configuration function to the setup code; these calls don’t replace existing Configure () calls. The configuration methods are executed in the same order in which they’re added to the HostBuilder, so they execute after the CreateDefaultBuilder() configuration methods.
注意:对 HostBuilder 上的 Configure() 方法的每次调用都会向设置代码添加一个额外的配置函数;这些调用不会替换现有的 Configure () 调用。配置方法的执行顺序与添加到 HostBuilder 的顺序相同,因此它们在 CreateDefaultBuilder() 配置方法之后执行。

One of the criticisms of early ASP.NET Core apps was that they were quite complex to understand when you’re getting started, and after working your way through this chapter, you might well be able to see why! In the next section we compare the generic host and Startup approach with the newer minimal hosting WebApplication approach and discuss when you might want to use one over the other.‌
对早期 ASP.NET Core 应用程序的批评之一是,当您开始时,它们非常难以理解,在完成本章之后,您很可能能够明白为什么!在下一节中,我们将通用 host 和 Startup 方法与较新的最小托管 WebApplication 方法进行比较,并讨论何时可能需要使用其中一种方法。

30.5 Understanding the complexity of the generic host‌

30.5 了解泛型主机的复杂性

Before .NET 6, all ASP.NET Core apps used the generic host and Startup approach. Many people liked the consistent structure this added, but it also has some drawbacks and complexity:
在 .NET 6 之前,所有 ASP.NET Core 应用程序都使用通用主机和启动方法。许多人喜欢它添加的一致结构,但它也有一些缺点和复杂性:

• Configuration is split between two files.
配置在两个文件之间拆分。

• The separation between Program.cs and Startup is somewhat arbitrary.
Program.cs 和 Startup 之间的划分有些武断。

• The generic IHostBuilder exposes newcomers to legacy decisions.
通用 IHostBuilder 使新人能够接触到传统决策。

• The lambda-based configuration can be hard to follow and reason about.
基于 lambda 的配置可能难以遵循和推理。

• The pattern-based conventions of Startup may be hard to discover.
Startup 的基于模式的约定可能很难发现。

• Tooling historically relies on your defining a CreateHostBuilder method in Program.cs.
工具以前依赖于您在 Program.cs 中定义 CreateHostBuilder 方法。

I’ll address each of these problems in turn and afterward discuss how WebApplication attempted to improve the situation.
我将依次解决这些问题中的每一个,然后讨论 WebApplication 如何尝试改善这种情况。

Points 1 and 2 in the preceding list deal with the separation between Program.cs and Startup. As you saw in section 30.1, theoretically the intention is that Program.cs defines the host and rarely changes, whereas Startup defines the app features (services, middleware, and endpoints). This seems like a reasonable decision, but one inevitable downside is that you need to flick back and forth between at least two files to understand all your bootstrapping code.
前面列表中的第 1 点和第 2 点涉及 Program.cs 和 Startup 之间的分离。正如您在 Section 30.1 中看到的,理论上的目的是 Program.cs 定义主机并且很少更改,而 Startup 定义应用程序功能(服务、中间件和端点)。这似乎是一个合理的决定,但一个不可避免的缺点是,您需要在至少两个文件之间来回切换才能理解所有引导代码。

On top of that, you don’t necessarily need to stick to these conventions. You can register services in Program.cs by calling HostBuilder.ConfigureServices(), for example, or register middleware using WebHostBuilder.Configure(). This is relatively rare but not entirely unheard-of, further blurring the lines between the files.
最重要的是,您不一定需要遵守这些约定。例如,您可以通过调用 HostBuilder.ConfigureServices() 在 Program.cs 中注册服务,或使用 WebHostBuilder.Configure() 注册中间件。这种情况相对罕见,但并非完全闻所未闻,进一步模糊了文件之间的界限。

Point 3 relates to the fact that you must call ConfigureWebHostDefaults() (which uses an IWebHostBuilder) to set up Kestrel and register your Startup class. This level of indirection (and the introduction of another builder type) is a remnant of decisions harking back to ASP.NET Core 1.0. For people familiar with ASP.NET Core, this pattern is just one of those things, but it adds confusion when you’re new to it.
第 3 点与必须调用 ConfigureWebHostDefaults()(使用 IWebHostBuilder)来设置 Kestrel 并注册 Startup 类这一事实有关。这种间接级别(以及另一种构建器类型的引入)是可以追溯到 ASP.NET Core 1.0 的决策的残余。对于熟悉 ASP.NET Core 的人来说,这种模式只是其中之一,但当你刚接触它时,它会增加困惑。

NOTE For a walk-through of the evolution of ASP.NET Core bootstrapping code, see my blog post at https://andrewlock.net/exploring-dotnet-6-part-2-comparing-webapplicationbuilder-to-the-generic-host/ .
注意有关 ASP.NET Core 引导代码演变的演练,请参阅我在 https://andrewlock.net/exploring-dotnet-6-part-2-comparing-webapplicationbuilder-to-the-generic-host/ 上的博客文章。

Similarly, the lambda-based configuration mentioned in point 4 can be hard for newcomers to ASP.NET Core to follow. If you’re new to .NET, lambdas are an extra concept you’ll need to understand before you can understand the basics of the code. On top of that, the execution of the lambdas doesn’t necessarily happen sequentially; the HostBuilder essentially queues the lambda methods so they’re executed at the right time. Consider the following snippet:
同样,第 4 点中提到的基于 lambda 的配置对于 ASP.NET Core 的新手来说可能很难理解。如果您不熟悉 .NET,则 lambda 是一个额外的概念,您需要先了解,然后才能了解代码的基础知识。最重要的是,lambda 的执行不一定是按顺序发生的;HostBuilder 实质上是将 lambda 方法排队,以便它们在正确的时间执行。请考虑以下代码段:

public static IhostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging => logging.AddSeq())
.ConfigureAppConfiguration(config => {})
.ConfigureServices(s => {})
.ConfigureHostConfiguration(config => {})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});

The lambdas execute in the following order:
lambda 按以下顺序执行:

  1. ConfigureWebHostDefaults()
  2. ConfigureHostConfiguration()
  3. ConfigureAppConfiguration()
  4. ConfigureLogging()
  5. ConfigureServices()
  6. Startup.ConfigureServices()
  7. Startup.Configure()

For the most part, this ordering detail shouldn’t matter, but it still adds apparent complexity for those who are new to ASP.NET Core.
在大多数情况下,这个排序细节应该无关紧要,但对于刚接触 ASP.NET Core 的人来说,它仍然增加了明显的复杂性。

Point 5 in the list of challenges relates to the Startup class and the default convention/ pattern-based approach. Users coming to ASP.NET Core for the first time will likely be familiar with interfaces and base classes, but they may not have experienced the reflection-based approach.
挑战列表中的第 5 点与 Startup 类和默认的约定/基于模式的方法有关。首次使用 ASP.NET Core 的用户可能熟悉接口和基类,但他们可能没有体验过基于反射的方法。

Using conventions instead of an explicit interface adds flexibility but can make things harder for discoverability. There are also various caveats and edge cases to consider. For example, you can inject only IWebHostEnvironment and IConfiguration into the Startup constructor; you can’t inject anything into the ConfigureServices() method, but you can inject any registered service into Configure(). These are implied rules that you discover primarily by breaking them and then having your app shout at you!‌
使用约定而不是显式接口可以增加灵活性,但可能会使可发现性变得更加困难。还有各种注意事项和边缘情况需要考虑。例如,只能将 IWebHostEnvironment 和 IConfiguration 注入 Startup 构造函数;你不能向 ConfigureServices() 方法注入任何内容,但你可以将任何已注册的服务注入到 Configure() 中。这些是隐含的规则,您主要是通过打破它们,然后让您的应用程序对您大喊大叫来发现的!

TIP The pattern-based approach allows for a lot more than DI into Configure. You can also create environment-specific methods, such as Configure-DevelopmentServices or ConfigureProductionServices, and ASP.NET Core invokes the correct method based on the environment. You can even create a whole StartupProduction class if you wish! For more details on these Startup conventions, see the documentation at http://mng.bz/Oxxw.
提示:基于模式的方法允许的不仅仅是 DI 到 Configure 中。您还可以创建特定于环境的方法,例如 Configure-DevelopmentServices 或 ConfigureProductionServices,ASP.NET Core 会根据环境调用正确的方法。如果您愿意,您甚至可以创建整个 StartupProduction 类!有关这些 Startup 约定的更多详细信息,请参阅 http://mng.bz/Oxxw 中的文档。

The Startup class isn’t the only place where ASP.NET Core relies on opaque conventions. You may remember in section30.2 I mentioned that Program.cs deliberately extracts the building of the IHostBuilder to a method called CreateHostBuilder. The name of this method was historically important. Tooling such as the EF Core tools hooked into it so that they could load your application configuration and services when running migrations and other functionality. In earlier versions of ASP.NET Core, renaming this method would break all your tooling!
Startup 类并不是 ASP.NET Core 依赖于不透明约定的唯一位置。你可能还记得30.2节我提到Program.cs特意将 IHostBuilder 的构建提取到一个名为 CreateHostBuilder 的方法中。这种方法的名称在历史上很重要。EF Core 工具等工具挂接到其中,以便它们可以在运行迁移和其他功能时加载应用程序配置和服务。在早期版本的 ASP.NET Core 中,重命名此方法会破坏您的所有工具!

NOTE As of .NET 6, you don’t have to create a CreateHostBuilder method; you can create your whole app inside your Main function (or using top-level statements), and the EF Core tools will work without error. This was fixed partly to add support for WebApplication. If you’re interested in the mechanics of how it was fixed, see my blog at http://mng.bz/Y11z.
注意:从 .NET 6 开始,您不必创建 CreateHostBuilder 方法;您可以在 Main 函数中(或使用顶级语句)创建整个应用程序,EF Core 工具将正常工作而不会出错。此问题已部分修复,以添加对 WebApplication 的支持。如果您对修复它的机制感兴趣,请参阅我的博客 http://mng.bz/Y11z

Once you’re experienced with ASP.NET Core, most of these gripes become relatively minor. You quickly get used to the standard patterns and avoid the pitfalls. But for new users of ASP.NET Core, Microsoft wanted a smoother experience, closer to the experience you get in many other languages.
一旦您体验了 ASP.NET Core,这些抱怨中的大多数都会变得相对较小。您很快就会习惯标准模式并避免陷阱。但对于 ASP.NET Core 的新用户,Microsoft 希望获得更流畅的体验,更接近您在许多其他语言中获得的体验。

The minimal hosting APIs provided by WebApplicationBuilder and WebApplication largely address these concerns. Configuration happens all in one file using an imperative style, with far fewer lambda-based configuration methods or implicit convention-based setup.
WebApplicationBuilder 和 WebApplication 提供的最小托管 API 在很大程度上解决了这些问题。使用命令式样式在一个文件中进行配置,基于 lambda 的配置方法或基于约定的隐式设置要少得多。

All the relevant objects like configuration and environment are exposed as properties on the WebApplicationBuilder or WebApplication types, so they’re easy to discover.‌
所有相关对象(如配置和环境)都作为 WebApplicationBuilder 或 WebApplication 类型的属性公开,因此很容易发现。

WebApplicationBuilder and WebApplication also try to hide much of the complexity and legacy decisions from you. Under the hood, WebApplication uses the generic host, but you don’t need to know that to use it or be productive. As you’ve seen throughout the book, WebApplication automatically adds various middleware to your pipeline, helping you avoid common pitfalls, such as incorrect middleware ordering.
WebApplicationBuilder 和 WebApplication 还试图向您隐藏许多复杂性和遗留决策。在后台,WebApplication 使用通用主机,但您无需知道它即可使用它或提高工作效率。正如您在整本书中所看到的,WebApplication 会自动将各种中间件添加到您的管道中,帮助您避免常见的陷阱,例如中间件顺序不正确。

NOTE If you’re interested in how WebApplicationBuilder abstracts over the generic host, see my post at https://andrewlock.net/exploring-dotnet-6-part-3-exploring-the-code-behind-webapplicationbuilder/ .
注意:如果您对 WebApplicationBuilder 如何在通用主机上进行抽象感兴趣,请参阅我在 https://andrewlock.net/exploring-dotnet-6-part-3-exploring-the-code-behind-webapplicationbuilder/ 上的帖子。

In most cases, minimal hosting provides an easier bootstrapping experience to the generic host and Startup, and Microsoft considers it to be the modern way to create ASP.NET Core apps. But there are cases in which you might want to consider using the generic host instead.
在大多数情况下,最小托管为通用主机和启动提供了更轻松的引导体验,Microsoft 认为这是创建 ASP.NET Core 应用程序的现代方式。但在某些情况下,您可能需要考虑改用通用主机。

30.6 Choosing between the generic host and minimal hosting‌

30.6 在通用主机和最小主机之间进行选择

The introduction of WebApplication and WebApplicationBuilder in .NET 6, also known as minimal hosting, was intended to provide a dramatically simpler “getting started” experience for newcomers to .NET and ASP.NET Core. All the built-in ASP.NET Core templates use minimal hosting now, and in most cases there’s little reason to look back. In this section I discuss some of the cases in which you might still want to use the generic host approach.
在 .NET 6 中引入 WebApplication 和 WebApplicationBuilder(也称为最小托管),旨在为 .NET 和 ASP.NET Core 的新手提供极其简单的“入门”体验。所有内置的 ASP.NET Core 模板现在都使用最少的托管,在大多数情况下,几乎没有理由回顾过去。在本节中,我将讨论您可能仍希望使用通用主机方法的一些情况。

In three main cases, you’ll likely want to stick with the generic host instead of using minimal hosting with WebApplication:
在三种主要情况下,您可能希望坚持使用通用主机,而不是对 WebApplication 使用最小托管:

• When you already have an ASP.NET Core application that uses the generic host
当您已有使用通用主机的 ASP.NET Core 应用程序时

• When you need (or want) fine control of building the IHost object
当您需要 (或想要) 精细控制构建 IHost 对象时

• When you’re creating a non-HTTP application
当您创建非 HTTP 应用程序时

The first use case is relatively obvious: if you already have an ASP.NET Core app that uses the generic host and Startup, you don’t need to change it. You can still upgrade your app to .NET 7, and you shouldn’t need to change any of your startup code. The generic host and Startup are fully supported in .NET 7, but they’re not the default experience.
第一个用例相对明显:如果您已经有一个使用通用主机和 Startup 的 ASP.NET Core 应用程序,则无需更改它。您仍然可以将应用程序升级到 .NET 7,并且不需要更改任何启动代码。.NET 7 完全支持泛型主机和启动,但它们不是默认体验。

TIP In many cases, upgrading an existing project to .NET 7 simply requires updating the framework in the .csproj file and updating some NuGet packages. If you’re unlucky, you may find that some APIs have changed. Microsoft publishes upgrade guides for each major version release, so it’s worth reading these before upgrading your apps: https://learn.microsoft.com/zh-cn/aspnet/core/migration/60-70 .
提示在许多情况下,将现有项目升级到 .NET 7 只需要更新 .csproj 文件中的框架并更新一些 NuGet 包。运气不好的话,你可能会发现一些 API 已经发生了变化。Microsoft 发布了每个主要版本的升级指南,因此在升级应用程序之前,值得阅读这些指南:https://learn.microsoft.com/zh-cn/aspnet/core/migration/60-70

If you’re creating a new app, but for some reason you don’t like the default options used by WebApplicationBuilder, using the generic host may be your best option. I generally wouldn’t advise this approach, as it will likely require more maintenance than using WebApplication, but it does give you complete control of your bootstrap code if you need or want it.
如果您正在创建新应用程序,但出于某种原因您不喜欢 WebApplicationBuilder 使用的默认选项,则使用通用主机可能是您的最佳选择。我通常不建议使用这种方法,因为它可能需要比使用 WebApplication 更多的维护,但如果您需要或想要它,它确实可以让您完全控制引导代码。

The final case applies when you’re building an ASP.NET Core application that primarily runs background processing services, handling messages from a queue for example, but doesn’t handle HTTP requests. The minimal hosting WebApplication and WebApplicationBuilder are, as their names imply, focused on building web applications, so they don’t make sense in this situation.
当您构建主要运行后台处理服务(例如处理来自队列的消息,但不处理 HTTP 请求)的 ASP.NET Core 应用程序时,最后一种情况适用。顾名思义,最小托管 WebApplication 和 WebApplicationBuilder 专注于构建 Web 应用程序,因此在这种情况下它们没有意义。

NOTE You’ll learn how to create background tasks and services using the generic host in chapter 34. .NET 8 introduces a non-HTTP version of the WebApplicationBuilder called HostApplicationBuilder which aims to simplify app bootstrapping for your background services.
注意:您将在第 34 章中学习如何使用通用主机创建后台任务和服务。.NET 8 引入了一个名为 HostApplicationBuilder 的非 HTTP 版本的 WebApplicationBuilder,旨在简化后台服务的应用程序启动。

If you’re not in any of these situations, strongly consider using the minimal hosting WebApplication approach and the imperative, scriptlike bootstrapping of top-level statements.
如果您不处于上述任何一种情况,强烈建议使用最小托管 WebApplication 方法和顶级语句的命令式脚本式引导。

NOTE The fact that you’re using WebApplication doesn’t mean you have to dump all your service and middleware configuration into Program.cs. For alternative approaches, such as using a Startup class you invoke manually or local functions to separate your configuration, see my blog post at https://andrewlock.net/exploring-dotnet-6-part-12-upgrading-a-dotnet-5-startup-based-app-to-dotnet-6/ .
注意:您使用的是 WebApplication 这一事实并不意味着您必须将所有服务和中间件配置转储到 Program.cs 中。有关替代方法,例如使用手动调用的 Startup 类或本地函数来分隔配置,请参阅我在 https://andrewlock.net/exploring-dotnet-6-part-12-upgrading-a-dotnet-5-startup-based-app-to-dotnet-6/ 上的博客文章。

In this chapter I provided a relatively quick overview of the generic host and Startup-based approach. If you’re thinking of moving from the generic host to minimal hosting, or if you’re familiar with minimal hosting but need to work with the generic host, you may find yourself looking around for an equivalent feature in the other hosting model. The documentation for migrating from .NET 5 to .NET 6 provides a good description of the differences between the two models, and how each individual feature has changed. You can find it at https://learn.microsoft.com/zh-cn/aspnet/core/migration/50-to-60.
在本章中,我相对快速地概述了通用主机和基于 Startup 的方法。如果您正在考虑从通用主机迁移到最小托管,或者如果您熟悉最小托管但需要与通用主机合作,您可能会发现自己在另一种托管模型中寻找等效功能。从 .NET 5 迁移到 .NET 6 的文档很好地描述了两种模型之间的差异,以及每个单独的功能是如何变化的。您可以在 https://learn.microsoft.com/zh-cn/aspnet/core/migration/50-to-60 找到它。

TIP Alternatively, David Fowler from the .NET team has a similar cheat sheet describing the migration. See https://gist.github.com/davidfowl/0e0372c3c1d895c3ce195ba983b1e03d .
提示:或者,来自 .NET 团队的 David Fowler 有一个类似的备忘单来描述迁移。请参阅 https://gist.github.com/davidfowl/0e0372c3c1d895c3ce195ba983b1e03d

Whether you choose to use the generic host or minimal hosting, all the same ASP.NET Core concepts are there: configuration, middleware, and DI. In the next chapter you’ll learn about some more advanced uses of each of these concepts, such as creating branching middleware pipelines and custom DI containers.
无论您选择使用通用主机还是最小托管,所有相同的 ASP.NET Core 概念都存在:配置、中间件和 DI。在下一章中,您将了解这些概念的一些更高级的用法,例如创建分支中间件管道和自定义 DI 容器。

30.7 Summary

30.7 总结

Before .NET 6, ASP.NET Core apps split configuration between two files: Program.cs and Startup.cs. Program.cs contains the entry point for the app and is used to configure and build a IHost object. Startup is where you configure the DI container, middleware pipeline, and endpoints for your app.
在 .NET 6 之前,ASP.NET Core 应用程序将配置拆分为两个文件:Program.cs 和 Startup.cs。Program.cs 包含应用程序的入口点,用于配置和生成 IHost 对象。Startup (启动) 是您为应用程序配置 DI 容器、中间件管道和终端节点的地方。

The Program class typically contains a method called CreateHostBuilder(), which creates an IHostBuilder instance. The Main entry point invokes CreateHostBuilder(), calls IHostBuilder.Build() to create an instance of IHost, and finally runs the app by calling IHost.Run().
Program 类通常包含一个名为 CreateHostBuilder() 的方法,该方法创建一个 IHostBuilder 实例。主入口点调用 CreateHostBuilder(),调用 IHostBuilder.Build() 来创建 IHost 的实例,最后通过调用 IHost.Run() 运行应用程序。

You can create an IHostBuilder by calling Host.CreateDefaultBuilder(). This creates a HostBuilder instance using the default configuration, similar to the configuration used when calling WebApplication.CreateBuilder(). The default HostBuilder uses default logging and configuration providers, configures the hosting environment based on environment variables and command-line arguments, and configures the DI container settings.
您可以通过调用 Host.CreateDefaultBuilder() 来创建 IHostBuilder。这将使用默认配置创建一个 HostBuilder 实例,类似于调用 WebApplication.CreateBuilder() 时使用的配置。默认 HostBuilder 使用默认日志记录和配置提供程序,根据环境变量和命令行参数配置托管环境,并配置 DI 容器设置。

ASP.NET Core apps using the generic host typically call ConfigureWebHostDefaults(), on the HostBuilder, providing a lambda that calls UseStartup<Startup>() on an IWebHostBuilder instance. This tells the HostBuilder to configure the DI container and middleware pipeline based on the Startup class.
使用通用主机的 ASP.NET Core 应用程序通常在 HostBuilder 上调用 ConfigureWebHostDefaults(),从而提供在 IWebHostBuilder 实例上调用 UseStartup<Startup>() 的 lambda。这会告诉 HostBuilder 根据 Startup 类配置 DI 容器和中间件管道。

Use the Startup class to register services with DI, configure your middleware pipeline, and register your endpoints. It is a conventional class, in that it doesn’t have to implement an interface or base class. Instead, the IHostBuilder looks for specific named methods to invoke using reflection.
使用 Startup 类向 DI 注册服务、配置中间件管道并注册终端节点。它是一个约定俗成的类,因为它不必实现接口或基类。相反,IHostBuilder 会查找要使用反射调用的特定命名方法。

Register your DI services in the ConfigureServices(IServiceCollection) method of Startup. You register services using the same Add methods you use to register services on WebApplicationBuilder.Services when using minimal hosting.
在 Startup 的 ConfigureServices(IServiceCollection) 方法中注册 DI 服务。使用最小托管时,您可以使用在 WebApplicationBuilder.Services 上注册服务时使用的相同 Add
方法注册服务。

If you need to access your app’s IConfiguration or IWebHostEnvironment (exposed as Configuration and Environment, respectively, on WebApplicationBuilder), you can inject them into your Startup constructor.You can’t inject any other services into the Startup constructor.
如果需要访问应用程序的 IConfiguration 或 IWebHostEnvironment(在 WebApplicationBuilder 上分别作为 Configuration 和 Environment 公开),则可以将它们注入到 Startup 构造函数中。您不能将任何其他服务注入 Startup 构造函数。

Register your middleware pipeline in Startup.Configure(IApplicationBuilder). Use the same Use methods you use with WebApplication to add middleware to the pipeline. As for WebApplication, the order in which you add the middleware defines their order in the pipeline.
在 Startup.Configure (IApplicationBuilder) 中注册中间件管道。使用与 WebApplication 相同的 Use
方法将中间件添加到管道中。对于 WebApplication,您添加中间件的顺序定义了它们在管道中的顺序。

WebApplication automatically adds middleware such as the routing middleware and endpoint middleware to the pipeline when you’re using minimal hosting. When using Startup, you must explicitly add this middleware yourself.
当您使用最小托管时,WebApplication 会自动将中间件(如路由中间件和终端节点中间件)添加到管道中。使用 Startup 时,您必须自己显式添加此中间件。

To register endpoints, call UseEndpoints(endpoints => {}) and call the appropriate Map functions on the provided IEndpointRouteBuilder in the lambda function. This differs significantly from minimal hosting, in which you can call Map directly on the WebApplication instance.
要注册终端节点,请调用 UseEndpoints(endpoints => {}) 并在 lambda 函数中提供的 IEndpointRouteBuilder 上调用相应的 Map 函数。这与最小托管有很大不同,在最小托管中,您可以直接在 WebApplication 实例上调用 Map。

You can customize the IHostBuilder instance by adding configuration methods such as ConfigureLogging() or ConfigureAppConfiguration(). These methods run after any previous invocations, adding extra layers of configuration to the IHostBuilder instance.
您可以通过添加配置方法(如 ConfigureLogging() 或 ConfigureAppConfiguration())来自定义 IHostBuilder 实例。这些方法在之前的任何调用之后运行,向 IHostBuilder 实例添加额外的配置层。

The generic host is flexible but has greater inherent complexity due to its deferred execution style, extensive use of lambda methods, and heavy use of convention. Minimal hosting aimed to simplify the bootstrapping code to make it more imperative, reducing much of the indirection.
泛型主机很灵活,但由于其延迟执行样式、广泛使用 lambda 方法和大量使用约定,因此具有更大的固有复杂性。最小托管旨在简化引导代码,使其更加必要,从而减少大部分间接性。

Minimal hosting enforces more defaults but is generally easier to work with for newcomers to ASP.NET Core.
最小托管强制实施更多默认值,但通常更容易使用 ASP.NET Core 的新手。

If you already have an ASP.NET Core application using Startup and the generic host, there’s no need to switch to using WebApplication and minimal hosting; the generic host is fully supported in .NET 7. Additionally, if you’re creating a non- HTTP application, the generic host is currently the best option.
如果您已经拥有使用 Startup 和通用主机的 ASP.NET Core 应用程序,则无需切换到使用 WebApplication 和最小托管;.NET 7 完全支持泛型主机。此外,如果要创建非 HTTP 应用程序,则通用主机是当前最佳选项。

If you’re creating a new ASP.NET Core application, minimal hosting will likely provide a smoother experience. You should generally favor it over the generic host for new apps unless you need fine control of the IHostBuilder configuration.
如果您正在创建新的 ASP.NET Core 应用程序,最小托管可能会提供更流畅的体验。对于新应用程序,您通常应该更喜欢它而不是通用主机,除非您需要对 IHostBuilder 配置进行精细控制。

ASP.NET Core in Action 29 Improving your application’s security

29 Improving your application’s security
29 提高应用程序的安全性

This chapter covers
本章涵盖

• Defending against cross-site scripting attacks
防御跨站点脚本攻击

• Protecting from cross-site request forgery attacks
防止跨站点请求伪造攻击

• Allowing calls to your API from other apps using CORS
允许使用 CORS从其他应用程序调用您的 API

• Avoiding attach vectors such as SQL injection attacks
避免 SQL 注入攻击等附加向量

In chapter 28 you learned how and why you should use HTTPS in your application: to protect your HTTP requests from attackers. In this chapter we look at more ways to protect your application and your application’s users from attackers. Because security is an extremely broad topic that covers lots of avenues, this chapter is by no means an exhaustive guide. It’s intended to make you aware of some of the most common threats to your app and how to counteract them, and also to highlight areas where you can inadvertently introduce vulnerabilities if you’re not careful.
在第 28 章中,您了解了如何以及为什么应该在应用程序中使用 HTTPS:保护您的 HTTP 请求免受攻击者的攻击。在本章中,我们将介绍更多方法来保护您的应用程序和应用程序用户免受攻击者的攻击。由于安全性是一个非常广泛的主题,涵盖了许多途径,因此本章绝不是详尽的指南。它旨在让您了解应用程序面临的一些最常见威胁以及如何应对这些威胁,并突出显示如果您不小心可能会无意中引入漏洞的区域。

TIP I strongly advise exploring additional resources around security after you’ve read this chapter. The Open Web Application Security Project (OWASP) (www.owasp.org) is an excellent resource. Alternatively, Troy Hunt has some excellent courses and workshops on security, geared toward .NET developers (https://www.troyhunt.com).
提示:我强烈建议您在阅读本章后探索有关安全性的其他资源。开放 Web 应用程序安全项目 (OWASP) (www.owasp.org) 是一个很好的资源。或者,Troy Hunt 有一些面向 .NET 开发人员 (https://www.troyhunt.com) 的优秀安全课程和研讨会。

In sections 29.1 and 29.2 you’ll start by learning about two potential attacks that should be on your radar: cross-site scripting (XSS) and cross-site request forgery (CSRF). We’ll explore how the attacks work and how you can prevent them in your apps. ASP.NET Core has built-in protection against both types of attacks, but you have to remember to use the protection correctly and resist the temptation to circumvent it unless you’re certain it’s safe to do so.
在第 29.1 节和第 29.2 节中,您将首先了解应该引起注意的两种潜在攻击:跨站点脚本 (XSS) 和跨站点请求伪造 (CSRF)。我们将探讨这些攻击的工作原理,以及如何在您的应用程序中防止它们。ASP.NET Core 具有针对这两种类型的攻击的内置保护,但您必须记住正确使用保护并抵制规避它的诱惑,除非您确定这样做是安全的。

Section 29.3 deals with a common scenario: you have an application that wants to use JavaScript requests to retrieve data from a second app. By default, web browsers block requests to other apps, so you need to enable cross-origin resource sharing (CORS) in your API to achieve this. We’ll look at how CORS works, how to create a CORS policy for your app, and how to apply it to specific endpoints.
Section 29.3 处理一个常见情况:您有一个应用程序,它想要使用 JavaScript 请求从第二个应用程序检索数据。默认情况下,Web 浏览器会阻止对其他应用程序的请求,因此您需要在 API 中启用跨域资源共享 (CORS) 才能实现此目的。我们将了解 CORS 的工作原理、如何为您的应用程序创建 CORS 策略以及如何将其应用于特定终端节点。

The final section of this chapter, section 29.4, covers a collection of common threats to your application. Each one represents a potentially critical flaw that an attacker could use to compromise your application. The solutions to each threat are generally relatively simple; the important thing is to recognize where the flaws could exist in your own apps so you can ensure that you don’t leave yourself vulnerable.
本章的最后一部分,即 29.4 节,涵盖了应用程序的一系列常见威胁。每个漏洞都代表一个潜在的严重缺陷,攻击者可以利用该漏洞来破坏您的应用程序。每种威胁的解决方案通常相对简单;重要的是识别您自己的应用程序中可能存在的缺陷,这样您就可以确保不会让自己容易受到攻击。

As I mentioned in chapter 28, you should always start by adding HTTPS to your app to encrypt the traffic between your users’ browsers and your app. Without HTTPS, attackers could subvert many of the safeguards you add to your app, so it’s an important first step to take.
正如我在第 28 章中提到的,您应该始终从将 HTTPS 添加到您的应用程序开始,以加密用户浏览器和应用程序之间的流量。如果没有 HTTPS,攻击者可能会破坏您添加到应用程序的许多保护措施,因此这是重要的第一步。

Unfortunately, most other security practices require rather more vigilance to ensure that you don’t accidentally introduce vulnerabilities into your app as it grows and develops. Many attacks are conceptually simple and have been known about for years, yet they’re still commonly found in new applications. In the next section we’ll look at one such attack and see how to defend against it when building apps using Razor Pages.
不幸的是,大多数其他安全实践都需要更加警惕,以确保您不会在应用程序的成长和发展过程中意外地将漏洞引入应用程序。许多攻击在概念上很简单,并且已经为人所知多年,但它们仍然常见于新应用程序。在下一节中,我们将介绍一种此类攻击,并了解如何在使用 Razor Pages 构建应用程序时防御它。

29.1 Defending against cross- site scripting (XSS) attacks‌

29.1 防御跨站点脚本 (XSS) 攻击

In this section I describe XSS attacks and how attackers can use them to compromise your users. I show how the Razor Pages framework protects you from these attacks, how to disable the protections when you need to, and what to look out for. I also discuss the difference between HTML encoding and JavaScript encoding, and the effect of using the wrong encoder.‌
在本节中,我将介绍 XSS 攻击以及攻击者如何利用它们来危害您的用户。我将展示 Razor Pages 框架如何保护您免受这些攻击,如何在需要时禁用保护,以及需要注意的事项。我还讨论了 HTML 编码和 JavaScript 编码之间的区别,以及使用错误编码器的影响。

Attackers can exploit a vulnerability in your app to create XSS attacks that execute code in another user’s browser. Commonly, attackers submit content using a legitimate approach, such as an input form, that is later rendered somewhere to the page. By carefully crafting malicious input, the attacker can execute arbitrary JavaScript on a user’s browser and so can steal cookies, impersonate the user, and generally do bad things.
攻击者可以利用您应用程序中的漏洞来创建 XSS 攻击,从而在其他用户的浏览器中执行代码。通常,攻击者使用合法方法(如输入表单)提交内容,这些方法稍后会呈现在页面的某个位置。通过精心设计恶意输入,攻击者可以在用户的浏览器上执行任意 JavaScript,从而窃取 Cookie、冒充用户,并通常会做坏事。

TIP For a detailed discussion of XSS attacks, see the “Cross Site Scripting (XSS)” article on the OWASP site: https://owasp.org/www-community/attacks/xss.
提示:有关 XSS 攻击的详细讨论,请参阅 OWASP 站点上的“跨站点脚本 (XSS)”文章:https://owasp.org/www-community/attacks/xss

Figure 29.1 shows a basic example of an XSS attack. Legitimate users of your app can send their name to your app by submitting a form. The app then adds the name to an internal list and renders the whole list to the page. If the names are not rendered safely, a malicious user can execute JavaScript in the browser of every other user who views the list.
图 29.1 显示了 XSS 攻击的一个基本示例。您应用的合法用户可以通过提交表单将其名称发送到您的应用。然后,应用程序将名称添加到内部列表,并将整个列表呈现到页面。如果名称未安全呈现,恶意用户可以在查看列表的所有其他用户的浏览器中执行 JavaScript。

alt text

Figure 29.1 How an XSS vulnerability is exploited. An attacker submits malicious content to your app, which is displayed in the browsers of other users. If the app doesn’t encode the content when writing to the page, the input becomes part of the HTML of the page and can run arbitrary JavaScript.
图 29.1 XSS 漏洞是如何被利用的。攻击者向您的应用提交恶意内容,这些内容会显示在其他用户的浏览器中。如果应用程序在写入页面时未对内容进行编码,则输入将成为页面 HTML 的一部分,并且可以运行任意 JavaScript。

In figure 29.1 the user entered a snippet of HTML, such as their name. When users view the list of names, the Razor template renders the names using @Html.Raw(), which writes the <script> tag directly to the document. The user’s input has become part of the page’s HTML structure. As soon as the page is loaded in a user’s browser, the<script> tag executes, and the user is compromised. Once an attacker can execute arbitrary JavaScript on a user’s browser, they can do pretty much anything.
在图 29.1 中,用户输入了一个 HTML 片段,例如他们的名称。当用户查看名称列表时,Razor 模板使用 @Html.Raw() 呈现名称,后者将<script>标记直接写入文档。用户的输入已成为页面 HTML 结构的一部分。一旦页面加载到用户的浏览器中,<script>标记就会执行,并且用户会受到威胁。一旦攻击者可以在用户的浏览器上执行任意 JavaScript,他们几乎可以做任何事情。

TIP You can dramatically limit the control an attacker has even if they exploit an XSS vulnerability using a Content- Security-Policy (CSP). You can read about CSP at http://mng.bz/nWW2. I have an open-source library you can use to integrate a CSP into your app available on NuGet at http://mng.bz/vnn4.
提示:您可以极大地限制攻击者的控制权,即使他们使用内容安全策略 (CSP) 利用 XSS 漏洞。您可以在 http://mng.bz/nWW2 上阅读有关 CSP 的信息。我有一个开源库,您可以使用它将 CSP 集成到 NuGet 上提供的应用程序中,网址为 http://mng.bz/vnn4

The vulnerability here is due to rendering the user input in an unsafe way. If the data isn’t encoded to make it safe before it’s rendered, you could open your users to attack. By default, Razor protects against XSS attacks by HTML- encoding any data written using Tag Helpers, HTML Helpers, or the @ syntax. So generally you should be safe, as you saw in chapter 17.
此处的漏洞是由于以不安全的方式呈现用户输入。如果数据在呈现之前没有进行编码以确保其安全,则可能会使用户受到攻击。默认情况下,Razor 通过对使用标记帮助程序、HTML 帮助程序或 @ 语法写入的任何数据进行 HTML 编码来防止 XSS 攻击。所以一般来说你应该是安全的,就像你在第 17 章中看到的那样。

Using @Html.Raw() is where the danger lies: if the HTML you’re rendering contains user input (even indirectly), you could have an XSS vulnerability. By rendering the user input with @ instead, the content is encoded before it’s written to the output, as shown in figure 29.2.
使用 @Html.Raw() 是危险所在:如果您渲染的 HTML 包含用户输入(即使是间接的),则可能存在 XSS 漏洞。通过使用 @ 来呈现用户输入,内容在写入输出之前进行编码,如图 29.2 所示。

alt text

Figure 29.2 Protecting against XSS attacks by HTML- encoding user input using @ in Razor templates. The <script> tag is encoded so that it is no longer rendered as HTML and can’t be used to compromise your app.
图 29.2 在 Razor 模板中使用 @ 对用户输入进行 HTML 编码来防范 XSS 攻击。该 <script>标记经过编码,因此它不再呈现为 HTML,也不能用于危害您的应用。

This example demonstrates using HTML encoding to prevent elements being directly added to the HTML Document Object Model (DOM), but it’s not the only case you have to think about. If you’re passing untrusted data to JavaScript or using untrusted data in URL query values, you must make sure to encode the data correctly.
此示例演示了如何使用 HTML 编码来防止元素被直接添加到 HTML 文档对象模型 (DOM) 中,但这并不是您必须考虑的唯一情况。如果要将不受信任的数据传递给 JavaScript 或在 URL 查询值中使用不受信任的数据,则必须确保正确编码数据。

A common scenario is when you’re using JavaScript with Razor Pages, and you want to pass a value from the server to the client. If you use the standard @ symbol to render the data to the page, the output will be HTML-encoded.
一种常见情况是,将 JavaScript 与 Razor Pages 配合使用,并且想要将值从服务器传递到客户端。如果使用标准 @ 符号将数据呈现到页面,则输出将采用 HTML 编码。

Unfortunately, if you HTML-encode a string and inject it directly into JavaScript, you probably won’t get what you expect.
不幸的是,如果你对字符串进行 HTML 编码并将其直接注入到 JavaScript 中,你可能不会得到你所期望的结果。

For example, if you have a variable in your Razor file called name, and you want to make it available in JavaScript, you might be tempted to use something like this:
例如,如果您的 Razor 文件中有一个名为 name 的变量,并且您希望在 JavaScript 中使其可用,您可能会想使用如下内容:

<script>var name = '@name'</script>

If the name contains special characters, Razor will encode them using HTML encoding, which probably isn’t what you want in this JavaScript context. For example, if name was Arnold "Arnie" Schwarzenegger, rendering it as you did previously would give this:
如果名称包含特殊字符,Razor 将使用 HTML 编码对其进行编码,这可能不是你在此 JavaScript 上下文中想要的。例如,如果 name 是 Arnold “Arnie” Schwarzenegger,则像以前一样呈现它将得到以下结果:

<script>var name = 'Arnold "Arnie" Schwarzenegger';</script>

Note that the double quotation marks (") have been HTML- encoded to ". If you use this value in JavaScript directly, expecting it to be a safe encoded value, it’s going to look wrong, as shown in figure 29.3.
请注意,双引号 (“) 已 HTML 编码为 ”.如果你直接在 JavaScript 中使用这个值,期望它是一个安全的编码值,它看起来会出错,如图 29.3 所示。

alt text

Figure 29.3 Comparison of alerts when using JavaScript encoding compared with HTML encoding
图 29.3 使用 JavaScript 编码与 HTML 编码时的警报比较

Instead, you should encode the variable using JavaScript encoding so that the double-quote character is rendered as a safe Unicode character, \u0022. You can achieve this by injecting a JavaScriptEncoder into the view and calling Encode() on the name variable:
相反,您应该使用 JavaScript 编码对变量进行编码,以便将双引号字符呈现为安全的 Unicode 字符 \u0022。您可以通过将 JavaScriptEncoder 注入视图并在 name 变量上调用 Encode() 来实现这一点:

@inject System.Text.Encodings.Web.JavaScriptEncoder encoder;
<script>var name = '@encoder.Encode(name)'</script>

To avoid having to remember to use JavaScript encoding, I recommend that you don’t write values into JavaScript like this. Instead, write the value to an HTML element’s attributes, and then read that into the JavaScript variable later, as shown in the following listing. That prevents the need for the JavaScript encoder entirely.
为避免记住使用 JavaScript 编码,我建议您不要像这样将值写入 JavaScript。相反,将值写入 HTML 元素的属性,然后稍后将其读取到 JavaScript 变量中,如下面的清单所示。这完全不需要 JavaScript 编码器。

Listing 29.1 Passing values to JavaScript by writing them to HTML attributes
清单 29.1 通过将值写入 HTML 属性来将值传递给 JavaScript

<div id="data" data-name="@name"></div>
<script> ❶
var ele = document.getElementById('data'); ❷
var name = ele.getAttribute('data-name'); ❸
</script>

❶ Write the value you want in JavaScript to a data-* attribute. This HTML-encodes the data.
在 JavaScript 中将你想要的值写入 data- 属性。此 HTML 对数据进行编码。

❷ Gets a reference to the HTML element
获取对 HTML 元素的引用

❸ Reads the data-* attribute into JavaScript, which converts it to JavaScript encoding
将 data- 属性读取到 JavaScript 中,从而将其转换为 JavaScript 编码

XSS attacks are still common, and it’s easy to expose yourself to them whenever you allow users to input data. Validation of the incoming data can help sometimes, but it’s often a tricky problem. For example, a naive name validator might require that you use only letters, which would prevent most attacks. Unfortunately, that doesn’t account for users with hyphens or apostrophes in their name, let alone users with non-Western names. People get (understandably) upset when you tell them that their name is invalid, so be wary of this approach!
XSS 攻击仍然很常见,只要您允许用户输入数据,就很容易将自己暴露在它们面前。验证传入数据有时会有所帮助,但这通常是一个棘手的问题。例如,一个 naive name validator 可能要求您只使用字母,这样可以防止大多数攻击。不幸的是,这并未考虑名称中包含连字符或撇号的用户,更不用说具有非西方名称的用户了。当你告诉他们他们的名字无效时,人们会(可以理解地)不安,所以要警惕这种做法!

Whether or not you use strict validation, you should always encode the data when you render it to the page. Think carefully whenever you find yourself writing @Html.Raw(). Is there any way, no matter how contrived, for a user to get malicious data into that field? If so, you’ll need to find another way to display the data.
无论是否使用严格验证,在将数据呈现到页面时,都应始终对数据进行编码。每当您发现自己编写 @Html.Raw() 时,请仔细考虑。无论多么人为,用户是否有任何方法可以将恶意数据导入该字段?如果是这样,您将需要找到另一种显示数据的方法。

XSS vulnerabilities allow attackers to execute JavaScript on a user’s browser. The next vulnerability we’re going to consider lets them make requests to your API as though they’re a different logged-in user, even when the user isn’t using your app. Scared? I hope so!‌
XSS 漏洞允许攻击者在用户的浏览器上执行 JavaScript。我们将要考虑的下一个漏洞允许他们向您的 API 发出请求,就好像他们是不同的登录用户一样,即使该用户没有使用您的应用程序。害怕吗?希望如此!

29.2 Protecting from cross-site request forgery (CSRF) attacks‌

29.2 防止跨站点请求伪造 (CSRF) 攻击

In this section you’ll learn about CSRF attacks, how attackers can use them to impersonate a user on your site, and how to protect against them using antiforgery tokens. Razor Pages protects you from these attacks by default, but you can disable these verifications, so it’s important to understand the implications of doing so.
在本节中,您将了解 CSRF 攻击、攻击者如何使用它们来冒充您网站上的用户,以及如何使用防伪令牌来防范它们。默认情况下,Razor Pages 会保护您免受这些攻击,但您可以禁用这些验证,因此请务必了解这样做的含义。

CSRF attacks can be a problem for websites or APIs that use cookies for authentication. A CSRF attack involves a malicious website making an authenticated request to your API on behalf of the user, without the user’s initiating the request. In this section we’ll explore how these attacks work and how you can mitigate them with antiforgery tokens.
对于使用 cookie 进行身份验证的网站或 API 来说,CSRF 攻击可能是一个问题。CSRF 攻击涉及恶意网站代表用户向您的 API 发出经过身份验证的请求,而无需用户发起请求。在本节中,我们将探讨这些攻击的工作原理,以及如何使用防伪令牌来缓解它们。

The canonical example of this attack is a bank transfer/withdrawal. Imagine you have a banking application that stores authentication tokens in a cookie, as is common (especially in traditional server-side rendered applications).Browsers automatically send the cookies associated with a domain with every request so the app knows whether a user is authenticated.
这种攻击的典型示例是银行转账/取款。假设您有一个银行应用程序,它将身份验证令牌存储在 Cookie 中,这很常见(尤其是在传统的服务器端呈现的应用程序中)。浏览器会自动将与域关联的 Cookie 与每个请求一起发送,以便应用程序知道用户是否经过身份验证。

Now imagine your application has a page that lets a user transfer funds from their account to another account using a POST request to the Balance Razor Page. You have to be logged in to access the form (you’ve protected the Razor Page with the [Authorize] attribute or global authorization requirements), but otherwise you post a form that says how much you want to transfer and where you want to transfer it. Seems simple enough?‌
现在,假设你的应用程序有一个页面,该页面允许用户使用对 Balance Razor 页面的 POST 请求将资金从其帐户转移到另一个帐户。您必须登录才能访问该表单(您已使用 [Authorize] 属性或全局授权要求保护了 Razor 页面),但除此之外,您需要发布一个表单,说明您要转移的金额以及要转移的位置。看起来很简单?

Suppose that a user visits your site, logs in, and performs a transaction. Then they visit a second website that the attacker has control of. The attacker has embedded a form in their website that performs a POST to your bank’s website, identical to the transfer-funds form on your banking website. This form does something malicious, such as transfer all the user’s funds to the attacker, as shown in figure 29.4.
假设用户访问您的网站、登录并执行事务。然后,他们访问攻击者可以控制的第二个网站。攻击者在其网站中嵌入了一个表单,该表单会向您的银行网站执行 POST,该表单与您的银行网站上的转账资金表单相同。这种形式会做一些恶意的事情,比如把用户的所有资金都转移给攻击者,如图 29.4 所示。

Browsers automatically send the cookies for the application when the page does a full form post, and the banking app has no way of knowing that this is a malicious request. The unsuspecting user has given all their money to the attacker!
当页面执行完整表单发布时,浏览器会自动发送应用程序的 Cookie,而银行应用程序无法知道这是恶意请求。毫无戒心的用户已经把他们所有的钱都给了攻击者!

alt text

Figure 29.4 A CSRF attack occurs when a logged-in user visits a malicious site. The malicious site crafts a form that matches one on your app and POSTs it to your app. The browser sends the authentication cookie automatically, so your app sees the request as a valid request from the user.
图 29.4 当登录用户访问恶意站点时,会发生 CSRF 攻击。恶意网站会制作一个与您的应用程序匹配的表单,并将其 POST 到您的应用程序。浏览器会自动发送身份验证 Cookie,因此您的应用会将该请求视为来自用户的有效请求。

The vulnerability here revolves around the fact that browsers automatically send cookies when a page is requested (using a GET request) or a form is POSTed. There’s no difference between a legitimate POST of the form in your banking app and the attacker’s malicious POST. Unfortunately, this behavior is baked into the web; it’s what allows you to navigate websites seamlessly after initially logging in.
此处的漏洞围绕以下事实展开:浏览器在请求页面(使用 GET 请求)或发布表单时自动发送 Cookie。您的银行应用程序中形式的合法 POST 与攻击者的恶意 POST 之间没有区别。不幸的是,这种行为已经融入了 Web;它允许您在初始登录后无缝浏览网站。

TIP Browsers have additional protections to prevent cookies being sent in this situation, called SameSite cookies. By default, most browsers use SameSite=Lax, which prevents this vulnerable behavior. You can read about SameSite cookies and how to work with them in ASP.NET Core at http://mng.bz/4DDj.
提示:浏览器具有额外的保护措施来防止在这种情况下发送 Cookie,称为 SameSite Cookie。默认情况下,大多数浏览器使用 SameSite=Lax,这可以防止这种易受攻击的行为。您可以在 http://mng.bz/4DDj 阅读有关 SameSite Cookie 以及如何在 ASP.NET Core 中使用它们的信息。

A common solution to this CSRF attack is the synchronizer token pattern, which uses user-specific, unique antiforgery tokens to enforce a difference between a legitimate POST and a forged POST from an attacker. One token is stored in a cookie, and another is added to the form you wish to protect. Your app generates the tokens at runtime based on the current logged-in user, so there’s no way for an attacker to create one for their forged form.
这种 CSRF 攻击的常见解决方案是同步器令牌模式,它使用特定于用户的唯一防伪令牌来强制区分来自攻击者的合法 POST 和伪造的 POST。一个令牌存储在 Cookie 中,另一个令牌将添加到您要保护的表单中。您的应用在运行时根据当前登录用户生成令牌,因此攻击者无法为其伪造表单创建令牌。

TIP The “Cross-Site Request Forgery Prevention Cheat Sheet” article on the OWASP site (http://mng.bz/5jRa) has a thorough discussion of the CSRF vulnerability, including the synchronizer token pattern.
提示:OWASP 站点 (http://mng.bz/5jRa) 上的“跨站点请求伪造预防备忘单”一文对 CSRF 漏洞进行了深入讨论,包括同步器令牌模式。

When the Balance Razor Page receives a form POST, it compares the value in the form with the value in the cookie. If either value is missing or the values don’t match, the request is rejected. If an attacker creates a POST, the browser posts the cookie token as usual, but there won’t be a token in the form itself or the token won’t be valid. The Razor Page rejects the request, protecting from the CSRF attack, as in figure 29.5.
当 Balance Razor 页面收到表单 POST 时,它会将表单中的值与 Cookie 中的值进行比较。如果缺少任一值或值不匹配,则请求将被拒绝。如果攻击者创建 POST,浏览器会照常发布 cookie 令牌,但表单本身不会有令牌,或者令牌无效。Razor Page 拒绝请求,防止 CSRF 攻击,如图 29.5 所示。

alt text

Figure 29.5 Protecting against a CSRF attack using antiforgery tokens. The browser automatically forwards the cookie token, but the malicious site can’t read it and so can’t include a token in the form.The app rejects the malicious request because the tokens don’t match.
图 29.5 使用防伪令牌防范 CSRF 攻击。浏览器会自动转发 Cookie 令牌,但恶意站点无法读取它,因此无法在表单中包含令牌。应用程序拒绝恶意请求,因为令牌不匹配。

The good news is that Razor Pages automatically protects you against CSRF attacks. The Form Tag Helper automatically sets an antiforgery token cookie and renders the token to a hidden field called _RequestVerificationToken for every <form> element in your app (unless you specifically disable them). For example, take this simple Razor template that posts back to the same Razor Page:
好消息是 Razor Pages 会自动保护您免受 CSRF 攻击。Form Tag Helper 会自动设置防伪令牌 Cookie,并将该令牌呈现到应用中每个<form>元素的名为 _RequestVerificationToken 的隐藏字段(除非您专门禁用它们)。例如,以这个简单的 Razor 模板为例,该模板回发到同一 Razor 页面:

<form method="post">
<label>Amount</label>
<input type="number" name="amount" />
<button type="submit">Withdraw funds</button>
</form>

When rendered to HTML, the antiforgery token is stored in the hidden field and is posted back with a legitimate request:
当呈现为 HTML 时,防伪令牌存储在 hidden 字段中,并通过合法请求发回:

<form method="post">
<label>Amount</label>
<input type="number" name="amount" />
<button type="submit" >Withdraw funds</button>
<input name="__RequestVerificationToken" type="hidden"

value="CfDJ8Daz26qb0hBGsw7QCK"/>

</form>

ASP.NET Core automatically adds the antiforgery tokens to every form, and Razor Pages automatically validates them. The framework ensures that the antiforgery tokens exist in both the cookie and the form data, ensures that they match, and rejects any requests where they don’t.
ASP.NET Core 会自动将防伪令牌添加到每个表单,Razor Pages 会自动验证它们。该框架确保防伪令牌同时存在于 Cookie 和表单数据中,确保它们匹配,并拒绝任何不匹配的请求。

If you’re using Model-View-Controller (MVC) controllers with views instead of Razor Pages, ASP.NET Core still adds the antiforgery tokens to every form. Unfortunately, it doesn’t validate them for you. Instead, you must decorate your controllers and actions with the [ValidateAntiForgeryToken] attribute. This ensures that the antiforgery tokens exist in both the cookie and the form data, checks that they match, and rejects any requests in which they don’t.
如果将模型-视图-控制器 (MVC) 控制器与视图而不是 Razor Pages 一起使用,则 ASP.NET Core 仍会将防伪令牌添加到每个表单中。不幸的是,它不会为您验证它们。相反,您必须使用 [ValidateAntiForgeryToken] 属性修饰控制器和作。这可确保防伪令牌同时存在于 Cookie 和表单数据中,检查它们是否匹配,并拒绝它们不匹配的任何请求。

WARNING ASP.NET Core doesn’t automatically validate antiforgery tokens if you’re using MVC controllers with Views. You must make sure to mark all vulnerable methods with [ValidateAntiForgeryToken] attributes instead, as described in the “Prevent Cross-Site Request Forgery (XSRF/CSRF) attacks in ASP.NET Core” documentation: http://mng.bz/QPPv. Note that if you’re not using cookies for authentication, you are not vulnerable to CSRF attacks: CSRF attacks arise from attackers exploiting the fact that browsers automatically attach cookies to requests. No cookies, no problem!
警告:ASP.NET 如果您将 MVC 控制器与视图一起使用,Core 不会自动验证防伪令牌。您必须确保使用 [ValidateAntiForgeryToken] 属性标记所有易受攻击的方法,如“防止 ASP.NET Core 中的跨站点请求伪造 (XSRF/CSRF) 攻击”文档中所述:http://mng.bz/QPPv。请注意,如果您不使用 cookie 进行身份验证,则不易受到 CSRF 攻击:CSRF 攻击是由于攻击者利用浏览器自动将 cookie 附加到请求这一事实而引起的。没有 cookie,没问题!

Generally, you need to use antiforgery tokens only for POST, DELETE, and other dangerous request types that are used for modifying state. GET requests shouldn’t be used for this purpose, so the framework doesn’t require valid antiforgery tokens to call them. Razor Pages validates antiforgery tokens for dangerous verbs like POST and ignores safe verbs like GET. As long as you create your app following this pattern‌‌ (and you should!), the framework does the right thing to keep you safe.
通常,您只需将防伪令牌用于 POST、DELETE 和其他用于修改状态的危险请求类型。GET 请求不应用于此目的,因此框架不需要有效的防伪令牌来调用它们。Razor Pages 会验证危险动词(如 POST)的防伪令牌,并忽略安全动词(如 GET)。只要你按照这种模式创建你的应用程序(你应该这样做),框架就会做正确的事情来保证你的安全。

If you need to explicitly ignore antiforgery tokens on a Razor Page for some reason, you can disable the validation by applying the [IgnoreAntiforgeryToken] attribute to a Razor Page’s PageModel. This bypasses the framework protections for those cases when you’re doing something that you know is safe and doesn’t need protecting, but in most cases it’s better to play it safe and validate.‌
如果出于某种原因需要显式忽略 Razor 页面上的防伪令牌,可以通过将 [IgnoreAntiforgeryToken] 属性应用于 Razor 页面的 PageModel 来禁用验证。当您执行一些已知安全且不需要保护的作时,这将绕过框架保护,但在大多数情况下,最好谨慎行事并进行验证。

CSRF attacks can be a tricky thing to get your head around from a technical point of view, but for the most part everything should work without much effort on your part.
从技术角度来看,CSRF 攻击可能是一件棘手的事情,但在大多数情况下,一切都应该可以正常工作,而无需您付出太多努力。

Razor adds antiforgery tokens to your forms, and the Razor Pages framework takes care of validation for you.
Razor 将防伪令牌添加到您的表单中,Razor Pages 框架会为您处理验证。

Things get trickier if you’re making a lot of requests to an API using JavaScript, and you’re posting JavaScript Object Notation (JSON) objects rather than form data. In these cases, you won’t be able to send the verification token as part of a form (because you’re sending JSON), so you’ll need to add it as a header in the request instead. Microsoft’s documentation “Prevent Cross-Site Request Forgery (XSRF/ CSRF) attacks in ASP.NET Core” contains an example of adding the header in JavaScript and validating it in your application. See http://mng.bz/XNNa.‌
如果您使用 JavaScript 向 API 发出大量请求,并且您发布的是 JavaScript 对象表示法 (JSON) 对象而不是表单数据,那么事情就会变得更加棘手。在这些情况下,您将无法将验证令牌作为表单的一部分发送(因为您发送的是 JSON),因此您需要将其作为标头添加到请求中。Microsoft 的文档“防止 ASP.NET Core 中的跨站点请求伪造 (XSRF/CSRF) 攻击”包含在 JavaScript 中添加标头并在应用程序中验证它的示例。请参阅 http://mng.bz/XNNa

TIP If you’re not using cookie authentication and instead have a single-page application (SPA) that sends authentication tokens in a header, the good news is that you don’t have to worry about CSRF at all! Malicious sites can send only cookies, not headers, to your API, so they can’t make authenticated requests.
提示:如果您不使用 cookie 身份验证,而是拥有在 Headers 中发送身份验证令牌的单页应用程序 (SPA),那么好消息是,您根本不需要担心 CSRF!恶意网站只能向您的 API 发送 Cookie,而不能发送标头,因此它们无法发出经过身份验证的请求。

Generating unique tokens with the data protection APIs
使用数据保护 API 生成唯一令牌

The antiforgery tokens used to prevent CSRF attacks rely on the ability of the framework to use strong symmetric encryption to encrypt and decrypt data. Encryption algorithms typically rely on one or more keys, which are used to initialize the encryption and to make the process reproducible. If you have the key, you can encrypt and decrypt data; without it, the data is secure.
用于防止 CSRF 攻击的防伪令牌依赖于框架使用强对称加密来加密和解密数据的能力。加密算法通常依赖于一个或多个密钥,这些密钥用于初始化加密并使过程可重现。如果你有密钥,你可以加密和解密数据;没有它,数据是安全的。

In ASP.NET Core, encryption is handled by the data protection APIs. They’re used to create the antiforgery tokens, encrypt authentication cookies, and generate secure tokens in general. Crucially, they also control the management of the key files that are used for encryption. A key file is a small XML file that contains the random key value used for encryption in ASP.NET Core apps. It’s critical that it’s stored securely. If an attacker got hold of it, they could impersonate any user of your app and generally do bad things!
在 ASP.NET Core 中,加密由数据保护 API 处理。它们通常用于创建防伪令牌、加密身份验证 Cookie 和生成安全令牌。至关重要的是,它们还控制用于加密的密钥文件的管理。密钥文件是一个小型 XML 文件,其中包含用于在 ASP.NET Core 应用程序中加密的随机密钥值。安全存储至关重要。如果攻击者掌握了它,他们就可以冒充您应用程序的任何用户,并且通常会做坏事!

The data protection system stores the keys in a safe location, depending on how and where you host your app:
数据保护系统会将密钥存储在安全的位置,具体取决于您托管应用的方式和位置:

• Azure Web App—In a special synced folder, shared between regions
Azure Web 应用程序 - 位于特殊同步文件夹中,在区域之间共享

• IIS without user profile—Encrypted in the registry
没有用户配置文件的 IIS - 在注册表中加密

• Account with user profile—In %LOCALAPPDATA%\ASP.NET\DataProtection-Keys on Windows, or ~/.aspnet/DataProtection-Keys on Linux or macOS
具有用户配置文件的帐户 - 在 Windows 上位于 %LOCALAPPDATA%\ASP.NET\DataProtection-Keys 中,在 Linux 或 macOS 上位于 ~/.aspnet/DataProtection-Keys 中

• All other cases—In memory; when the app restarts, the keys will be lost
所有其他情况 - 在内存中;当应用程序重新启动时,密钥将丢失

So why do you care? For your app to be able to read your users’ authentication cookies, it must decrypt them by using the same key that was used to encrypt them. If you’re running in a web-farm scenario, by default each server has its own key and won’t be able to read cookies encrypted by other servers.
那么,您为什么关心呢?为了使您的应用程序能够读取用户的身份验证 Cookie,它必须使用用于加密用户的相同密钥对其进行解密。如果您在 Web 场方案中运行,则默认情况下,每个服务器都有自己的密钥,并且无法读取由其他服务器加密的 Cookie。

To get around this, you must configure your app to store its data protection keys in a central location. This could be a shared folder on a hard drive, a Redis instance, or an Azure blob storage instance, for example.
要解决此问题,您必须将应用程序配置为将其数据保护密钥存储在一个中心位置。例如,这可以是硬盘驱动器上的共享文件夹、Redis 实例或 Azure Blob 存储实例。

Microsoft’s documentation on the data protection APIs is extremely detailed, but it can be overwhelming. I recommend reading the section on configuring data protection, (“Configure ASP.NET Core Data Protection,” http://mng.bz/d40i) and configuring a key storage provider for use in a web- farm scenario (“Key storage providers in ASP.NET Core,” http://mng.bz/5pW6). I also have an introduction to the data protection APIs on my blog at http://mng.bz/yQQd.
Microsoft 关于数据保护 API 的文档非常详细,但可能会让人不知所措。我建议阅读有关配置数据保护的部分(“配置 ASP.NET Core 数据保护”,http://mng.bz/d40i 年)和配置用于 Web 场方案的密钥存储提供程序(“ASP.NET Core 中的密钥存储提供程序”,http://mng.bz/5pW6 年)。我还在我的博客 http://mng.bz/yQQd 上介绍了数据保护 API。

It’s worth clarifying that the CSRF vulnerability discussed in this section requires that a malicious site does a full form POST to your app. The malicious site can’t make the request to your API using client-side-only JavaScript, as browsers block JavaScript requests to your API that are from a different origin.
值得澄清的是,本节中讨论的 CSRF 漏洞要求恶意网站对您的应用程序执行完整形式的 POST。恶意站点无法使用仅限客户端的 JavaScript 向您的 API 发出请求,因为浏览器会阻止来自不同来源的 JavaScript 请求。

This is a safety feature, but it can often cause you problems. If you’re building a client-side SPA, or even if you have a little JavaScript on an otherwise server-side rendered app, you may need to make such cross-origin requests. In the next section I describe a common scenario you’re likely to run into and show how you can modify your apps to work around Pit.
这是一项安全功能,但它通常会给您带来麻烦。如果您正在构建客户端 SPA,或者即使您在其他服务器端呈现的应用程序上有一点 JavaScript,也可能需要发出此类跨域请求。在下一节中,我将介绍您可能会遇到的常见场景,并展示如何修改您的应用程序以解决 Pit 问题。

29.3 Calling your web APIs from other domains using CORS‌

29.3 使用 CORS 从其他域调用 Web API

In this section you’ll learn about cross-origin resource sharing (CORS), a protocol to allow JavaScript to make requests from one domain to another. CORS is a frequent area of confusion for many developers, so this section describes why it’s necessary and how CORS headers work. You’ll then learn how to add CORS to both your whole application and specific web API actions, and how to configure multiple CORS policies for your application.
在本节中,您将了解跨域资源共享 (CORS),这是一种允许 JavaScript 从一个域向另一个域发出请求的协议。CORS 是许多开发人员经常混淆的领域,因此本节介绍为什么需要 CORS 以及 CORS 标头的工作原理。然后,您将了解如何将 CORS 添加到整个应用程序和特定 Web API作,以及如何为应用程序配置多个 CORS 策略。

As you’ve already seen, CSRF attacks can be powerful, but they would be even more dangerous if it weren’t for browsers implementing the same-origin policy. This policy blocks apps from using JavaScript to call a web API at a different location unless the web API explicitly allows it.
正如你已经看到的,CSRF 攻击可能很强大,但如果不是浏览器实施同源策略,它们会更加危险。此政策禁止应用使用 JavaScript 调用位于其他位置的 Web API,除非 Web API 明确允许。

DEFINITION Origins are deemed to be the same if they match the scheme (HTTP or HTTPS), domain (example.com), and port (80 by default for HTTP and 443 for HTTPS). If an app attempts to access a resource using JavaScript, and the origins aren’t identical, the browser blocks the request.
定义:如果源与方案(HTTP 或 HTTPS)、域 (example.com) 和端口(HTTP 默认为 80,HTTPS 为 443)匹配,则认为源相同。如果应用程序尝试使用 JavaScript 访问资源,并且来源不相同,则浏览器会阻止该请求。

The same-origin policy is strict. The origins of the two URLs must be identical for the request to be allowed. For example, the following origins are the same:
同源策略很严格。两个 URL 的来源必须相同,才能允许请求。例如,以下来源是相同的:

http://example.com/home
http://example.com/site.css

The paths are different for these two URLs (/home and /site.css), but the scheme, domain, and port (80) are identical. So if you were on the home page of your app, you could request the /site.css file using JavaScript without any problems.
这两个 URL (/home 和 /site.css) 的路径不同,但 scheme、domain 和 port (80) 相同。因此,如果你在应用程序的主页上,你可以使用 JavaScript 请求 /site.css 文件,而不会出现任何问题。

By contrast, the origins of the following sites are different, so you couldn’t request any of these URLs using JavaScript from the http://example.com origin:
相比之下,以下网站的来源不同,因此您无法使用 JavaScript 从 http://example.com 来源请求这些 URL 中的任何一个:

https://example.com—Different scheme (https)

http://www.example.com—Different domain (includes a subdomain)

http://example.com:5000—Different port (default HTTP port is 80)

For simple apps, where you have a single web app handling all your functionality, this limitation might not be a problem, but it’s extremely common for an app to make requests to another domain. For example, you might have an e- commerce site hosted at http://shopping.com, and you’re attempting to load data from http://api.shop ping.com to display details about the products available for sale. With this configuration, you’ll fall foul of the same-origin policy.Any attempt to make a request using JavaScript to the API domain will fail, with an error similar to figure 29.6.
对于简单的应用程序,您有一个 Web 应用程序处理您的所有功能,此限制可能不是问题,但应用程序向另一个域发出请求的情况非常常见。例如,您可能在 http://shopping.com 上托管了一个电子商务网站,并且您正在尝试从 http://api.shop ping.com 加载数据以显示有关可供销售产品的详细信息。使用此配置,您将违反同源策略。任何使用 JavaScript 向 API 域发出请求的尝试都将失败,并出现类似于图 29.6 的错误。

alt text

Figure 29.6 The console log for a failed cross-origin request. Chrome has blocked a cross-origin request from the app http://shopping.com:6333 to the API at http://api.shopping.com:5111.
图 29.6 失败的跨域请求的控制台日志。Chrome 在 http://api.shopping.com:5111 时阻止了应用 http://shopping.com:6333 向 API 发出的跨域请求。

The need to make cross-origin requests from JavaScript is increasingly common with the rise of client-side SPAs and the move away from monolithic apps. Luckily, there’s a web standard that lets you work around this in a safe way; this standard is CORS. You can use CORS to control which apps can call your API, so you can enable scenarios like this one.
随着客户端 SPA 的兴起和从整体式应用程序的转变,从 JavaScript 发出跨域请求的需求越来越普遍。幸运的是,有一个 Web 标准可以让您以安全的方式解决这个问题;这个标准是 CORS。您可以使用 CORS 来控制哪些应用程序可以调用您的 API,因此您可以启用此类方案。

29.3.1 Understanding CORS and how it works‌

29.3.1 了解 CORS 及其工作原理

CORS is a web standard that allows your web API to make statements about who can make cross-origin requests to it. For example, you could make statements such as these:
CORS 是一种 Web 标准,它允许您的 Web API 声明谁可以向其发出跨域请求。例如,您可以做出如下陈述:

• Allow cross-origin requests from https://shopping.com and https://app.shopping.com.
允许来自 https://shopping.comhttps://app.shopping.com 的跨域请求。

• Allow only GET cross-origin requests.
仅允许 GET 跨域请求。

• Allow returning the Server header in responses to cross-origin requests.
允许在响应跨域请求时返回 Server 标头。

• Allow credentials (such as authentication cookies or authorization headers) to be sent with cross- origin requests.
允许通过跨域请求发送凭据 (例如身份验证 Cookie 或授权标头)。

You can combine these rules into a policy and apply different policies to different endpoints of your API. You could apply a policy to your entire application or a different policy to every API action.
您可以将这些规则合并到一个策略中,并将不同的策略应用于 API 的不同终端节点。您可以将策略应用于整个应用程序,也可以将不同的策略应用于每个 API作。

CORS works using HTTP headers. When your web API application receives a request, it sets special headers on the response to indicate whether cross-origin requests are allowed, which origins they’re allowed from, and which HTTP verbs and headers the request can use—pretty much everything about the request.
CORS 使用 HTTP 标头工作。当您的 Web API 应用程序收到请求时,它会在响应上设置特殊标头,以指示是否允许跨域请求、允许它们来自哪些来源以及请求可以使用哪些 HTTP 动词和标头 — 几乎涵盖了有关请求的所有内容。

In some cases, before sending a real request to your API, the browser sends a preflight request, a request sent using the OPTIONS verb, which the browser uses to check whether it’s allowed to make the real request. If the API sends back the correct headers, the browser sends the true cross-origin request, as shown in figure 29.7.‌
在某些情况下,在向 API 发送实际请求之前,浏览器会发送预检请求,即使用 OPTIONS 谓词发送的请求,浏览器使用该请求来检查是否允许发出实际请求。如果 API 发回正确的 Headers,则浏览器会发送真正的跨域请求,如图 29.7 所示。

alt text

Figure 29.7 Two cross-origin requests. The response to the GET request doesn’t contain any CORS headers, so the browser blocks the app from reading it, even though the response may contain data from the server. The second request requires a preflight OPTIONS request to check whether CORS is enabled. As the response contains CORS headers, the browser makes the real request and provides the response to the JavaScript app.
图 29.7 两个跨域请求。对 GET 请求的响应不包含任何 CORS 标头,因此浏览器会阻止应用程序读取它,即使响应可能包含来自服务器的数据。第二个请求需要预检 OPTIONS 请求来检查是否启用了 CORS。由于响应包含 CORS 标头,因此浏览器会发出真正的请求并向 JavaScript 应用程序提供响应。

TIP For a more detailed discussion of CORS, see CORS in Action, by Monsur Hossain (Manning, 2014), http://mng.bz/aD41.‌
提示:有关 CORS 的更详细讨论,请参阅 CORS in Action,Monsur Hossain 著(Manning,2014 年),http://mng.bz/aD41

The CORS specification, which you can find at http://mng.bz/MBBB, is complicated, with a variety of headers, processes, and terminology to contend with. Fortunately, ASP.NET Core handles the details of the specification for you, so your main concern is working out exactly who needs to access your API, and under what circumstances.
CORS 规范(您可以在 http://mng.bz/MBBB 上找到)很复杂,需要处理各种标头、流程和术语。幸运的是,ASP.NET Core 会为您处理规范的细节,因此您主要关心的是准确确定谁需要访问您的 API,以及在什么情况下需要访问您的 API。

29.3.2 Adding a global CORS policy to your whole app‌

29.3.2 向整个应用程序添加全局 CORS 策略

Typically, you shouldn’t set up CORS for your APIs until you need it. Browsers block cross-origin communication for a reason: it closes an avenue of attack. They’re not being awkward. Wait until you have an API hosted on a different domain to the app that needs to access it.
通常,除非需要,否则不应为 API 设置 CORS。浏览器阻止跨域通信是有原因的:它关闭了攻击途径。他们没有尴尬。等待,直到您将 API 托管在与需要访问它的应用程序不同的域上。

Adding CORS support to your application requires you to do four things:
向应用程序添加 CORS 支持需要您执行以下四项作:

• Add the CORS services to your app.
将 CORS 服务添加到应用程序。

• Configure at least one CORS policy.
至少配置一个 CORS 策略。

• Add the CORS middleware to your middleware pipeline.
将 CORS 中间件添加到您的中间件管道中。

• Set a default CORS policy for your entire app or decorate your endpoints with EnableCors metadata to selectively enable CORS for specific endpoints.
为整个应用程序设置默认 CORS 策略,或使用 EnableCors 元数据装饰终端节点,以选择性地为特定终端节点启用 CORS。

To add the CORS services to your application, call AddCors() on your WebApplicationBuilder instance in Program.cs:
要将 CORS 服务添加到应用程序中,请在 Program.cs 中的 WebApplicationBuilder 实例上调用 AddCors():

builder.Services.AddCors();

The bulk of your effort in configuring CORS will go into policy configuration. A CORS policy controls how your application responds to cross-origin requests. It defines which origins are allowed, which headers to return, which HTTP methods to allow, and so on. You normally define your policies inline when you add the CORS services to your application.
配置 CORS 的大部分工作将用于策略配置。CORS 策略控制应用程序如何响应跨域请求。它定义允许哪些源、要返回哪些标头、允许哪些 HTTP 方法等。通常在将 CORS 服务添加到应用程序时,以内联方式定义策略。

Consider the previous e-commerce site example. You want your API that is hosted at http://api.shopping.com to be available from the main app via client-side JavaScript, hosted at http://shopping.com. You therefore need to configure the API to allow cross-origin requests.
考虑前面的电子商务网站示例。您希望托管在 http://api.shopping.com 的 API 可以通过托管在 http://shopping.com 的客户端 JavaScript 从主应用程序访问。因此,您需要配置 API 以允许跨域请求。

NOTE Remember, it’s the app that will get errors when attempting to make cross-origin requests, but it’s the API you’re accessing that you need to add CORS to, not the app making the requests.
注意:请记住,在尝试发出跨域请求时,应用程序会遇到错误,但需要将 CORS 添加到您正在访问的 API 上,而不是发出请求的应用程序。

The following listing shows how to configure a policy called "AllowShoppingApp" to enable cross-origin requests from http://shopping.com to the API. Additionally, we explicitly allow any HTTP verb type; without this call, only simple methods (GET, HEAD, and POST) are allowed. The policies are built up using the familiar fluent builder style you’ve seen throughout this book.
以下清单显示了如何配置一个名为 “AllowShoppingApp” 的策略,以启用从 http://shopping.com 到 API 的跨域请求。此外,我们明确允许任何 HTTP 动词类型;如果没有此调用,则只允许使用简单的方法 (GET、HEAD 和 POST) 。这些策略是使用您在本书中看到的熟悉的 Fluent Builder 风格构建的。

Listing 29.2 Configuring a CORS policy to allow requests from a specific origin
示例 29.2 配置 CORS 策略以允许来自特定源的请求

public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options => { ❶
options.AddPolicy("AllowShoppingApp", policy => ❷
policy.WithOrigins("http://shopping.com") ❸
.AllowAnyMethod()); ❹
});
// other service configuration
}

❶ The AddCors method exposes an Action<CorsOptions> overload.
AddCors 方法公开Action<CorsOptions> 重载。

❷ Every policy has a unique name.
每个策略都有一个唯一的名称。

❸ The WithOrigins method specifies which origins are allowed. Note that the URL has no trailing /.
WithOrigins 方法指定允许的源。请注意,该 URL 没有尾部 /。

❹ Allows all HTTP verbs to call the API
允许所有 HTTP 动词调用 API

WARNING When listing origins in WithOrigins(), ensure that they don’t have a trailing "/"; otherwise, the origin will never match, and your cross-origin requests will fail.
警告:在 WithOrigins() 中列出源时,请确保它们没有尾随的 “/”;否则,源将永远不会匹配,并且您的跨源请求将失败。

Once you’ve defined a CORS policy, you can apply it to your application. In the following listing, you apply the "AllowShoppingApp" policy to the whole application using CorsMiddleware by calling UseCors().
定义 CORS 策略后,您可以将其应用于您的应用程序。在下面的清单中,通过调用 UseCors() 使用 CorsMiddleware 将 “AllowShoppingApp” 策略应用于整个应用程序。

Listing 29.3 Adding the CORS middleware and configuring a default CORS policy
清单 29.3 添加 CORS 中间件并配置默认 CORS 策略

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => {
options.AddPolicy("AllowShoppingApp", policy =>
policy.WithOrigins("http://shopping.com")
.AllowAnyMethod());
});
var app = builder.Build();
app.UseRouting();
app.UseCors("AllowShoppingApp"); ❶
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/products", () => new string[] {});
app.Run();

❶ Adds the CORS middleware and uses AllowShoppingApp as the default policy
添加 CORS 中间件并使用 AllowShoppingApp 作为默认策略

NOTE As with all middleware, the order of the CORS middleware is important. You must place the call to UseCors() after UseRouting(). The CORS middleware needs to intercept cross-origin requests to your web API actions so it can generate the correct responses to preflight requests and add the necessary headers. It’s common to place the CORS middleware before a call to UseAuthentication().
注意:与所有中间件一样,CORS 中间件的顺序也很重要。您必须在 UseRouting() 之后调用 UseCors()。CORS 中间件需要拦截对 Web API作的跨域请求,以便它可以生成对预检请求的正确响应并添加必要的标头。通常将 CORS 中间件放在调用 UseAuthentication() 之前。

With the CORS middleware in place for the API, the shopping app can now make cross-origin requests. You can call the API from the http://shopping.com site, and the browser lets the CORS request through, as shown in figure 29.8. If you make the same request from a domain other than http://shopping.com, the request continues to be blocked.
为 API 部署 CORS 中间件后,购物应用程序现在可以发出跨域请求。您可以从 http://shopping.com 站点调用 API,浏览器允许 CORS 请求通过,如图 29.8 所示。如果您从 http://shopping.com 以外的域发出相同的请求,该请求将继续被阻止。

alt text

Figure 29.8 With CORS enabled, as in the bottom image, cross-origin requests can be made, and the browser will make the response available to the JavaScript. Compare this to the top image, in which the request was blocked.
图 29.8 启用 CORS 后,如下图所示,可以发出跨域请求,并且浏览器会将响应提供给 JavaScript。将此图像与请求被阻止的顶部图像进行比较。

Applying a CORS policy globally to your application in this way may be overkill. If there’s only a subset of actions in your API that need to be accessed from other origins, it’s prudent to enable CORS only for those specific actions. This can be achieved by adding metadata to your endpoints.
以这种方式将 CORS 策略全局应用于您的应用程序可能有点矫枉过正。如果您的 API 中只有一个作子集需要从其他源访问,则谨慎的做法是仅为这些特定作启用 CORS。这可以通过向终端节点添加元数据来实现。

29.3.3 Adding CORS to specific endpoints with EnableCors metadata‌

29.3.3 使用 EnableCors 元数据将 CORS 添加到特定端点

Browsers block cross-origin requests by default for good reason: they have the potential to be abused by malicious or compromised sites. Enabling CORS for your entire app may not be worth the risk if you know that only a subset of actions will ever need to be accessed cross-origin.
默认情况下,浏览器会阻止跨域请求,这是有充分理由的:它们有可能被恶意或受感染的网站滥用。如果您知道只需要跨域访问一部分作,那么为整个应用程序启用 CORS 可能不值得冒险。

If that’s the case, it’s best to enable a CORS policy only for those specific endpoints. ASP.NET Core provides the RequireCors() method, which you can apply to your minimal API endpoints or route groups, and the [EnableCors] attribute, which lets you select a policy to apply to a given controller or action method.
如果是这种情况,最好仅为这些特定终端节点启用 CORS 策略。ASP.NET Core 提供了 RequireCors() 方法(可应用于最小 API 终端节点或路由组)和 [EnableCors] 属性(可用于选择要应用于给定控制器或作方法的策略)。

NOTE Both these methods add CORS metadata to the endpoint, which is used by the CorsMiddleware to determine the policy to apply. This is why the CorsMiddleware should be placed after the RoutingMiddleware, so that the CorsMiddleware knows which endpoint was selected and so which CORS policy to apply.
注意:这两种方法都会将 CORS 元数据添加到终端节点,CorsMiddleware 使用该元数据来确定要应用的策略。这就是为什么 CorsMiddleware 应该放在 RoutingMiddleware 之后,这样 CorsMiddleware 就知道选择了哪个端点,以及要应用哪个 CORS 策略。

With the RequireCors() method and [EnableCors] attribute, you can apply different CORS policies to different endpoints. For example, you could allow GET requests access to your entire API from the http://shopping.com domain but‌ allow other HTTP verbs only for a specific endpoint while allowing anyone to access your product list endpoints.
使用 RequireCors() 方法和 [EnableCors] 属性,您可以将不同的 CORS 策略应用于不同的端点。例如,您可以允许 GET 请求从 http://shopping.com 域访问您的整个 API,但仅允许特定终端节点使用其他 HTTP 动词,同时允许任何人访问您的产品列表终端节点。

You define CORS policies in the call to AddCors() by calling AddPolicy() and giving the policy a name, as you saw in listing 29.2. If you’re using endpoint-specific policies, instead of calling UseCors("AllowShoppingApp") as you saw in listing 29.3, you should add the middleware without a default policy by calling UseCors() only.
通过调用 AddPolicy() 并为策略命名,您可以在对 AddCors() 的调用中定义 CORS 策略,如清单 29.2 所示。如果您使用的是特定于端点的策略,而不是像您在清单 29.3 中看到的那样调用 UseCors(“AllowShoppingApp”),您应该仅通过调用 UseCors() 来添加没有默认策略的中间件。

You can then selectively enable CORS for individual endpoints and specifying the policy to apply. To apply CORS to a minimal API endpoint or route group, call RequireCors("AllowShoppingApp"), as shown in the following listing. To apply a policy to a controller or an action method, apply the [EnableCors("AllowShoppingApp"] attribute. You can disable cross-origin access for an endpoint by applying the [DisableCors] attribute.
然后,您可以有选择地为单个终端节点启用 CORS 并指定要应用的策略。要将 CORS 应用于最小 API 终端节点或路由组,请调用 RequireCors(“AllowShoppingApp”),如下面的清单所示。要将策略应用于控制器或作方法,请应用 [EnableCors(“AllowShoppingApp”] 属性。您可以通过应用 [DisableCors] 属性来禁用终端节点的跨域访问。

Listing 29.4 Applying a CORS policy to minimal API endpoints
清单 29.4 将 CORS 策略应用于最小的 API 端点

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options => { /* Config not shown*/});
var app = builder.Build();
app.UseCors(); ❶
app.MapGet("/api/products", () => new string[] {})
.RequireCors("AllowShoppingApp"); ❷
app.MapGet("/api/products",
[EnableCors("AllowShoppingApp")] () => new { }); ❸
app.MapGroup("/api/categories")
.RequireCors("AllowAnyOrigin"); ❹
app.MapDelete("/api/products",
[DisableCors] () => Results.NoContent()); ❺
app.Run();

❶ Adds the CorsMiddleware without configuring a default policy
添加 CorsMiddleware 而不配置默认策略

❷ Applies the AllowShoppingApp CORS policy to the endpoint
将 AllowShoppingApp CORS 策略应用于终端节点

❸ You can apply attributes to the lamba or handler method, as well as to MVC action methods.
您可以将属性应用于 lamba 或处理程序方法,以及 MVC动作方法。

❹ You can apply CORS policies to whole route groups.
您可以将 CORS 策略应用于整个路由组。

❺ The DisableCors attribute disables CORS for the endpoint completely.
DisableCors 属性完全禁用终端节点的 CORS。

If you define a default policy but then also call RequireCors() or add an [EnableCors] attribute, then both policies are applied. This can get confusing, so I recommend not applying a default CORS policy in the middleware and specifying the policy at the route group or endpoint level. Alternatively, if you do want to apply a policy to your whole app, avoid applying individual policies to endpoints as well.
如果定义了默认策略,但随后还调用 RequireCors() 或添加 [EnableCors] 属性,则会应用这两个策略。这可能会造成混淆,因此我建议不要在中间件中应用默认 CORS 策略,而是在路由组或终端节点级别指定策略。或者,如果您确实希望将策略应用于整个应用程序,请避免将单个策略也应用于终端节点。

Whether you choose to use a single default CORS policy or multiple policies, you need to configure the CORS policies for your application in the call to AddCors. Many options are available when configuring CORS. In the next section I provide an overview of the possibilities.
无论您选择使用单个默认 CORS 策略还是多个策略,都需要在对 AddCors 的调用中为应用程序配置 CORS 策略。配置 CORS 时,有许多选项可用。在下一节中,我将概述各种可能性。

29.3.4 Configuring CORS policies‌

29.3.4 配置 CORS 策略

Browsers implement the cross-origin policy for security reasons, so you should carefully consider the implications of relaxing any of the restrictions they impose. Even if you enable cross-origin requests, you can still control what data cross-origin requests can send and what your API returns. For example, you can configure
浏览器出于安全原因实施跨域策略,因此您应该仔细考虑放宽它们施加的任何限制的影响。即使您启用了跨域请求,您仍然可以控制跨域请求可以发送的数据以及 API 返回的数据。例如,您可以配置

• The origins that may make a cross-origin request to your API
可能向您的 API 发出跨源请求的源

• The HTTP verbs (such as GET, POST, and DELETE) that can be used
可以使用的 HTTP 动词 (如 GET、POST 和 DELETE)

• The headers the browser can send
浏览器可以发送的标头

• The headers the browser can read from your app’s response
浏览器可以从应用的响应中读取的标头

• Whether the browser will send authentication credentials with the request
浏览器是否会随请求发送身份验证凭证

You define all these options when creating a CORS policy in your call to AddCors() using the CorsPolicyBuilder, as you saw in listing 29.2. A policy can set all or none of these options, so you can customize the results to your heart’s content. Table 29.1 shows some of the options available and their effects.
使用 CorsPolicyBuilder 在调用 AddCors() 中创建 CORS 策略时,您可以定义所有这些选项,如清单 29.2 所示。策略可以设置所有这些选项,也可以不设置这些选项,因此您可以根据自己的喜好自定义结果。Table 29.1 显示了一些可用的选项及其效果。

Table 29.1 The methods available for configuring a CORS policy and their effect on the policy
表 29.1 可用于配置 CORS 策略的方法及其对策略的影响

CorsPolicyBuilder method example Result
WithOrigins("http://shopping.com") Allows cross-origin requests from http:/ /shopping.com
允许来自 http:/ /shopping.com 的跨域请求
AllowAnyOrigin() Allows cross-origin requests from any origin. This means any website can make JavaScript requests to your API.
允许来自任何源的跨域请求。这意味着任何网站都可以向您的 API 发出 JavaScript 请求。
WithMethods()/AllowAnyMethod() Sets the allowed methods (such as GET, POST, and DELETE) that can be made to your API
设置允许对 API 进行的方法(例如 GET、POST 和 DELETE)
WithHeaders()/AllowAnyHeader() Sets the headers that the browser may send to your API. If you restrict the headers, you must include at least Accept, Content-Type, and Origin to allow valid requests.
设置浏览器可以发送到 API 的标头。如果您限制标头,则必须至少包含 Accept、Content-Type 和 Origin 才能允许有效请求。
WithExposedHeaders() Allows your API to send extra headers to the browser. By default, only the Cache-Control, Content-Language,Content-Type, Expires, Last-Modified,and Pragma headers are sent in the response.
允许 API 向浏览器发送额外的标头。默认情况下,响应中仅发送 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified 和 Pragma 标头。
AllowCredentials() By default, the browser won’t send authentication details with cross- origin requests unless you explicitly allow it. You must also enable sending credentials client-side in JavaScript when making the request.
默认情况下,除非您明确允许,否则浏览器不会通过跨域请求发送身份验证详细信息。发出请求时,您还必须在 JavaScript 中启用客户端发送凭证。

One of the first problems in setting up CORS is realizing you have a cross-origin problem at all. Several times I’ve been stumped trying to figure out why a request won’t work, until I realize the request is going cross-domain or from HTTP to HTTPS, for example.
设置 CORS 的首要问题之一是意识到您根本存在跨域问题。有好几次,我试图弄清楚为什么一个请求不起作用,直到我意识到请求是跨域的,或者从 HTTP 到 HTTPS,例如。

Whenever possible, I recommend avoiding cross-origin requests. You can end up with subtle differences in the way browsers handle them, which can cause more headaches. In particular, avoid HTTP to HTTPS cross-domain problems by running all your applications behind HTTPS. As discussed in chapter 28, that’s a best practice anyway, and it’ll help prevent a whole class of CORS headaches.
我建议尽可能避免跨源请求。您最终可能会在浏览器处理它们的方式上产生细微的差异,这可能会导致更多麻烦。特别是,通过在 HTTPS 后面运行所有应用程序来避免 HTTP 到 HTTPS 的跨域问题。正如第 28 章所讨论的,无论如何,这都是最佳实践,它将有助于防止一整类 CORS 头痛。

TIP Another (often preferable) option is to configure CORS policies in your reverse proxy or application gateway. You can configure Azure App Service with allowed origins, for example, so that you don’t need to modify your application code.
提示:另一个(通常更可取的)选项是在反向代理或应用程序网关中配置 CORS 策略。例如,可以使用允许的源配置 Azure 应用服务,这样就无需修改应用程序代码。

Once I’ve established that I definitely need a CORS policy, I typically start with the WithOrigins() method. Then I expand or restrict the policy further, as need be, to provide cross-origin lockdown of my API while still allowing the required functionality. CORS can be tricky to work around, but remember, the restrictions are there for your safety.
一旦我确定我肯定需要一个 CORS 策略,我通常从 WithOrigins() 方法开始。然后,我根据需要进一步扩展或限制策略,以提供 API 的跨域锁定,同时仍然允许所需的功能。CORS 可能很难解决,但请记住,这些限制是为了您的安全。

Cross-origin requests are only one of many potential avenues attackers could use to compromise your app. Many of these are trivial to defend against, but you need to be aware of them and know how to mitigate them. In the next section we’ll look at common threats and how to avoid them.
跨域请求只是攻击者可能用来破坏您的应用的众多潜在途径之一。其中许多是微不足道的防御,但您需要了解它们并知道如何减轻它们。在下一节中,我们将介绍常见的威胁以及如何避免它们。

29.4 Exploring other attack vectors‌

29.4 探索其他攻击媒介

So far in this chapter, I’ve described two potential ways attackers can compromise your apps—XSS and CSRF attacks and how to prevent them. Both of these vulnerabilities regularly appear in the OWASP top ten list of most critical web app risks, so it’s important to be aware of them and to avoid introducing them into your apps.
到目前为止,在本章中,我已经介绍了攻击者破坏您的应用程序的两种潜在方式 — XSS 和 CSRF 攻击以及如何预防它们。这两个漏洞经常出现在 OWASP 十大最关键的 Web 应用程序风险列表中,因此了解它们并避免将它们引入您的应用程序非常重要。

TIP OWASP publishes the list online, with descriptions of each attack and how to prevent those attacks. There’s a cheat sheet for staying safe here: https://cheatsheetseries.owasp.org.
提示:OWASP 在线发布该列表,其中包含每种攻击的描述以及如何防止这些攻击。这里有一张保持安全的备忘单:https://cheatsheetseries.owasp.org

In this section I’ll provide an overview of some of the other most common vulnerabilities and how to avoid them in your apps.
在本节中,我将概述其他一些最常见的漏洞,以及如何在您的应用程序中避免它们。

29.4.1 Detecting and avoiding open redirect attacks‌

29.4.1 检测和避免开放重定向攻击

A common OWASP vulnerability is due to open redirect attacks. An open redirect attack occurs when a user clicks a link to an otherwise-safe app and ends up being redirected to a malicious website, such as one that serves malware. The safe app contains no direct links to the malicious website, so how does this happen?
一个常见的 OWASP 漏洞是由于开放重定向攻击造成的。当用户点击指向其他安全应用程序的链接并最终被重定向到恶意网站(例如提供恶意软件的网站)时,就会发生开放重定向攻击。安全应用程序不包含指向恶意网站的直接链接,那么这是怎么发生的呢?

Open redirect attacks occur where the next page is passed as a parameter to an endpoint. The most common example is when you’re logging in to an app. Typically, apps remember the page a user is on before redirecting them to a login page by passing the current page as a returnUrl query string parameter. After the user logs in, the app redirects the user to the returnUrl to carry on where they left off.
当下一页作为参数传递给终端节点时,会发生开放重定向攻击。最常见的示例是当您登录应用程序时。通常,应用程序会记住用户所在的页面,然后通过将当前页面作为 returnUrl 查询字符串参数传递,将用户重定向到登录页面。用户登录后,应用程序会将用户重定向到 returnUrl 以从他们离开的位置继续。

Imagine a user is browsing an e-commerce site. They click Buy for a product and are redirected to the login page. The product page they were on is passed as the returnUrl, so after they log in, they’re redirected to the product page instead of being dumped back to the home screen.
假设用户正在浏览一个电子商务网站。他们单击产品的 Buy (购买) 并被重定向到登录页面。他们所在的产品页面作为 returnUrl 传递,因此在他们登录后,他们会被重定向到产品页面,而不是被转储回主屏幕。

An open redirect attack takes advantage of this common pattern, as shown in figure 29.9. A malicious attacker creates a login URL where the returnUrl is set to the website they want to send the user to and convinces the user to click the link to your web app. After the user logs in, a vulnerable app redirects the user to the malicious site.
开放重定向攻击利用了这种常见模式,如图 29.9 所示。恶意攻击者创建一个登录 URL,其中 returnUrl 设置为他们要将用户发送到的网站,并说服用户单击指向您的 Web 应用程序的链接。用户登录后,易受攻击的应用程序会将用户重定向到恶意站点。

alt text

Figure 29.9 An open redirect makes use of the common return URL pattern. This is typically used for login pages but may be used in other areas of your app too. If your app doesn’t verify that the URL is safe before redirecting the user, it could redirect users to malicious sites.
图 29.9 开放重定向使用常见的返回 URL 模式。这通常用于登录页面,但也可能用于应用程序的其他区域。如果您的应用程序在重定向用户之前未验证 URL 是否安全,则可能会将用户重定向到恶意网站。

The simple solution to this attack is to always validate that the returnUrl is a local URL that belongs to your app before redirecting users to it. The default Identity UI does this already, so you shouldn’t have to worry about the login page if you’re using Identity, as described in chapter 23.
这种攻击的简单解决方案是在将用户重定向到 returnUrl 之前,始终验证 returnUrl 是否是属于您的应用程序的本地 URL。默认的 Identity UI 已经这样做了,因此如果您使用的是 Identity,则不必担心登录页面,如第 23 章所述。

If you have redirects in other parts of your app, ASP.NET Core provides a couple of helper methods for staying safe, the most useful of which is Url.IsLocalUrl(). Listing 29.5 shows how you could verify that a provided return URL is safe and, if not, redirect to the app’s home page.
如果您在应用程序的其他部分有重定向,ASP.NET Core 提供了几个帮助程序方法来保持安全,其中最有用的是 Url.IsLocalUrl()。清单 29.5 显示了如何验证提供的返回 URL 是否安全,如果不是,则重定向到应用程序的主页。

You can also use the LocalRedirect() helper method on the ControllerBase and Razor Page PageModel classes, which throw an exception if the provided URL isn’t local.‌‌
还可以在 ControllerBase 和 Razor Page PageModel 类上使用 LocalRedirect() 帮助程序方法,如果提供的 URL 不是本地的,则会引发异常。

Listing 29.5 Detecting open redirect attacks by checking for local return URLs
清单 29.5 通过检查本地返回 URL 来检测开放重定向攻击

[HttpPost]
public async Task<IActionResult> Login(
LoginViewModel model, string returnUrl = null) ❶
{
// Verify password, and sign user in
if (Url.IsLocalUrl(returnUrl)) ❷
{
return Redirect(returnUrl); ❸
}
else
{
return RedirectToPage("Index"); ❹
}
}

❶ The return URL is provided as an argument to the action method.
返回 URL 作为作方法的参数提供。

❷ Returns true if the return URL starts with / or ~/
如果返回 URL 以 / 或 ~/开头,则返回 true

❸ The URL is local, so it’s safe to redirect to it.
该 URL 是本地的,因此可以安全地重定向到它。

❹ The URL was not local and could be an open redirect attack, so redirect to the homepage for safety.
该 URL 不是本地的,可能是公开重定向攻击,因此为了安全起见,请重定向到主页。

This simple pattern protects against open redirect attacks that could otherwise expose your users to malicious content. Whenever you’re redirecting to a URL that comes from a query string or other user input, you should use this pattern.
这种简单的模式可以防止开放重定向攻击,否则可能会使您的用户接触到恶意内容。每当重定向到来自查询字符串或其他用户输入的 URL 时,都应使用此模式。

TIP In some authentication flows, such as when authenticating with OpenID Connect, you can’t redirect to a local URL, so you can’t use this pattern. Instead, OpenID Connect requires that you preregister the allowed redirect URLs and redirect only to a registered URL. You should consider using this pattern when you can’t enforce a local- only redirect.
提示:在某些身份验证流中,例如使用 OpenID Connect 进行身份验证时,您无法重定向到本地 URL,因此不能使用此模式。相反,OpenID Connect 要求您预先注册允许的重定向 URL,并且仅重定向到已注册的 URL。当您无法强制执行仅限本地的重定向时,您应该考虑使用此模式。

Open redirect attacks present a risk to your users rather than to your app directly. The next vulnerability represents a critical vulnerability in your app itself.
开放重定向攻击会给您的用户带来风险,而不是直接给您的应用程序带来风险。下一个漏洞表示应用程序本身的严重漏洞。

29.4.2 Avoiding SQL injection attacks with EF Core and parameterization‌

29.4.2 使用 EF Core 和参数化避免 SQL 注入攻击

SQL injection attacks represent one of the most dangerous threats to your application. Attackers craft simple malicious input, which they send to your application as traditional form-based input or by customizing URLs and query strings to execute arbitrary code against your database. An SQL injection vulnerability could expose your entire database to attackers, so it’s critical that you spot and remove any such vulnerabilities in your apps.
SQL 注入攻击是应用程序面临的最危险的威胁之一。攻击者制作简单的恶意输入,这些输入作为传统的基于表单的输入发送到您的应用程序,或者通过自定义 URL 和查询字符串来针对您的数据库执行任意代码。SQL 注入漏洞可能会将您的整个数据库暴露给攻击者,因此发现并删除应用程序中的任何此类漏洞至关重要。

I hope I’ve scared you a little with that introduction, so now for the good news: if you’re using Entity Framework Core (EF Core) or pretty much any other object-relational mapper (ORM) in a standard way, you should be safe. EF Core has built-in protections against SQL injection, so as long as you’re not doing anything funky, you should be fine.
我希望我的介绍让您有点害怕,所以现在好消息是:如果您以标准方式使用 Entity Framework Core (EF Core) 或几乎任何其他对象关系映射器 (ORM),您应该是安全的。EF Core 具有针对 SQL 注入的内置保护功能,因此只要您不做任何时髦的事情,应该没问题。

SQL injection vulnerabilities occur when you build SQL statements yourself and include dynamic input that an attacker provides, even indirectly. EF Core provides the ability to create raw SQL queries using the FromSqlRaw() method, so you must be careful when using this method.
当您自己构建 SQL 语句并包含攻击者提供的动态输入(甚至是间接提供的)时,就会出现 SQL 注入漏洞。EF Core 提供了使用 FromSqlRaw() 方法创建原始 SQL 查询的功能,因此在使用此方法时必须小心。

Imagine your recipe app has a search form that lets you search for a recipe by name. If you write the query using LINQ extension methods (as discussed in chapter 12), you would have no risk of SQL injection attacks. However, if you decide to write your SQL query by hand, you open yourself to such a vulnerability, as shown in the following listing.
假设您的食谱应用程序有一个搜索表单,可让您按名称搜索食谱。如果使用 LINQ 扩展方法编写查询(如第 12 章所述),则不会有 SQL 注入攻击的风险。但是,如果您决定手动编写 SQL 查询,则可能会面临此类漏洞,如下面的清单所示。

Listing 29.6 An SQL injection vulnerability in EF Core due to string concatenation
列表 29.6 由于字符串串联而导致的 EF Core 中的 SQL 注入漏洞

public IList<User> FindRecipe(string search) ❶
{
return _context.Recipes ❷
.FromSqlRaw("SELECT * FROM Recipes" + ❸
"WHERE Name = '" + search + "'") ❹
.ToList();
}

❶ The search parameter comes from user input, so it’s unsafe.
search 参数来自用户输入,因此不安全。

❷ The current EF Core DbContext is held in the _context field.
当前 EF Core DbContext 保存在 _context 字段中。

❸ You can write queries by hand using the FromSqlRaw extension method.
您可以使用 FromSqlRaw 扩展方法手动编写查询。

❹ This introduces the vulnerability—including unsafe content directly in an SQL string.
这会引入漏洞 — 直接在 SQL字符串中包含不安全的内容。

In this listing, the user input held in search is included directly in the SQL query. By crafting malicious input, users can potentially perform any operation on your database.
在此清单中,搜索中保存的用户输入直接包含在 SQL 查询中。通过精心设计恶意输入,用户可能会对您的数据库执行任何作。

Imagine an attacker searches your website using the text
想象一下,攻击者使用文本

'; DROP TABLE Recipes; --

Your app assigns this to the search parameter, and the SQL query executed against your database becomes
您的应用程序将此参数分配给 search 参数,并且针对您的数据库执行的 SQL 查询将变为

SELECT * FROM Recipes WHERE Name = ''; DROP TABLE Recipes; --'

Simply by entering text into the search form of your app, the attacker has deleted the entire Recipes table from your app! That’s catastrophic, but an SQL injection vulnerability provides more or less unfettered access to your database.Even if you’ve set up database permissions correctly to prevent this sort of destructive action, attackers will likely be able to read all the data from your database, including your users’ details.
只需在应用的搜索表单中输入文本,攻击者就从您的应用中删除了整个 Recipes 表!这是灾难性的,但 SQL 注入漏洞或多或少提供了对数据库的不受限制的访问。即使您已正确设置数据库权限以防止此类破坏性作,攻击者也可能能够从您的数据库读取所有数据,包括您的用户的详细信息。

The simple way to prevent this from happening is to avoid creating SQL queries by hand this way. If you do need to write your own SQL queries, don’t use string concatenation, as in listing 29.6. Instead, use parameterized queries, in which the (potentially unsafe) input data is separate from the query itself, as shown here.
防止这种情况发生的简单方法是避免以这种方式手动创建 SQL 查询。如果你确实需要编写自己的 SQL 查询,请不要使用字符串连接,如清单 29.6 所示。相反,请使用参数化查询,其中(可能不安全的)输入数据与查询本身是分开的,如下所示。

Listing 29.7 Avoiding SQL injection by using parameterization
示例 29.7 使用参数化避免 SQL 注入

public IList<User> FindRecipe(string search)
{
return _context.Recipes
.FromSqlRaw( "SELECT * FROM Recipes WHERE Name = '{0}'", ❶
search) ❷
.ToList();
}

❶ The SQL query uses a placeholder {0} for the parameter.
SQL 查询使用参数的占位符{0}。

❷ The dangerous input is passed as a parameter, separate from the query.
危险输入作为参数传递,与查询分开。

Parameterized queries are not vulnerable to SQL injection attacks, so the attack presented earlier won’t work. If you use EF Core or other ORMs to access data using standard LINQ queries, you won’t be vulnerable to injection attacks. EF Core automatically creates all SQL queries using parameterized queries to protect you. Even if you’re using the low-level ADO.NET database APIs, stick to parameterized queries!
参数化查询不易受到 SQL 注入攻击,因此前面介绍的攻击不起作用。如果使用 EF Core 或其他 ORM 通过标准 LINQ 查询访问数据,则不会容易受到注入攻击。EF Core 使用参数化查询自动创建所有 SQL 查询以保护你。即使您使用的是低级 ADO.NET 数据库 API,也请坚持使用参数化查询!

NOTE I’ve talked about SQL injection attacks only in terms of a relational database, but this vulnerability can appear in NoSQL and document databases too. Always use parameterized queries or the equivalent, and don’t craft queries by concatenating strings with user input.
注意:我仅从关系数据库的角度讨论了 SQL 注入攻击,但此漏洞也可能出现在 NoSQL 和文档数据库中。始终使用参数化查询或等效查询,并且不要通过将字符串与用户输入连接起来来创建查询。

Injection attacks have been the number-one vulnerability on the web for more than a decade, so it’s crucial to be aware of them and how they arise. Whenever you need to write raw SQL queries, make sure that you always use parameterized queries.
十多年来,注入攻击一直是 Web 上的头号漏洞,因此了解它们及其出现方式至关重要。每当需要编写原始 SQL 查询时,请确保始终使用参数化查询。

The next vulnerability is also related to attackers accessing data they shouldn’t be able to. It’s a little subtler than a direct injection attack but is trivial to perform; the only skill the attacker needs is the ability to count.
下一个漏洞还与攻击者访问他们不应该访问的数据有关。它比直接注入攻击更微妙一些,但执行起来很简单;攻击者唯一需要的技能是计数能力。

29.4.3 Preventing insecure direct object references‌

29.4.3 防止不安全的直接对象引用

Insecure direct object reference is a bit of a mouthful, but it means users accessing things they shouldn’t by noticing patterns in URLs. Let’s revisit our old friend the recipe app. As a reminder, the app shows you a list of recipes. You can view any of them, but you can edit only recipes you created yourself. When you view someone else’s recipe, there’s no Edit button visible.‌
不安全的直接对象引用有点拗口,但这意味着用户通过注意到 URL 中的模式来访问他们不应该访问的内容。让我们重温一下我们的老朋友食谱应用程序。提醒一下,该应用程序会向您显示食谱列表。您可以查看其中任何一个,但只能编辑您自己创建的配方。当您查看其他人的配方时,没有可见的 Edit (编辑) 按钮。

A user clicks the Edit button on one of their recipes and notices that the URL is /Recipes/Edit/120. That 120 is a dead giveaway as being the underlying database ID of the entity you’re editing. A simple attack would be to change that ID to gain access to a different entity, one that you wouldn’t normally have access to. The user could try entering /Recipes/Edit/121. If that lets them edit or view a recipe that they shouldn’t be able to, you have an insecure direct object reference vulnerability.
用户单击其中一个配方上的 Edit (编辑) 按钮,并注意到 URL 为 /Recipes/Edit/120。这 120 是一个死的赠品,因为这是您正在编辑的实体的基础数据库 ID。一个简单的攻击是更改该 ID 以获得对不同实体的访问权限,该实体通常无权访问。用户可以尝试输入 /Recipes/Edit/121。如果这允许他们编辑或查看他们不应该能够编辑或查看的配方,则您存在不安全的直接对象引用漏洞。

The solution to this problem is simple: you should have resource-based authorization in your endpoint handlers. If a user attempts to access an entity they’re not allowed to access, they should get a permission-denied error. They shouldn’t be able to bypass your authorization by typing a URL directly into the search bar of their browser.
此问题的解决方案很简单:您应该在终端节点处理程序中具有基于资源的授权。如果用户尝试访问不允许他们访问的实体,他们应该会收到 permission-denied 错误。他们不应该能够通过在浏览器的搜索栏中直接输入 URL 来绕过您的授权。

In ASP.NET Core apps, this vulnerability typically arises when you attempt to restrict users by hiding elements from your UI, such as by hiding the Edit button. Instead, you should use resource-based authorization, as discussed in chapter 24.
在 ASP.NET Core 应用程序中,当您尝试通过隐藏 UI 中的元素(例如隐藏 Edit 按钮)来限制用户时,通常会出现此漏洞。相反,您应该使用基于资源的授权,如 Chapter 24 中所述。

WARNING You must always use resource-based authorization to restrict which entities a user can access. Hiding or disabling UI elements provides an improved user experience, but it isn’t a security measure.
警告您必须始终使用基于资源的授权来限制用户可以访问的实体。隐藏或禁用 UI 元素可以提供更好的用户体验,但这不是一项安全措施。

You can sidestep this vulnerability somewhat by avoiding integer IDs for your entities in the URLs, perhaps by using a pseudorandom globally unique identifier (GUID) such as C2E296BA-7EA8-4195-9CA7-C323304CCD12 instead.
您可以通过避免在 URL 中使用实体的整数 ID 来稍微回避此漏洞,也许可以改用伪随机全局唯一标识符 (GUID),例如 C2E296BA-7EA8-4195-9CA7-C323304CCD12。

This makes the process of guessing other entities harder, as you can’t simply add 1 to an existing number, but it’s masking the problem rather than fixing it. Nevertheless, using GUIDs can be useful when you want to have publicly accessible pages that don’t require authentication but don’t want their IDs to be easily discoverable.
这使得猜测其他实体的过程更加困难,因为你不能简单地将 1 添加到现有数字上,但它掩盖了问题,而不是解决问题。不过,当您希望拥有不需要身份验证但又不希望其 ID 易于发现的可公开访问页面时,使用 GUID 可能很有用。

The final section in this chapter doesn’t deal with a single vulnerability. Instead, I discuss a separate but related problem: protecting your users’ data.
本章的最后一节不涉及单个漏洞。相反,我讨论了一个单独但相关的问题:保护用户的数据。

29.4.4 Protecting your users’ passwords and data‌

29.4.4 保护用户的口令和数据

For many apps, the most sensitive data you’ll be storing is the personal data of your users. This could include emails, passwords, address details, or payment information. You should be careful when storing any of this data. As well as presenting an inviting target for attackers, you may have legal obligations for how you handle it, such as data protection laws and Payment Card Industry (PCI) compliance requirements.
对于许多应用程序,您将存储的最敏感数据是用户的个人数据。这可能包括电子邮件、密码、地址详细信息或付款信息。在存储任何此类数据时,您应该小心。除了为攻击者提供诱人的目标外,您可能还对如何处理它负有法律义务,例如数据保护法和支付卡行业 (PCI) 合规性要求。

The easiest way to protect yourself is to not store data you don’t need. If you don’t need your user’s address, don’t ask for it. That way, you can’t lose it! Similarly, if you use a third- party identity service to store user details, as described in chapter 23, you won’t have to work as hard to protect your users’ personal information.
保护自己的最简单方法是不存储您不需要的数据。如果您不需要用户的地址,请不要询问。这样,你就不会丢失它!同样,如果您使用第三方身份服务来存储用户详细信息,如第 23 章所述,则不必费力地保护用户的个人信息。

If you store user details in your own app or build your own identity provider, then you need to make sure to follow best practices when handling user information. The new project templates that use ASP.NET Core Identity follow most of these practices by default, so I highly recommend you start from one of these. You need to consider many aspects, too many to go into detail here,1 but they include the following:
如果您将用户详细信息存储在自己的应用程序中或构建自己的身份提供商,则需要确保在处理用户信息时遵循最佳实践。默认情况下,使用 ASP.NET Core Identity 的新项目模板遵循其中的大部分做法,因此我强烈建议您从其中一种做法开始。您需要考虑许多方面,太多了,无法在这里详细介绍,1但它们包括以下内容:

• Never store user passwords anywhere directly. You should store only cryptographic hashes computed using an expensive hashing algorithm, such as BCrypt or PBKDF2.
切勿将用户密码直接存储在任何位置。您应该只存储使用昂贵的哈希算法(如 BCrypt 或 PBKDF2)计算的加密哈希。

• Don’t store more data than you need. You should never store credit card details.
不要存储超出您需要的数据。您永远不应该存储信用卡详细信息。

• Allow users to use multifactor authentication (MFA) to sign in to your site.
允许用户使用多重身份验证 (MFA) 登录您的网站。

• Prevent users from using passwords that are known to be weak or compromised, such as disallowing dictionary words, sequential characters, and so on.
防止用户使用已知较弱或已泄露的密码,例如不允许使用字典单词、连续字符等。

• Mark authentication cookies as http (so that they can’t be read using JavaScript) and secure so they’ll be sent only over an HTTPS connection, never over HTTP. Where possible, you should also mark your cookies as SameSite=strict. See the documentation for details: http://mng.bz/a11m.
将身份验证 Cookie 标记为 http(这样就无法使用 JavaScript 读取它们)和安全,这样它们将仅通过 HTTPS 连接发送,而不是通过 HTTP。在可能的情况下,还应将 Cookie 标记为 SameSite=strict。有关详细信息,请参阅文档:http://mng.bz/a11m

• Don’t expose whether a user is already registered with your app. Leaking this information can expose you to enumeration attacks.
不要暴露用户是否已在您的应用程序中注册。泄露此信息可能会使您面临枚举攻击。

TIP You can learn more about website enumeration in this video tutorial by Troy Hunt: http://mng.bz/PAAA.
提示:您可以在 Troy Hunt 提供的此视频教程中了解有关网站枚举的更多信息:http://mng.bz/PAAA

These guidelines represent the minimum you should be doing to protect your users. The most important thing is to be aware of potential security problems as you’re building your app. Trying to bolt on security at the end is always harder than thinking about it from the start, so it’s best to think about it earlier rather than later.
这些准则代表了为保护用户而应采取的最低限度作。最重要的是在构建应用程序时了解潜在的安全问题。试图在最后加强安全性总是比从一开始就考虑它更难,因此最好尽早考虑而不是晚点考虑。

This chapter has been a whistle-stop tour of things to look out for. We’ve touched on most of the big names in security vulnerabilities, but I strongly encourage you to check out the other resources mentioned in this chapter. They provide a more exhaustive list of things to consider, complementing the defenses mentioned in this chapter. On top of that, don’t forget about input validation and mass assignment/overposting, as discussed in chapter 16. ASP.NET Core includes basic protections against some of the most common attacks, but you can still shoot yourself in the foot. Make sure it’s not your app making headlines for being breached!
本章是对需要注意的事项的简要介绍。我们已经触及了安全漏洞中的大多数知名专家,但我强烈建议您查看本章中提到的其他资源。它们提供了更详尽的需要考虑的事项列表,以补充本章中提到的防御措施。最重要的是,不要忘记 input validation 和 mass assignment / overposting,如 Chapter 16 所述。ASP.NET Core 包括针对一些最常见攻击的基本保护,但您仍然可以搬起石头砸自己的脚。确保不是您的应用因被泄露而成为头条新闻!

29.5 Summary

29.5 总结

XSS attacks involve malicious users injecting content into your app, typically to run malicious JavaScript when users browse your app. You can prevent XSS injection attacks by always encoding unsafe input before writing it to a page. Razor Pages do this automatically unless you use the @Html.Raw() method, so use it sparingly and carefully.
XSS 攻击涉及恶意用户将内容注入您的应用程序,通常是在用户浏览您的应用程序时运行恶意 JavaScript。您可以通过在将不安全的输入写入页面之前始终对其进行编码来防止 XSS 注入攻击。除非您使用 @Html.Raw() 方法,否则 Razor Pages 会自动执行此作,因此请谨慎使用。

CSRF attacks are a problem for apps that use cookie-based authentication, such as ASP.NET Core Identity. These attacks rely on the fact that browsers automatically send cookies to a website. A malicious website could create a form that POSTs to your site, and the browser will send the authentication cookie with the request. This allows malicious websites to send requests as though they’re the logged-in user.
CSRF 攻击对于使用基于 Cookie 的身份验证(例如 ASP.NET Core Identity)的应用程序来说是一个问题。这些攻击依赖于浏览器自动向网站发送 cookie 的事实。恶意网站可能会创建一个表单,该表单将 POST 到您的网站,并且浏览器会将身份验证 Cookie 与请求一起发送。这允许恶意网站像登录用户一样发送请求。

You can mitigate CSRF attacks using antiforgery tokens, which involve writing a hidden field in every form that contains a random string based on the current user. A similar token is stored in a cookie. A legitimate request will have both parts, but a forged request from a malicious website will have only the cookie half; it cannot re-create the hidden field in the form. By validating these tokens, your API can reject forged requests.
您可以使用防伪令牌缓解 CSRF 攻击,这涉及以每种形式编写一个隐藏字段,其中包含基于当前用户的随机字符串。类似的令牌存储在 Cookie 中。合法请求将包含两个部分,但来自恶意网站的伪造请求将只有 cookie 的一半;它无法在表单中重新创建隐藏字段。通过验证这些令牌,您的 API 可以拒绝伪造的请求。

The Razor Pages framework automatically adds antiforgery tokens to any forms you create using Razor and validates the tokens for inbound requests. You can disable the validation check if necessary, using the [IgnoreAntiForgeryToken] attribute.
Razor Pages 框架会自动将防伪令牌添加到您使用 Razor 创建的任何表单中,并验证入站请求的令牌。如有必要,您可以使用 [IgnoreAntiForgeryToken] 属性禁用验证检查。

Browsers won’t allow websites to make JavaScript AJAX requests from one app to others at different origins. To match the origin, the app must have the same scheme, domain, and port. If you wish to make cross-origin requests like this, you must enable CORS in your API.
浏览器不允许网站从一个应用程序向不同来源的其他应用程序发出 JavaScript AJAX 请求。要匹配源,应用程序必须具有相同的 scheme、domain 和 port。如果您希望发出这样的跨域请求,则必须在 API 中启用 CORS。

CORS uses HTTP headers to communicate with browsers and defines which origins can call your API. In ASP.NET Core, you can define multiple policies, which can be applied globally to your whole app or to specific controllers and actions.
CORS 使用 HTTP 标头与浏览器通信,并定义哪些源可以调用您的 API。在 ASP.NET Core 中,您可以定义多个策略,这些策略可以全局应用于整个应用程序或特定控制器和作。

You can add the CORS middleware by calling UseCors() on WebApplication and optionally providing the name of the default CORS policy to apply. You can also apply CORS to endpoints by calling RequireCors() or adding the [EnableCors] attribute and providing the name of the policy to apply.
您可以通过在 WebApplication 上调用 UseCors() 并选择性地提供要应用的默认 CORS 策略的名称来添加 CORS 中间件。您还可以通过调用 RequireCors() 或添加 [EnableCors] 属性并提供要应用的策略的名称,将 CORS 应用于终端节点。

Configure the policies for your application by calling AddCors() on WebApplicationBuilder and adding policies in the lambda using AddPolicy(). A policy defines which origins are allowed to call an endpoint, which HTTP methods they can use, and which headers are allowed.
通过在 WebApplicationBuilder 上调用 AddCors() 并使用 AddPolicy() 在 lambda 中添加策略来配置应用程序的策略。策略定义允许哪些源调用终端节点、它们可以使用哪些 HTTP 方法以及允许哪些标头。

Open redirect attacks use the common returnURL mechanism after logging in to redirect users to malicious websites. You can prevent this attack by ensuring that you redirect only to local URLs—URLs that belong to your app.
Open 重定向攻击在登录后使用常见的 returnURL 机制将用户重定向到恶意网站。您可以通过确保仅重定向到本地 URL(属于您的应用程序的 URL)来防止此攻击。

Insecure direct object references are a common problem where you expose the ID of database entities in the URL. You should always verify that users have permission to access or change the requested resource by using resource-based authorization in your action methods.
不安全的直接对象引用是一个常见问题,即在 URL 中公开数据库实体的 ID。您应该始终通过在作方法中使用基于资源的授权来验证用户是否有权访问或更改请求的资源。

SQL injection attacks are a common attack vector when you build SQL requests manually. Always use parameterized queries when building requests or use a framework like EF Core, which isn’t vulnerable to SQL injection.
当您手动构建 SQL 请求时,SQL 注入攻击是一种常见的攻击媒介。在生成请求时,请始终使用参数化查询,或使用 EF Core 等框架,该框架不易受到 SQL 注入的攻击。

The most sensitive data in your app is often the data of your users. Mitigate this risk by storing only data that you need. Ensure that you store passwords only as a hash, protect against weak or compromised passwords, and provide the option for MFA. ASP.NET Core Identity provides all of this out of the box, so it’s a great choice if you need to create an identity provider.
应用程序中最敏感的数据通常是用户的数据。通过仅存储您需要的数据来降低此风险。确保仅将密码存储为哈希值,防止弱密码或泄露密码,并提供 MFA 选项。ASP.NET Core Identity 提供了所有这些开箱即用的功能,因此如果您需要创建身份提供商,它是一个不错的选择。

  1. In 2020 the National Institute of Standards and Technology (NIST) updated its Digital Identity Guidelines on handling user details, which contains some great advice. See http://mng.bz/6gRA.
  2. 2020 年,美国国家标准与技术研究院 (NIST) 更新了关于处理用户详细信息的数字身份指南,其中包含一些很好的建议。请参阅 http://mng.bz/6gRA

ASP.NET Core in Action 28 Adding HTTPS to an application

28 Adding HTTPS to an application
28 将 HTTPS 添加到应用程序

This chapter covers
本章涵盖

• Encrypting traffic between clients and your app using HTTPS
使用 HTTPS加密客户端和应用程序之间的流量

• Using the HTTPS development certificate for local development
使用 HTTPS 开发证书进行本地开发

• Configuring Kestrel with a custom HTTPS certificate
使用自定义 HTTPS 证书配置 Kestrel

• Enforcing HTTPS for your whole app
为整个应用程序强制实施 HTTPS

Web application security is a hot topic at the moment. Practically every week another breach is reported, or confidential details are leaked. It may seem like the situation is hopeless, but the reality is that the vast majority of breaches could have been prevented with the smallest amount of effort.
Web 应用程序安全是目前的一个热门话题。几乎每周都会报告另一起数据泄露事件,或泄露机密细节。情况似乎没有希望,但现实是,绝大多数数据泄露本可以通过最小的努力来预防。

In chapter 29 we’ll look at a range of common attacks and how to protect against them in your ASP.NET Core app. In this chapter we start by looking at one of the most basic security measures: encrypting the traffic between a client such as a browser and your application.
在第 29 章中,我们将介绍一系列常见攻击,以及如何在 ASP.NET Core 应用程序中防范这些攻击。在本章中,我们首先介绍最基本的安全措施之一:加密客户端(如浏览器)和应用程序之间的流量。

Without HTTPS encryption, you risk third parties spying on or modifying the requests and responses as they travel over the internet. The risks associated with unencrypted traffic mean that HTTPS is effectively mandatory for production apps these days, and it is heavily encouraged by the makers of modern browsers such as Chrome and Firefox. In section 28.1 you’ll learn more about these risks and some of the approaches you can take to protect your application.
如果没有 HTTPS 加密,当请求和响应通过 Internet 传输时,您可能会面临第三方监视或修改它们的风险。与未加密流量相关的风险意味着 HTTPS 如今实际上是生产应用程序的强制性要求,并且 Chrome 和 Firefox 等现代浏览器的制造商强烈鼓励 HTTPS。在 Section 28.1 中,您将了解有关这些风险的更多信息以及您可以采取的一些方法来保护您的应用程序。

In section 28.2 you’ll see how to get started with HTTPS locally using the ASP.NET Core development certificate. I describe what it is, how to trust it on your application, and what to do if it’s not working as you expect.
在 Section 28.2 中,您将看到如何使用 ASP.NET Core 开发证书在本地开始使用 HTTPS。我将介绍它是什么,如何在您的应用程序上信任它,以及如果它没有按预期工作该怎么办。

The development certificate is great for local work, but in production you’ll need to configure a real, production certificate. I don’t describe the process of obtaining a certificate in section 28.3, as that will vary by provider; instead, I show how to configure Kestrel to use a custom certificate you’ve acquired.
开发证书非常适合本地工作,但在生产环境中,您需要配置一个真实的生产证书。我没有在第 28.3 节中描述获取证书的过程,因为这会因提供商而异;相反,我将介绍如何将 Kestrel 配置为使用您获取的自定义证书。

In section 28.4 I describe some of the approaches to enforcing HTTPS in your application. Unfortunately, web browsers still expect apps to be available over HTTP by default, so you typically need to expose your application on both HTTP and HTTPS ports. Nevertheless, there are things you can do to push clients toward the HTTPS endpoint, which are considered security best practices these days.
在 Section 28.4 中,我描述了在应用程序中强制执行 HTTPS 的一些方法。遗憾的是,默认情况下,Web 浏览器仍然希望应用程序通过 HTTP 可用,因此您通常需要在 HTTP 和 HTTPS 端口上公开您的应用程序。不过,您可以采取一些措施来将客户端推送到 HTTPS 终端节点,这如今被认为是安全最佳实践。

Before we look at HTTPS in ASP.NET Core specifically, we’ll start by looking at HTTPS in general and why you should use it in all your applications.
在我们具体研究 ASP.NET Core 中的 HTTPS 之前,我们首先一般地了解一下 HTTPS,以及为什么您应该在所有应用程序中使用它。

28.1 Why do I need HTTPS?

28.1 为什么我需要 HTTPS?

In this section you’ll learn about HTTPS: what it is, and why you need to be aware of it for all your production applications. We’re not going to go into details about the protocol or how certificates work at this point, instead focusing on why you need to use HTTPS. You’ll see two approaches to adding HTTPS to your application: supporting HTTPS directly in your application and using SSL/TLS-offloading with a reverse proxy.
在本节中,您将了解 HTTPS:它是什么,以及为什么您需要在所有生产应用程序中了解 HTTPS。此时,我们不会详细介绍协议或证书的工作原理,而是重点介绍为什么需要使用 HTTPS。您将看到两种将 HTTPS 添加到应用程序的方法:直接在应用程序中支持 HTTPS,以及将 SSL/TLS 卸载与反向代理一起使用。

So far in this book, I’ve shown how the user’s browser sends a request across the internet to your app using the HTTP protocol. We haven’t looked too much into the details of that protocol other than to establish that it uses verbs to describe the type of request (such as GET and POST), that it contains headers with metadata about the request, and optionally includes a body payload of data.
到目前为止,在本书中,我已经展示了用户的浏览器如何使用 HTTP 协议通过 Internet 向您的应用程序发送请求。我们没有深入研究该协议的细节,只是确定它使用动词来描述请求类型(例如 GET 和 POST),它包含带有请求元数据的标头,以及可选的数据正文有效负载。

By default, HTTP requests are unencrypted; they’re plain-text files being sent over the internet. Anyone on the same network as a user (such as someone using the same public Wi-Fi in a coffee shop) can read the requests and responses sent back and forth. Attackers can even modify the requests or responses as they’re in transit, as shown in figure 28.1.
默认情况下,HTTP 请求未加密;它们是通过 Internet 发送的纯文本文件。与用户位于同一网络上的任何人(例如在咖啡店使用同一公共 Wi-Fi 的人)都可以阅读来回发送的请求和响应。攻击者甚至可以在传输过程中修改请求或响应,如图 28.1 所示。

alt text

Figure 28.1 Unencrypted HTTP requests can be read by users on the same network. Attackers can even intercept the request and response, reading or changing the data. HTTPS requests can’t be read or manipulated by attackers.
图 28.1 同一网络上的用户可以读取未加密的 HTTP 请求。攻击者甚至可以拦截请求和响应,读取或更改数据。攻击者无法读取或纵 HTTPS 请求。

Using unencrypted web apps in this way presents both a privacy and a security risk to your users. Attackers could read sensitive details such as passwords and personally identifiable information (PII), they could inject malicious code into your responses to attack users, or they could steal authentication cookies and impersonate the user on your app.
以这种方式使用未加密的 Web 应用程序会给用户带来隐私和安全风险。攻击者可以读取密码和个人身份信息 (PII) 等敏感详细信息,他们可能会将恶意代码注入您的响应中以攻击用户,或者他们可能会窃取身份验证 Cookie 并在您的应用程序上冒充用户。

To protect your users, your app should encrypt the traffic between the user’s browser and your app as it travels over the network by using the HTTPS protocol. This is similar to HTTP traffic, but it uses an SSL/TLS certificate to encrypt requests and responses, so attackers cannot read or modify the contents.
为了保护您的用户,您的应用应使用 HTTPS 协议加密用户浏览器和您的应用之间的流量。这类似于 HTTP 流量,但它使用 SSL/TLS 证书来加密请求和响应,因此攻击者无法读取或修改内容。

DEFINITION Secure Sockets Layer (SSL) is an older standard that facilitates HTTPS. The SSL protocol has been superseded by Transport Layer Security (TLS), so I’ll be using TLS preferentially throughout this chapter. Normally, if you hear someone talking about SSL or SSL certificates, they actually mean TLS. You can find the RFC for the latest version of the TLS protocol at https://www.rfc-editor.org/rfc/rfc8446.
定义:SSL是一种促进 HTTPS 的旧标准。SSL 协议已被传输层安全性 (TLS) 取代,因此在本章中,我将优先使用 TLS。通常,如果您听到有人谈论 SSL 或 SSL 证书,他们实际上指的是 TLS。您可以在 https://www.rfc-editor.org/rfc/rfc8446 中找到最新版本的 TLS 协议的 RFC。

In browsers, you can tell that a site is using HTTPS by the https:// prefix to URLs (notice the s), or sometimes by a padlock, as shown in figure 28.2. Most modern browsers these days deemphasize that a site is using HTTPS, as most sites use HTTPS, and instead highlight when you’re on a site that isn’t using HTTPS, flagging it as insecure.
在浏览器中,您可以通过 URL 的 https:// 前缀(注意 s)或有时通过挂锁来判断站点正在使用 HTTPS,如图 28.2 所示。如今,大多数现代浏览器都不再强调网站正在使用 HTTPS,因为大多数网站都使用 HTTPS,而是在您访问未使用 HTTPS 的网站上时突出显示,将其标记为不安全。

alt text

Figure 28.2 Encrypted apps using HTTPS and unencrypted apps using HTTP in Edge. Using HTTPS protects your application from being viewed or tampered with by attackers.
图 28.2 Edge 中使用 HTTPS 的加密应用程序和使用 HTTP 的未加密应用程序。使用 HTTPS 可以保护您的应用程序不被攻击者查看或篡改。

The reality is that these days, you should always serve your production websites over HTTPS. The industry is pushing toward HTTPS by default, with most browsers marking HTTP sites as explicitly not secure. Skipping HTTPS will hurt the perception of your app in the long run, so even if you’re not interested in the security benefits, it’s in your best interest to set up HTTPS.
现实情况是,如今,您应该始终通过 HTTPS 为您的生产网站提供服务。默认情况下,该行业正在推动 HTTPS,大多数浏览器将 HTTP 站点标记为明确不安全。从长远来看,跳过 HTTPS 会损害您的应用程序的看法,因此即使您对安全优势不感兴趣,设置 HTTPS 也符合您的最佳利益。

TIP You can find a good cheat sheet for HTTPS by OWASP at http://mng.bz/PzxY. ASP.NET Core takes care of most of the points in this list for you, but there are some important ones in the Application section specifically.
提示:您可以在 http://mng.bz/PzxY 上找到 OWASP 的 HTTPS 优秀备忘单。ASP.NET Core 为您处理了此列表中的大部分要点,但 Application (应用程序) 部分中还有一些重要的要点。

Another reason to support HTTPS is that many browser features are available only when your site is served over HTTPS. Some of these features are JavaScript browser APIs, such as location APIs, microphone APIs, and storage APIs. These are available only over HTTPS to protect users from attackers that could modify insecure HTTP requests. Other features apply to server-side apps too, such as Brotli compression and HTTP/2 support.
支持 HTTPS 的另一个原因是,许多浏览器功能仅在您的网站通过 HTTPS 提供服务时可用。其中一些功能是 JavaScript 浏览器 API,例如位置 API、麦克风 API 和存储 API。这些选项仅通过 HTTPS 提供,以保护用户免受可能修改不安全 HTTP 请求的攻击者的攻击。其他功能也适用于服务器端应用程序,例如 Brotli 压缩和 HTTP/2 支持。

TIP For details on how the SSL/TLS protocols work, see chapter 9 of Real-World Cryptography, by David Wong (Manning, 2021), http://mng.bz/zxz1.
提示:有关 SSL/TLS 协议如何工作的详细信息,请参阅 David Wong (Manning, 2021) http://mng.bz/zxz1 合著的《真实世界密码学》第 9 章。

To enable HTTPS, you need to obtain and configure a TLS certificate for your server. Unfortunately, although that process is a lot easier than it used to be and is now essentially free thanks to Let’s Encrypt (https://letsencrypt.org), it’s still far from simple in many cases. If you’re setting up a production server, I recommend carefully following the tutorials on the Let’s Encrypt site. It’s easy to get it wrong, so take your time.
要启用 HTTPS,您需要为您的服务器获取并配置 TLS 证书。不幸的是,尽管这个过程比以前容易得多,并且现在由于 Let's Encrypt (https://letsencrypt.org) 而基本上是免费的,但在许多情况下,它仍然远非简单。如果您正在设置生产服务器,我建议您仔细按照 Let's Encrypt 站点上的教程进行作。很容易出错,所以要慢慢来。

TIP If you’re hosting your app in the cloud, most providers will provide one-click TLS certificates so that you don’t have to manage certificates yourself. This is extremely useful, and I highly recommend it for everyone. You don’t even have to host your application in the cloud to take advantage of this. Cloudflare (https://www.cloudflare.com) provides a CDN service that you can add TLS to. You can even use it for free.
提示:如果您在云中托管应用程序,大多数提供商将提供一键式 TLS 证书,这样您就不必自己管理证书。这非常有用,我强烈推荐给大家。您甚至不必在云中托管您的应用程序即可利用这一点。Cloudflare (https://www.cloudflare.com) 提供 CDN 服务,您可以将 TLS 添加到该服务中。您甚至可以免费使用它。

As an ASP.NET Core application developer, you can often get away without directly supporting HTTPS in your app by taking advantage of the reverse-proxy architecture, as shown in figure 28.3, in a process called SSL/TLS offloading/termination. This is generally standard in Platform as a Service (PaaS) cloud services, such as Azure App Service.
作为 ASP.NET Core 应用程序开发人员,您通常可以通过利用反向代理架构(如图 28.3 所示)在称为 SSL/TLS 卸载/终止的过程中,无需直接在应用程序中支持 HTTPS。这通常是平台即服务 (PaaS) 云服务(如 Azure 应用服务)中的标准。

alt text

Figure 28.3 You have two options when using HTTPS with a reverse proxy: SSL/TLS passthrough and SSL/TLS offloading. In SSL/TLS passthrough, the data is encrypted all the way to your ASP.NET Core app. For SSL/TLS offloading, the reverse proxy handles decrypting the data, so your app doesn’t have to.
图 28.3 将 HTTPS 与反向代理一起使用时,您有两个选项:SSL/TLS 直通和 SSL/TLS 卸载。在 SSL/TLS 直通中,数据会一直加密到 ASP.NET Core 应用程序。对于 SSL/TLS 卸载,反向代理会处理解密数据,因此您的应用不必这样做。

With SSL/TLS offloading, instead of your application handling requests using HTTPS directly, your app continues to use HTTP. The reverse proxy is responsible for encrypting and decrypting HTTPS traffic to the browser. This often gives you the best of both worlds: data is encrypted between the user’s browser and the server, but you don’t have to worry about configuring certificates in your application.
使用 SSL/TLS 卸载时,您的应用将继续使用 HTTP,而不是直接使用 HTTPS 处理请求。反向代理负责加密和解密到浏览器的 HTTPS 流量。这通常可以为您提供两全其美的效果:数据在用户的浏览器和服务器之间加密,但您不必担心在应用程序中配置证书。

NOTE If you’re concerned that the traffic is unencrypted between the reverse proxy and your app, I recommend reading Troy Hunt’s post “CloudFlare, SSL and unhealthy security absolutism”: http://mng.bz/eHCi. It discusses the pros and cons of the problem as it relates to decrypting on the reverse proxy and why you must consider the most likely attacks on your website, in a process called threat modeling.
注意:如果您担心反向代理和您的应用程序之间的流量未加密,我建议您阅读 Troy Hunt 的博文“CloudFlare、SSL 和不健康的安全绝对主义”:http://mng.bz/eHCi。它讨论了与反向代理解密相关的问题的利弊,以及为什么您必须在称为威胁建模的过程中考虑最有可能对您网站的攻击

Depending on the specific infrastructure where you’re hosting your app, SSL/TLS could be offloaded to a dedicated device on your network, a third-party service like Cloudflare, or a reverse proxy (such as Internet Information Services [IIS], NGINX, or HAProxy) running on the same or a different server. Nevertheless, in some situations, you may need to handle SSL/TLS directly in your app:
根据您托管应用程序的特定基础设施,SSL/TLS 可以卸载到您网络上的专用设备、第三方服务(如 Cloudflare)或反向代理(如 Internet Information Services [IIS]、NGINX 或 HAProxy)在相同或不同的服务器上运行。不过,在某些情况下,您可能需要直接在应用程序中处理 SSL/TLS:

• If you’re exposing Kestrel to the internet directly, without a reverse proxy—This is a supported approach since ASP.NET Core 3.0, and can give high performance. It is also often the case when you’re developing your app locally.
如果您将 Kestrel 直接暴露在 Internet 上,而不使用反向代理 - 这是自 ASP.NET Core 3.0 以来受支持的方法,并且可以提供高性能。在本地开发应用程序时,也经常会出现这种情况。

• If having HTTP between the reverse proxy and your app is not acceptable—While securing traffic inside your network is less critical compared with external traffic, it is undoubtedly more secure to use HTTPS for internal traffic too. This may be a hard requirement for some applications or sectors.
如果不能接受在反向代理和应用程序之间使用 HTTP – 虽然与外部流量相比,保护网络内部流量不那么重要,但对内部流量使用 HTTPS 无疑也更安全。对于某些应用程序或部门来说,这可能是一个硬性要求。

• If you’re using technology that requires HTTPS—Some newer network protocols, such as gRPC and HTTP/2, generally require an end-to-end HTTPS connection.
如果使用需要 HTTPS 的技术 - 某些较新的网络协议 (如 gRPC 和 HTTP/2) 通常需要端到端 HTTPS 连接。

In each of these scenarios, you’ll need to configure a TLS certificate for your application so Kestrel can receive HTTPS traffic. In section 28.2 you’ll see the easiest way to get started with HTTPS when developing locally, using the ASP.NET Core development certificate.
在上述每种情况下,您都需要为应用程序配置 TLS 证书,以便 Kestrel 可以接收 HTTPS 流量。在 Section 28.2 中,您将看到在本地开发时使用 ASP.NET Core 开发证书开始使用 HTTPS 的最简单方法。

28.2 Using the ASP.NET Core HTTPS development certificates

28.2 使用 ASP.NET Core HTTPS 开发证书

Working with HTTPS certificates is easier than it used to be, but unfortunately it can still be a confusing topic, especially if you’re a newcomer to the web. In this section you’ll learn how the .NET software development kit (SDK), Visual Studio, and IIS Express try to improve this experience by handling a lot of the grunt work for you, and what to do when things go wrong.
使用 HTTPS 证书比以前更容易,但不幸的是,它仍然是一个令人困惑的话题,尤其是如果您是 Web 新手。在本节中,您将了解 .NET 软件开发工具包 (SDK)、Visual Studio 和 IIS Express 如何通过为您处理大量繁重的工作来尝试改善这种体验,以及出现问题时该怎么做。

The first time you run a dotnet command using the .NET SDK, the SDK installs an HTTPS development certificate on your machine. Any ASP.NET Core application you create using the default templates (or for which you don’t explicitly configure certificates) will use this development certificate to handle HTTPS traffic. However, the development certificate is not trusted by default. If you access a site that’s using an untrusted certificate, you’ll get a browser warning, as shown in figure 28.4.
首次使用 .NET SDK 运行 dotnet 命令时,SDK 会在计算机上安装 HTTPS 开发证书。您使用默认模板(或未为其明确配置证书)创建的任何 ASP.NET Core 应用程序都将使用此开发证书来处理 HTTPS 流量。但是,默认情况下,开发证书不受信任。如果您访问的站点使用不受信任的证书,您将收到浏览器警告,如图 28.4 所示。

alt text

Figure 28.4 The developer certificate is not trusted by default, so apps serving HTTPS traffic using it will be marked as insecure by browsers. Although you can bypass the warnings if necessary, you should instead update the certificate to be trusted.
图 28.4 默认情况下,开发者证书不受信任,因此使用该证书提供 HTTPS 流量的应用程序将被浏览器标记为不安全。尽管您可以根据需要绕过警告,但您应该更新要信任的证书。

A brief primer on certificates and signing
证书和签名

HTTPS uses public key cryptography as part of the data-encryption process. This uses two keys: a public key that anyone can see and a private key that only your server can see. Anything encrypted with the public key can be decrypted only with the private key. That way, a browser can encrypt something with your server’s public key, and only your server can decrypt it. A complete TLS certificate consists of both the public and private parts.
HTTPS 的简要入门使用公钥加密作为数据加密过程的一部分。这使用两个密钥:任何人都可以看到的公钥和只有您的服务器可以看到的私钥。使用公钥加密的任何内容都只能使用私钥解密。这样,浏览器可以使用您服务器的公钥加密某些内容,并且只有您的服务器可以解密它。完整的 TLS 证书由公有部分和私有部分组成。

When a browser connects to your app, the server sends the public key part of the TLS certificate. But how does the browser know that it was definitely your server that sent the certificate? To achieve this, your TLS certificate contains additional certificates, including one or more certificates from a third party, a certificate authority (CA). At the end of the certificate chain is the root certificate.
当浏览器连接到您的应用时,服务器会发送 TLS 证书的公钥部分。但是浏览器如何知道发送证书的绝对是您的服务器呢?为此,您的 TLS 证书包含其他证书,包括来自第三方(证书颁发机构 (CA))的一个或多个证书。证书链的末尾是根证书。

CAs are special trusted entities, and browsers are hardcoded to trust specific root certificates. For the TLS certificate for your app to be trusted, it must contain (or be signed by) a trusted root certificate. Browsers periodically update their internal list of root certificates and revoke root certificates that can no longer be trusted.
CA 是特殊的受信任实体,浏览器被硬编码为信任特定的根证书。要使应用的 TLS 证书受信任,它必须包含受信任的根证书(或由其签名)。浏览器会定期更新其内部根证书列表,并吊销不再受信任的根证书。

When you use the ASP.NET Core development certificate, or if you create your own self-signed certificate, your site’s HTTPS is missing that trusted root certificate. That means browsers won’t trust your certificate and won’t connect to your server by default. To get around this, you need to tell your development machine to explicitly trust the certificate.
当您使用 ASP.NET Core 开发证书时,或者如果您创建自己的自签名证书,则站点的 HTTPS 缺少该受信任的根证书。这意味着浏览器不会信任您的证书,默认情况下不会连接到您的服务器。要解决此问题,您需要告诉开发计算机显式信任该证书。

In production, you can’t use a development or self-signed certificate, as a user’s browser won’t trust it. Instead, you need to obtain a signed HTTPS certificate from a service like Let’s Encrypt or from a cloud provider like AWS, Azure, or Cloudflare. These certificates are already signed by a trusted CA, so they are automatically trusted by browsers.
在生产环境中,您不能使用开发证书或自签名证书,因为用户的浏览器不会信任它。相反,您需要从 Let's Encrypt 等服务或 AWS、Azure 或 Cloudflare 等云提供商处获取签名的 HTTPS 证书。这些证书已由受信任的 CA 签名,因此浏览器会自动信任它们。

To solve these browser warnings, you need to trust the certificate. Trusting a certificate is a sensitive operation; it’s saying “I know this certificate doesn’t look quite right, but ignore that,” so it’s hard to do automatically. If you’re running on Windows or macOS, you can trust the development certificate by running
要解决这些浏览器警告,您需要信任该证书。信任证书是一项敏感作;它说“我知道这个证书看起来不太对劲,但请忽略它”,所以很难自动完成。如果您在 Windows 或 macOS 上运行,则可以通过在 Windows 或 macOS 上运行

dotnet dev-certs https --trust

This command trusts the certificate by registering it in the operating system’s certificate store. After you run this command, you should be able to access your websites without seeing any warnings or “not secure” labels, as shown in figure 28.5.
此命令通过在作系统的证书存储中注册证书来信任证书。运行此命令后,您应该能够访问您的网站,而不会看到任何警告或 “not secure” 标签,如图 28.5 所示。

alt text

Figure 28.5 Once the development certificate is trusted, you will no longer see browser warnings about the connection.
图 28.5 一旦开发证书被信任,您将不再看到有关连接的浏览器警告。

TIP You may need to close your browser after trusting the certificate to clear the browser’s cache.
提示:您可能需要在信任证书后关闭浏览器以清除浏览器的缓存。

If you’re using Windows, Visual Studio, and IIS Express for development, then you might not need to explicitly trust the development certificate. IIS Express acts as a reverse proxy when you’re developing locally, so it handles the SSL/TLS setup itself. On top of that, Visual Studio should trust the IIS development certificate as part of installation, so you may never see the browser warnings at all.
如果您使用 Windows、Visual Studio 和 IIS Express 进行开发,则可能不需要显式信任开发证书。在本地开发时,IIS Express 充当反向代理,因此它会自行处理 SSL/TLS 设置。最重要的是,Visual Studio 应该信任 IIS 开发证书作为安装的一部分,因此您可能根本看不到浏览器警告。

TIP In macOS, before .NET 7, you would have to retrust the developer certificate repeatedly for every new app. In .NET 7, the process is a lot smoother, so you shouldn’t have to retrust it anything like as often!
提示:在 macOS 中,在 .NET 7 之前,您必须为每个新应用程序反复重新信任开发人员证书。在 .NET 7 中,该过程要顺畅得多,因此您不必像以前那样经常重新信任它!

Trusting the developer certificate works smoothly in Windows and macOS, in most cases. Unfortunately, trusting the certificate in Linux is a little trickier and depends on the specific flavor of Linux you’re using. On top of that, software in Linux often uses its own certificate store, so you’ll probably need to add the certificate directly to your favorite browser. If you’re using any of the following scenarios, you’ll need to do more work:
在大多数情况下,信任开发人员证书在 Windows 和 macOS 中可以顺利运行。不幸的是,在 Linux 中信任证书有点棘手,具体取决于您使用的 Linux 的特定风格。最重要的是,Linux 中的软件通常使用自己的证书存储,因此您可能需要将证书直接添加到您最喜欢的浏览器中。如果您使用的是以下任何方案,则需要执行更多工作:

• Firefox browser in Windows, macOS, or Linux
• Edge or Chrome browsers in Linux
• API-to-API communication in Linux
• An app running in Windows Subsystem for Linux (WSL)
• Running applications in Docker

Each of these scenarios requires a slightly different approach. In many cases it’s one or two commands, so I suggest following the documentation for your scenario carefully at http://mng.bz/JglK.
这些方案中的每一种都需要略有不同的方法。在许多情况下,它只有一个或两个命令,因此我建议您在 http://mng.bz/JglK 中仔细遵循适用于您的方案的文档。

TIP If you’ve tried trusting the certificate, and your app is still giving errors, try closing all your browser windows and running dotnet dev-certs https --clean followed by dotnet dev-certs https --trust. Browsers cache certificate trust, so the close and open step is important!
提示:如果已尝试信任证书,但应用仍然提供错误,请尝试关闭所有浏览器窗口并运行 dotnet dev-certs https --clean,然后运行 dotnet dev-certs https --trust。浏览器会缓存证书信任,因此关闭和打开步骤很重要!

The ASP.NET Core and IIS development certificates make it easy to use Kestrel with HTTPS locally, but those certificates won’t help once you move to production. In the next section I show how to configure Kestrel to use a production TLS certificate.
ASP.NET Core 和 IIS 开发证书使在本地使用 Kestrel 和 HTTPS 变得容易,但一旦您迁移到生产环境,这些证书将无济于事。在下一节中,我将介绍如何配置 Kestrel 以使用生产 TLS 证书。

28.3 Configuring Kestrel with a production HTTPS certificate

28.3 使用生产 HTTPS 证书配置 Kestrel

Creating a TLS certificate for production is often a laborious process, as it requires proving to a third-party CA that you own the domain you’re creating the certificate for. This is an important step in the trust process and ensures that attackers can’t impersonate your servers. The result of the process is one or more files, which is the HTTPS certificate you need to configure for your app.
创建用于生产的 TLS 证书通常是一个费力的过程,因为它需要向第三方 CA 证明您拥有要为其创建证书的域。这是信任过程中的一个重要步骤,可确保攻击者无法模拟您的服务器。该过程的结果是一个或多个文件,这是您需要为应用程序配置的 HTTPS 证书。

TIP The specifics of how to obtain a certificate vary by provider and by your OS platform, so follow your provider’s documentation carefully. The vagaries and complexities of this process are one of the reasons I strongly favor the SSL/TLS-offloading or “one-click” approaches described previously. Those approaches mean my apps don’t need to deal with certificates, and I don’t need to use the approaches described in this section; I delegate that responsibility to another piece of the network, or to the underlying platform.
提示:如何获取证书的具体内容因提供商和 OS 平台而异,因此请仔细遵循提供商的文档。此过程的变幻莫测和复杂性是我强烈支持前面描述的 SSL/TLS 卸载或“一键式”方法的原因之一。这些方法意味着我的应用程序不需要处理证书,也不需要使用本节中描述的方法;我将该责任委托给网络的另一个部分或底层平台。

Once you have a certificate, you need to configure Kestrel to use it to serve HTTPS traffic. In chapter 27 you saw how to set the port your application listens on with the ASPNETCORE_URLS environment variable or via the command line, and you saw that you could provide an HTTPS URL. As you didn’t provide any certificate configuration, Kestrel used the development certificate by default. In production you need to tell Kestrel which certificate to use.
获得证书后,您需要配置 Kestrel 以使用它来提供 HTTPS 流量。在第 27 章中,您了解了如何使用 ASPNETCORE_URLS 环境变量或通过命令行设置应用程序侦听的端口,并且您还了解了可以提供 HTTPS URL。由于您未提供任何证书配置,因此 Kestrel 默认使用开发证书。在生产环境中,您需要告诉 Kestrel 要使用哪个证书。

You can configure the certificates Kestrel uses in multiple ways. For a start, you can load the certificate from multiple locations: from a .pfx file, from .pem/.crt and .key files, or from the OS certificate store. You can also use different certificates for different ports, use a different configuration for each URL endpoint you expose, or configure Server Name Indication (SNI). For full details, see the “Replace the default certificate from configuration” section of Microsoft’s “Configure endpoints for the ASP.NET Core Kestrel web server” documentation: http://mng.bz/wvv2.
您可以通过多种方式配置 Kestrel 使用的证书。首先,您可以从多个位置加载证书:从 .pfx 文件、从 .pem/.crt 和 .key 文件或从 OS 证书存储。您还可以对不同的端口使用不同的证书,对您公开的每个 URL 终端节点使用不同的配置,或配置服务器名称指示 (SNI)。有关完整详细信息,请参阅 Microsoft 的“为 ASP.NET Core Kestrel Web 服务器配置端点”文档的“从配置中替换默认证书”部分:http://mng.bz/wvv2

The following listing shows one possible way to set a custom HTTPS certificate for your production app by configuring the default certificate Kestrel uses for HTTPS connections. You can add the “Kestrel:Certificates:Default” section to your appsettings.json file (or use any other configuration source, as described in chapter 10) to define the .pfx file of the certificate to use. You must also provide the password for accessing the certificate.
以下清单显示了一种可能的方法,即通过配置 Kestrel 用于 HTTPS 连接的默认证书来为生产应用程序设置自定义 HTTPS 证书。您可以将 “Kestrel:Certificates:Default” 部分添加到您的 appsettings.json 文件中(或使用任何其他配置源,如第 10 章所述)来定义要使用的证书的 .pfx 文件。您还必须提供用于访问证书的密码。

Listing 28.1 Configuring the default HTTPS certificate for Kestrel using a .pfx file
清单 28.1 使用 .pfx 文件为 Kestrel 配置默认 HTTPS 证书

{
  “Kestrel”: {             #A
    “Certificates”: {      #A
      “Default”: {         #A
        “Path”: “localhost.pfx”,     #B
        “Password”: “testpassword”   #C
      }
    }
  }
}

❶ Creates a configuration section at Kestrel:Certificates:Default
在 Kestrel创建配置部分:Certificates:Default

❷ The relative or absolute path to the certificate
证书的相对或绝对路径

❸ The password for opening the certificate
打开证书的密码

The preceding example is the simplest way to replace the HTTPS certificate, as it doesn’t require changing any of Kestrel’s defaults. You can use a similar approach to load the HTTPS certificate from the OS certificate store (Windows or macOS), as shown in the “Replace the default certificate from configuration” documentation mentioned previously (http://mng.bz/wvv2).
前面的示例是替换 HTTPS 证书的最简单方法,因为它不需要更改 Kestrel 的任何默认值。您可以使用类似的方法从作系统证书存储区(Windows 或 macOS)加载 HTTPS 证书,如前面提到的“从配置中替换默认证书”文档 (http://mng.bz/wvv2) 中所示。

WARNING Listing 28.1 hardcoded the certificate filename and password for demonstration, but you should never do this in production. Either load these from a configuration store like user-secrets, as you saw in chapter 10, or load the certificate from the local store. Never put production passwords in your appsettings.json files.
警告:Listing 28.1 对证书文件名和密码进行了硬编码以进行演示,但是您永远不应该在 production 中这样做。如第 10 章所示,从配置存储(如 user-secrets)加载这些证书,或者从本地存储加载证书。切勿将生产密码放入 appsettings.json 文件中。

All the default ASP.NET Core templates configure your application to serve both HTTP and HTTPS traffic, and with the configuration you’ve seen so far, you can ensure that your application can handle both HTTP and HTTPS in development and in production.
所有默认的 ASP.NET Core 模板都将您的应用程序配置为同时提供 HTTP 和 HTTPS 流量,并且使用您目前看到的配置,您可以确保您的应用程序可以在开发和生产中同时处理 HTTP 和 HTTPS。

However, whether you use HTTP or HTTPS may depend on the URL users click when they first browse to your app. For example, imagine you have an app that listens using the default ASP.NET Core URLs: http://localhost:5000 for HTTP traffic and https://localhost:5001 for HTTPS traffic. The HTTPS endpoint is available, but if a user doesn’t know that and uses the HTTP URL (the default option in browsers), their traffic is unencrypted. Seeing as you’ve gone to all the trouble to set up HTTPS, it’s probably best that you force users to use it.
但是,您使用的是 HTTP 还是 HTTPS 可能取决于用户首次浏览到您的应用程序时单击的 URL。例如,假设您有一个应用程序使用默认的 ASP.NET 核心网址进行监听:http://localhost:5000 用于 HTTP 流量,https://localhost:5001 用于 HTTPS 流量。HTTPS 终端节点可用,但如果用户不知道并使用 HTTP URL(浏览器中的默认选项),则其流量将未加密。鉴于您已经费尽心思设置 HTTPS,最好强制用户使用它。

28.4 Enforcing HTTPS for your whole app

28.4 为整个应用程序强制执行 HTTPS

Enforcing HTTPS across your whole website is practically required these days. Browsers are beginning to explicitly label HTTP pages as insecure; for security reasons, you must use TLS any time you’re transmitting sensitive data across the internet. Additionally, thanks to HTTP/2 (and the upcoming HTTP/3), adding TLS can improve your app’s performance. In this section you’ll learn three techniques for enforcing HTTPS in your application.
如今,在整个网站上强制实施 HTTPS 实际上是必要的。浏览器开始明确地将 HTTP 页面标记为不安全;出于安全原因,您在通过 Internet 传输敏感数据时必须使用 TLS。此外,得益于 HTTP/2(以及即将推出的 HTTP/3),添加 TLS 可以提高应用程序的性能。在本节中,您将学习在应用程序中强制实施 HTTPS 的三种技术。

TIP HTTP/2 offers many performance improvements over HTTP/1.x, and all modern browsers require HTTPS to enable it. For a great introduction to HTTP/2, see Google’s “Introduction to HTTP/2” at http://mng.bz/9M8j. ASP.NET Core even includes support for HTTP/3, the next version of the protocol! You can read about HTTP/3 at http://mng.bz/qrrJ.
提示HTTP/2 提供了许多优于 HTTP/1.x 的性能改进,所有现代浏览器都需要 HTTPS 才能启用它。有关 HTTP/2 的精彩介绍,请参阅 Google 的“HTTP/2 简介”,网址为 http://mng.bz/9M8j。ASP.NET Core 甚至包括对 HTTP/3 的支持,这是该协议的下一个版本!您可以在 http://mng.bz/qrrJ 上阅读有关 HTTP/3 的信息。

There are multiple approaches to enforcing HTTPS for your application. If you’re using a reverse proxy with SSL/TLS-offloading, it might be handled for you anyway, without your having to worry about it within your apps. If that’s the case, you may be able to disregard some of the steps in this section.
有多种方法可以为您的应用程序强制实施 HTTPS。如果您使用的是具有 SSL/TLS 卸载功能的反向代理,则它可能无论如何都会为您处理,而无需您在应用程序中担心它。如果是这种情况,您可以忽略本节中的某些步骤。

WARNING If you’re building a web API rather than a Razor Pages app, it’s common to reject insecure HTTP requests entirely. You’ll see this approach in section 28.4.3.
警告:如果要构建 Web API 而不是 Razor Pages 应用,则通常会完全拒绝不安全的 HTTP 请求。您将在 Section 28.4.3 中看到这种方法。

One approach to improving the security of your app is to use HTTP security headers. These are HTTP headers sent as part of your HTTP response that tell the browser how it should behave. There are many headers available, most of which restrict the features your app can use in exchange for increased security. In chapter 30 you’ll see how to add your own custom headers to your HTTP responses by creating custom middleware.
提高应用程序安全性的一种方法是使用 HTTP 安全标头。这些是作为 HTTP 响应的一部分发送的 HTTP 标头,用于告诉浏览器它应该如何运行。有许多可用的标头,其中大多数都限制了您的应用程序可以使用的功能,以换取更高的安全性。在第 30 章中,您将看到如何通过创建自定义中间件将自己的自定义标头添加到 HTTP 响应中。

TIP Scott Helme has some great guidance on this and other security headers you can add to your site, such as the Content Security Policy (CSP) header. See “Hardening your HTTP response headers” on his website at http://mng.bz/7DDe.
提示:Scott Helme 对此标头以及您可以添加到站点中的其他安全标头提供了一些很好的指导,例如内容安全策略 (CSP) 标头。请参阅其网站上的“强化 HTTP 响应标头”,网址为 http://mng.bz/7DDe

One of these security headers, the HTTP Strict Transport Security (HSTS) header, can help ensure that browsers use HTTPS where it’s available instead of defaulting to HTTP.
其中一个安全标头,即 HTTP 严格传输安全 (HSTS) 标头,可以帮助确保浏览器在可用的情况下使用 HTTPS,而不是默认使用 HTTP。

28.4.1 Enforcing HTTPS with HTTP Strict Transport Security headers

28.4.1 使用 HTTP 严格传输安全标头强制执行 HTTPS

It’s unfortunate, but by default, browsers load apps over HTTP unless otherwise specified. That means your apps must typically support both HTTP and HTTPS, even if you don’t want to serve any traffic over HTTP, as shown in figure 28.6. On top of that, if the initial request is over HTTP, the browser may end up sending subsequent requests over HTTP too.
很遗憾,但默认情况下,除非另有说明,否则浏览器会通过 HTTP 加载应用程序。这意味着您的应用程序通常必须同时支持 HTTP 和 HTTPS,即使您不想通过 HTTP 提供任何流量,如图 28.6 所示。最重要的是,如果初始请求是通过 HTTP 发送的,浏览器最终也可能通过 HTTP 发送后续请求。

alt text

Figure 28.6 When you type in a URL, browsers load the app over HTTP by default. Depending on the links returned by your app or the URLs entered, the browser may make HTTP or HTTPS requests.
图 28.6 当您键入 URL 时,浏览器默认通过 HTTP 加载应用程序。根据应用程序返回的链接或输入的 URL,浏览器可能会发出 HTTP 或 HTTPS 请求。

One partial mitigation (and a security best practice) is to add HTTP Strict Transport Security headers to your responses.
一种部分缓解措施(也是安全最佳实践)是将 HTTP Strict Transport Security 标头添加到您的响应中。

DEFINITION HTTP Strict Transport Security (HSTS) is a specification (https://www.rfc-editor.org/rfc/rfc6797) for the Strict-Transport-Security header that instructs the browser to use HTTPS for all subsequent requests to your application. The HSTS header can be sent only with responses to HTTPS requests. It is also relevant only for requests originating from a browser; it has no effect on server-to-server communication or on mobile apps.
定义:HTTP 严格传输安全 (HSTS) 是 Strict-Transport-Security 标头的规范 (https://www.rfc-editor.org/rfc/rfc6797),它指示浏览器对应用程序的所有后续请求使用 HTTPS。HSTS 标头只能与对 HTTPS 请求的响应一起发送。它也仅与来自浏览器的请求相关;它对服务器到服务器的通信或移动应用程序没有影响。

After a browser receives a valid HSTS header, the browser stops sending HTTP requests to your app and uses only HTTPS instead, as shown in figure 28.7. Even if your app has an http:// link or the user enters http:// in the URL bar of the app, the browser automatically replaces the request with an https:// version.
在浏览器收到有效的 HSTS 标头后,浏览器将停止向您的应用程序发送 HTTP 请求,并仅使用 HTTPS,如图 28.7 所示。即使您的应用程序具有 http:// 链接或用户在应用程序的 URL 栏中输入 http://,浏览器也会自动将请求替换为 https:// 版本。

alt text

Figure 28.7 After a browser sends an HTTPS request, the app returns an HSTS header, instructing the browser to always send requests over HTTPS. The next time the user attempts to make an http:// request, the browser aborts the request and makes an https:// request instead.
图 28.7 浏览器发送 HTTPS 请求后,应用程序返回 HSTS 标头,指示浏览器始终通过 HTTPS 发送请求。下次用户尝试发出 http:// 请求时,浏览器会中止该请求并改为发出 https:// 请求。

TIP You can achieve a similar upgrading of HTTP to HTTPS requests using the Upgrade-Insecure-Requests directive in the Content-Security-Policy (CSP) header. This provides fewer protections than the HSTS header but can be used in combination with it. For more details on this directive and CSP in general, see http://mng.bz/mVV4.
提示:您可以使用 Content-Security-Policy (CSP) 标头中的 Upgrade-Insecure-Requests 指令实现从 HTTP 到 HTTPS 请求的类似升级。这提供的保护比 HSTS 标头少,但可以与之结合使用。有关此指令和 CSP 的更多详细信息,请参阅 http://mng.bz/mVV4

HSTS headers are strongly recommended for production apps. You generally don’t want to enable them for local development, as that would mean you could never run a non-HTTPS app locally. In a similar fashion, you should use HSTS only on sites for which you always intend to use HTTPS, as it’s hard (sometimes impossible) to turn off HTTPS once it’s enforced with HSTS.
强烈建议将 HSTS 标头用于生产应用程序。您通常不希望为本地开发启用它们,因为这意味着您永远无法在本地运行非 HTTPS 应用程序。以类似的方式,您应该仅在您始终打算使用 HTTPS 的站点上使用 HSTS,因为一旦使用 HSTS 强制实施 HTTPS,就很难(有时不可能)关闭 HTTPS。

ASP.NET Core comes with built-in middleware for setting HSTS headers, which is included in some of the default templates automatically. The following listing shows how you can configure the HSTS headers for your application using the HstsMiddleware in Program.cs.
ASP.NET Core 附带用于设置 HSTS 标头的内置中间件,该中间件自动包含在一些默认模板中。下面的清单显示了如何使用 Program.cs 中的 HstsMiddleware 为应用程序配置 HSTS 头文件。

Listing 28.2 Using HstsMiddleware to add HSTS headers to an application
Listing 28.2 使用 HstsMiddleware 向应用程序添加 HSTS 头文件

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddHsts(options =>    #A
{    #A
    options.MaxAge = TimeSpan.FromHours(1);    #A
});    #A

WebApplication app = builder.Build();

if(app.Environment.IsProduction())  #B
{
    app.UseHsts();    #C
}

app.UseStaticFiles();
app.UseRouting();

app.MapRazorPages();

app.Run();

❶ Configures your HSTS header settings and changes the MaxAge from the default of 30 days
配置您的 HSTS 标头设置并将 MaxAge 从默认的更改为 30 天

❷ You shouldn’t use HSTS in local environments.
您不应在本地环境中使用 HSTS。

❸ Adds the HstsMiddleware
新增 HstsMiddleware

The preceding example shows how to change the MaxAge sent in the HSTS header. It’s a good idea to start with a small value initially. Once you’re sure your app’s HTTPS is functioning correctly, you can increase the age for greater security. A typical value for production deployments is one year.
前面的示例显示了如何更改 HSTS 标头中发送的 MaxAge。最好先从较小的值开始。确定应用的 HTTPS 正常运行后,您可以提高使用期限以提高安全性。生产部署的典型值为一年。

WARNING Once client browsers have received the HSTS header, browsers will default to using HTTPS for all requests to your application. That means you must commit to always using HTTPS for as long as you set MaxAge. If you disable HTTPS, browsers will not revert to using HTTP until this duration has expired, so your application may be inaccessible until then if you aren’t listening on HTTPS! You can notify the browser that your app no longer supports HSTS by setting MaxAge to 0.
警告:客户端浏览器收到 HSTS 标头后,浏览器将默认对应用程序的所有请求使用 HTTPS。这意味着,只要您设置了 MaxAge,就必须承诺始终使用 HTTPS。如果您禁用 HTTPS,浏览器在此持续时间到期之前不会恢复为使用 HTTP,因此如果您不监听 HTTPS,您的应用程序在此之前可能无法访问!您可以通过将 MaxAge 设置为 0 来通知浏览器您的应用程序不再支持 HSTS。

One limitation with the HSTS header is that you must make an initial request over HTTPS before you can receive the header. If the browser makes only HTTP requests, the app never has a chance to send the HSTS header, so the browser never knows to use HTTPS. One potential solution is called HSTS preload.
HSTS 标头的一个限制是,必须先通过 HTTPS 发出初始请求,然后才能接收标头。如果浏览器仅发出 HTTP 请求,则应用程序永远没有机会发送 HSTS 标头,因此浏览器永远不知道使用 HTTPS。一种可能的解决方案称为 HSTS 预加载。

HSTS preload isn’t part of the HSTS specification, but it’s supported by all modern browsers. Preload bakes your HSTS header into the browser so that the browser knows it should make only HTTPS requests to your site. That removes the “first request” problem entirely, but be aware that HSTS preload commits you to HTTPS forever, as it can’t easily be undone.
HSTS 预加载不是 HSTS 规范的一部分,但所有现代浏览器都支持它。Preload 将您的 HSTS 标头烘焙到浏览器中,以便浏览器知道它应该只向您的网站发出 HTTPS 请求。这完全消除了“第一个请求”问题,但请注意,HSTS 预加载会永久提交您到 HTTPS,因为它不能轻易撤消。

Once you’re comfortable with your application’s HTTPS configuration, you can prepare your app for HSTS preload by configuring an HSTS header that
一旦您对应用程序的 HTTPS 配置感到满意,就可以通过配置 HSTS 标头来为 HSTS 预加载准备应用程序,该标头

• Has a MaxAge of at least one year, though two years are recommended
MaxAge 至少为一年,但建议为两年

• Has the includeSubDomains directive
具有 includeSubDomains 指令

• Has the preload directive
具有 preload 指令

Listing 28.3 shows how you can configure these directives in your app. The listing also shows how to exclude the domain never-https.com so that if you host your app at this domain, HSTS headers won’t be sent. This can be useful for testing purposes.
清单 28.3 展示了如何在应用程序中配置这些指令。该清单还显示了如何排除域 never-https.com,以便在此域中托管应用程序时,不会发送 HSTS 标头。这对于测试目的非常有用。

Listing 28.3 Configuring the application HSTS header for preload
清单 28.3 配置应用程序 HSTS 头文件以进行预加载

builder.Services.AddHsts(options =>
{
    options.Preload = true;    #A
    options.IncludeSubDomains = true;    #B
    options.MaxAge = TimeSpan.FromDays(365);    #C
    options.ExcludedHosts.Add("never-https.com");    #D
});

❶ Sends the preload directive
发送 preload 指令

❷ Sends the includeSubDomains directive
发送 includeSubDomains 指令

❸ You must use a max-age directive of at least one year.
您必须使用至少一年的 max-age 指令。

❹ Don’t send the HSTS header in responses to requests for this domain.
不要发送 HSTS 标头来响应此域的请求。

Once you’ve prepared your application for HSTS preload, you can submit your app for inclusion in the HSTS preload list that ships with modern browsers. Visit the site https://hstspreload.org, confirm that your application meets the requirements, and submit your domain. If all goes well, your domain will be included in a future release of all modern browsers!
为 HSTS 预加载准备应用程序后,您可以提交应用程序以包含在现代浏览器附带的 HSTS 预加载列表中。https://hstspreload.org 访问网站,确认您的申请符合要求,然后提交您的域。如果一切顺利,您的域将包含在所有现代浏览器的未来版本中!

TIP For more details on HSTS and attacks it can mitigate, see Scott Helme’s article “HSTS—The missing link in Transport Layer Security,” at http://mng.bz/5wwa.
提示有关 HSTS 及其可缓解的攻击的更多详细信息,请参阅 Scott Helme 的文章“HSTS — 传输层安全性中缺失的环节”,第 http://mng.bz/5wwa 页。

HSTS is a great option for forcing users to use HTTPS on your website, and if you can use HSTS preload, you can ensure that modern clients never send requests over HTTP. Nevertheless, HSTS preload can take months to enforce, and you won’t always want to take that approach. In the meantime, if a browser makes an initial request over HTTP, it won’t receive the HSTS header and may stay on HTTP! That’s unfortunate, but you can mitigate the problem by redirecting insecure requests to HTTPS immediately.
HSTS 是强制用户在您的网站上使用 HTTPS 的绝佳选择,如果您可以使用 HSTS 预加载,则可以确保现代客户端永远不会通过 HTTP 发送请求。尽管如此,HSTS 预加载可能需要几个月的时间才能执行,并且您并不总是希望采用这种方法。同时,如果浏览器通过 HTTP 发出初始请求,它将不会收到 HSTS 标头,并且可能会停留在 HTTP!这很遗憾,但您可以通过立即将不安全的请求重定向到 HTTPS 来缓解问题。

28.4.2 Redirecting from HTTP to HTTPS with HTTPS redirection middleware

28.4.2 使用 HTTPS 重定向中间件从 HTTP 重定向到 HTTPS

The HstsMiddleware should always be used in conjunction with middleware that redirects all HTTP requests to HTTPS.
HstsMiddleware 应始终与将所有 HTTP 请求重定向到 HTTPS 的中间件结合使用。

TIP It’s possible to apply HTTPS redirection only to specific parts of your application, such as to specific Razor Pages, but I don’t recommend that, as it’s too easy to open a security hole in your application.
提示:可以仅将 HTTPS 重定向应用于应用程序的特定部分,例如特定的 Razor Pages,但我不建议这样做,因为很容易在应用程序中打开安全漏洞。

ASP.NET Core comes with HttpsRedirectionMiddleware, which you can use to enforce HTTPS across your whole app. You add it to the middleware pipeline in Program.cs, and it ensures that any requests that pass through it are secure. If an HTTP request reaches the HttpsRedirectionMiddleware, the middleware immediately short-circuits the pipeline with a redirect to the HTTPS version of the request. The browser then repeats the request using HTTPS instead of HTTP, as shown in figure 28.8.
ASP.NET Core 附带 HttpsRedirectionMiddleware,可用于在整个应用程序中强制实施 HTTPS。您可以将其添加到 Program.cs 中的中间件管道中,并确保通过它的任何请求都是安全的。如果 HTTP 请求到达 HttpsRedirectionMiddleware,中间件会立即通过重定向到请求的 HTTPS 版本来使管道短路。然后,浏览器使用 HTTPS 而不是 HTTP 重复请求,如图 28.8 所示。

alt text

Figure 28.8 The HttpsRedirectionMiddleware works with the HstsMiddleware to ensure that all requests after the initial request are always sent over HTTPS.
图 28.8 HttpsRedirectionMiddleware 与 HstsMiddleware 配合使用,以确保初始请求之后的所有请求始终通过 HTTPS 发送。

NOTE Even with HSTS and the HTTPS redirection middleware, there is still an inherent weakness: by default, browsers always make an initial insecure request over HTTP to your app. The only way to prevent this is with HSTS preload, which tells browsers to always use HTTPS.
注意:即使使用 HSTS 和 HTTPS 重定向中间件,仍然存在一个固有的弱点:默认情况下,浏览器总是通过 HTTP 向您的应用程序发出初始不安全的请求。防止这种情况的唯一方法是使用 HSTS 预加载,它告诉浏览器始终使用 HTTPS。

The HttpsRedirectionMiddleware is added in some of the default ASP.NET Core templates. It is typically placed after the error handling and HstsMiddleware, as shown in the following listing. By default, the middleware redirects all HTTP requests to the secure endpoint, using an HTTP 307 Temporary Redirect status code.
HttpsRedirectionMiddleware 已添加到一些默认的 ASP.NET Core 模板中。它通常放在 error handling 和 HstsMiddleware 之后,如下面的清单所示。默认情况下,中间件使用 HTTP 307 临时重定向状态代码将所有 HTTP 请求重定向到安全终端节点。

Listing 28.4 Using HttpsRedirectionMiddleware to enforce HTTPS for an application
列表 28.4 使用 HttpsRedirectionMiddleware 为应用程序强制执行 HTTPS

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddHsts(o => options.MaxAge = TimeSpan.FromHours(1));

WebApplication app = builder.Build();

if(app.Environment.IsProduction())
{
    app.UseHsts();
}

app.UseHttpsRedirection();     #A

app.UseStaticFiles();
app.UseRouting();

app.MapRazorPages();

app.Run();

❶ Adds the HttpsRedirectionMiddleware to the pipeline and redirects all HTTP requests to HTTPS
将 HttpsRedirectionMiddleware 添加到管道并将所有 HTTP 请求重定向到 HTTPS

The HttpsRedirectionMiddleware automatically redirects HTTP requests to the first configured HTTPS endpoint for your application. If your application isn’t configured for HTTPS, the middleware won’t redirect and instead logs a warning:
HttpsRedirectionMiddleware 会自动将 HTTP 请求重定向到应用程序的第一个配置的 HTTPS 终结点。如果您的应用程序未配置 HTTPS,则中间件不会重定向,而是会记录警告:

warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware[3]
      Failed to determine the https port for redirect.

If you want the middleware to redirect to a different port than Kestrel knows about, you can configure that by setting the ASPNETCORE_HTTPS_PORT environment variable. This is sometimes necessary if you’re using a reverse proxy, and it can be set in alternative ways, as described in Microsoft’s “Enforce HTTPS in ASP.NET Core” documentation: http://mng.bz/6DDA.
如果您希望中间件重定向到 Kestrel 所知道的不同端口,您可以通过设置 ASPNETCORE_HTTPS_PORT 环境变量来配置它。如果您使用的是反向代理,这有时是必需的,并且可以以其他方式进行设置,如 Microsoft 的“在 ASP.NET Core 中强制实施 HTTPS”文档中所述:http://mng.bz/6DDA

SSL/TLS offloading, header forwarding, and detecting secure requests
SSL/TLS 卸载、标头转发和检测安全请求

At the start of section 28.1 I encouraged you to consider terminating HTTPS requests at a reverse proxy. That way, the user uses HTTPS to talk to the reverse proxy, and the reverse proxy talks to your app using HTTP. With this setup, your users are protected, but your app doesn’t have to deal with TLS certificates itself.
在第 28.1 节开始时,我鼓励您考虑在反向代理上终止 HTTPS 请求。这样,用户使用 HTTPS 与反向代理通信,而反向代理使用 HTTP 与你的应用通信。通过此设置,您的用户会受到保护,但您的应用程序不必自行处理 TLS 证书。

For the HttpsRedirectionMiddleware to work correctly, Kestrel needs some way of knowing whether the original request that the reverse proxy received was over HTTP or HTTPS. The reverse proxy communicates to your app over HTTP, so Kestrel can’t figure that out without extra help.
为了使 HttpsRedirectionMiddleware 正常工作,Kestrel 需要某种方式来了解反向代理收到的原始请求是通过 HTTP 还是 HTTPS。反向代理通过 HTTP 与您的应用程序通信,因此如果没有额外的帮助,Kestrel 无法解决这个问题。

The standard approach used by most reverse proxies (such as IIS, NGINX, and HAProxy) is to add headers to the request before forwarding it to your app. Specifically, a header called X-Forwarded-Proto is added, indicating whether the original request protocol was HTTP or HTTPS.
大多数反向代理(例如 IIS、NGINX 和 HAProxy)使用的标准方法是在将请求转发到应用程序之前向请求添加标头。具体来说,添加了一个名为 X-Forwarded-Proto 的标头,指示原始请求协议是 HTTP 还是 HTTPS。

ASP.NET Core includes ForwardedHeadersMiddleware to look for this header (and others) and update the request accordingly, so your app treats a request that was originally secured by HTTPS as secure for all intents and purposes.
ASP.NET Core 包含 ForwardedHeadersMiddleware 来查找此标头(和其他标头)并相应地更新请求,因此您的应用会将最初由 HTTPS 保护的请求视为对所有 intent 和目的都是安全的。

If you’re using IIS with the UseIisIntegration() extension, the header forwarding is handled for you automatically. If you’re using a different reverse proxy, such as NGINX or HAProxy, you can enable the middleware by setting the environment variable ASPNETCORE_FORWARDEDHEADERS_ENABLED=true, as you saw in chapter 27. Alternatively, you can add the middleware to your application manually, as shown in section 27.3.2.
如果将 IIS 与 UseIisIntegration() 扩展一起使用,则会自动处理标头转发。如果你正在使用不同的反向代理,比如 NGINX 或 HAProxy,你可以通过设置环境变量 ASPNETCORE_FORWARDEDHEADERS_ENABLED=true 来启用中间件,就像你在第 27 章中看到的那样。或者,您可以手动将中间件添加到应用程序中,如 Section 27.3.2 所示。

When the reverse proxy forwards a request, the ForwardedHeadersMiddleware looks for the X-Forwarded-Proto header and updates the request details as appropriate. For all subsequent middleware, the request is considered secure. When adding the middleware manually, it’s important that you place ForwardedHeadersMiddleware before the call to UseHsts() or UseHttpsRedirection() so that the forwarded headers are read and the request is marked secure, as appropriate.
当反向代理转发请求时,ForwardedHeadersMiddleware 会查找 X-Forwarded-Proto 标头并根据需要更新请求详细信息。对于所有后续中间件,该请求都被视为安全请求。手动添加中间件时,请务必将 ForwardedHeadersMiddleware 放在调用 UseHsts() 或 UseHttpsRedirection() 之前,以便读取转发的标头并根据需要将请求标记为安全。

Using the HSTS and HTTPS redirection middleware is best practice when you’re building a server-side application such as a Razor Pages app that will always be accessed in the browser. If you’re building an API application. however, a better approach is to not listen for insecure HTTP requests at all!
在构建始终在浏览器中访问的服务器端应用程序(如 Razor Pages 应用)时,最佳做法是使用 HSTS 和 HTTPS 重定向中间件。如果您正在构建 API 应用程序。但是,更好的方法是根本不监听不安全的 HTTP 请求!

28.4.3 Rejecting HTTP requests in API applications

28.4.3 在 API 应用程序中拒绝 HTTP 请求

Browsers have been adding more and more protections, such as the HSTS header, to try to protect users from using insecure HTTP requests. But not all clients are using a web browser. In this section you’ll learn why API applications should generally disable HTTP entirely.
浏览器一直在添加越来越多的保护措施,例如 HSTS 标头,以尝试保护用户免受使用不安全的 HTTP 请求的侵害。但并非所有客户端都使用 Web 浏览器。在本节中,您将了解为什么 API 应用程序通常应该完全禁用 HTTP。

If you’re building an API application, you often can’t rely on requests coming from a browser. Your API application may primarily serve a client-side framework in the browser, but it may also serve mobile applications or provide an API to other backend services. That means you can’t rely on the protections built into web browsers to use HTTPS for your API apps.
如果您正在构建 API 应用程序,则通常不能依赖来自浏览器的请求。您的 API 应用程序可能主要在浏览器中提供客户端框架,但它也可能提供移动应用程序或为其他后端服务提供 API。这意味着您不能依赖 Web 浏览器中内置的保护措施来将 HTTPS 用于 API 应用程序。

On top of that, even if you know all your users are using a browser, the only way to prevent sending all requests over HTTP is to use HSTS preload, as you saw in section 28.4.2. Sending even one request over HTTP can compromise a user, so the safest approach is to listen only for HTTPS requests, not HTTP requests. This is the best option for API apps.
最重要的是,即使您知道所有用户都在使用浏览器,防止通过 HTTP 发送所有请求的唯一方法是使用 HSTS 预加载,如您在 Section 28.4.2 中看到的那样。即使通过 HTTP 发送一个请求也可能危及用户,因此最安全的方法是仅侦听 HTTPS 请求,而不是 HTTP 请求。这是 API 应用程序的最佳选择。

NOTE It would be safest to take this same approach for your browser apps, but unfortunately, browsers currently default to the HTTP versions of apps by default.
注意:对浏览器应用程序采用相同的方法是最安全的,但遗憾的是,浏览器目前默认使用应用程序的 HTTP 版本。

You can disable HTTP requests for your application by setting the URLs for your app to include only https:// requests, using ASPNETCORE_URLS or another approach, as described in chapter 27. Setting
您可以通过使用 ASPNETCORE_URLS 或其他方法将应用程序的 URL 设置为仅包含 https:// 请求来禁用应用程序的 HTTP 请求,如第 27 章所述。设置

ASPNETCORE_URLS=https://*:5001

would ensure that your app serves only HTTPS requests on port 5001 and won’t handle HTTP connections at all. This protects your clients, as they can’t incorrectly make HTTP requests, and it may even make things simpler on your side, as you don’t need to add the HTTP redirection middleware.
将确保您的应用程序仅在端口 5001 上提供 HTTPS 请求,并且根本不处理 HTTP 连接。这可以保护你的客户端,因为它们不会错误地发出 HTTP 请求,甚至可能使你的事情变得更简单,因为你不需要添加 HTTP 重定向中间件。

HTTPS is one of the most basic requirements for adding security to your application these days. It can be tricky to set up initially, but once you’re up and running, you can largely forget about it, especially if you’re using SSL/TLS termination at a reverse proxy.
HTTPS 是当今为应用程序添加安全性的最基本要求之一。最初设置可能很棘手,但是一旦您启动并运行,您基本上可以忘记它,尤其是在反向代理上使用 SSL/TLS 终止时。

Unfortunately, most other security practices require rather more vigilance to ensure that you don’t accidentally introduce vulnerabilities into your app as it grows and develops. In the next chapter we’ll look at several common attacks, learn how ASP.NET Core protects you, and see a few things you need to watch out for.
不幸的是,大多数其他安全实践都需要更加警惕,以确保您不会在应用程序的成长和发展过程中意外地将漏洞引入应用程序。在下一章中,我们将介绍几种常见的攻击,了解 ASP.NET Core 如何保护您,并了解您需要注意的一些事项。

28.5 Summary

28.5 总结

HTTPS is used to encrypt your app’s data as it travels from the server to the browser and back. This encryption prevents third parties from seeing or modifying it.
HTTPS 用于加密应用程序的数据,因为它在服务器和浏览器之间传输。此加密可防止第三方查看或修改它。

HTTPS is virtually mandatory for production apps, as modern browsers like Chrome and Firefox mark non-HTTPS apps as explicitly “not secure.”
HTTPS 对于生产应用程序几乎是必需的,因为 Chrome 和 Firefox 等现代浏览器将非 HTTPS 应用程序明确标记为“不安全”。

In production, you can avoid handling the TLS in your app by using SSL/TLS offloading. This is where a reverse proxy uses HTTPS to talk to the browser, but the traffic is unencrypted between your app and the reverse proxy. The reverse proxy could be on the same or a different server, such as IIS or NGINX, or it could be a third-party service, such as Cloudflare.
在生产环境中,您可以通过使用 SSL/TLS 卸载来避免在应用程序中处理 TLS。这是反向代理使用 HTTPS 与浏览器通信,但应用与反向代理之间的流量未加密的位置。反向代理可以位于相同或不同的服务器上,例如 IIS 或 NGINX,也可以是第三方服务,例如 Cloudflare。

You can use the ASP.NET Core developer certificate or the IIS express developer certificate to enable HTTPS during development. This can’t be used for production, but it’s sufficient for testing locally. You must run dotnet dev-certs https --trust when you first install the .NET SDK to trust the certificate.
在开发过程中,您可以使用 ASP.NET Core 开发人员证书或 IIS Express 开发人员证书来启用 HTTPS。这不能用于生产,但足以在本地进行测试。首次安装 .NET SDK 时,必须运行 dotnet dev-certs https --trust 才能信任证书。

Kestrel is the default web server in ASP.NET Core. It is responsible for reading and writing data from and to the network, parsing the bytes based on the underlying HTTP and network protocols and converting from raw bytes to .NET objects you can use in your apps.
Kestrel 是 ASP.NET Core 中的默认 Web 服务器。它负责从网络读取和写入数据,根据底层 HTTP 和网络协议解析字节,以及将原始字节转换为可在应用程序中使用的 .NET 对象。

You can configure an HTTPS certificate for Kestrel in production using the Kestrel:Certificates:Default configuration section. This does not require any code changes to your application; Kestrel automatically loads the certificate when your app starts and uses it to serve HTTPS requests.
您可以使用 Kestrel:Certificates:Default 配置部分在生产中为 Kestrel 配置 HTTPS 证书。这不需要对应用程序进行任何代码更改;Kestrel 会在您的应用程序启动时自动加载证书,并使用它来处理 HTTPS 请求。

You can use the HstsMiddleware to set HSTS headers for your application to ensure that the browser always sends HTTPS requests to your app instead of HTTP requests. HSTS can be enforced only when an initial HTTPS request is made to your app, so it’s best used in conjunction with HTTP to HTTPS redirection.
你可以使用 HstsMiddleware 为你的应用设置 HSTS 头,确保浏览器总是向你的应用发送 HTTPS 请求,而不是 HTTP 请求。只有在向应用程序发出初始 HTTPS 请求时,才能强制执行 HSTS,因此最好将其与 HTTP 到 HTTPS 重定向结合使用。

You can enable HSTS preload for your application to ensure that HTTP requests from browsers are never sent and are always upgraded to HTTPS. You must configure your app as shown in listing 28.3, deploy your app with a TLS certificate, and register your app at the URL https://hstspreload.org. This will schedule your app to be included in browsers’ built-in list of HTTPS only sites.
您可以为您的应用程序启用 HSTS 预加载,以确保来自浏览器的 HTTP 请求永远不会发送,并且始终升级到 HTTPS。您必须按照清单 28.3 中所示配置您的应用程序,使用 TLS 证书部署您的应用程序,并在 URL https://hstspreload.org 处注册您的应用程序。这将安排您的应用程序包含在浏览器的内置仅限 HTTPS 站点列表中。

You can enforce HTTPS for your whole app using the HttpsRedirectionMiddleware. This will redirect any HTTP requests to the HTTPS version of endpoints.
你可以使用 HttpsRedirectionMiddleware 为整个应用程序强制实施 HTTPS。这会将任何 HTTP 请求重定向到终端节点的 HTTPS 版本。

If you’re building an API application, you should avoid exposing your application over HTTP entirely and use only HTTPS. Mobile and other nonbrowser clients don’t have protections such as HSTS, so there’s no safe way to support both HTTP and HTTPS. Disable HTTP for your app by listening only on https:// URLs, such as by setting ASPNETCORE_URLS=https://*:5001.
如果您正在构建 API 应用程序,则应避免完全通过 HTTP 公开应用程序,而仅使用 HTTPS。移动客户端和其他非浏览器客户端没有 HSTS 等保护措施,因此没有安全的方法可以同时支持 HTTP 和 HTTPS。通过仅侦听 https:// URL 来禁用应用程序的 HTTP,例如通过设置 ASPNETCORE_URLS=https://*:5001。

ASP.NET Core in Action 27 Publishing and deploying your application

27 Publishing and deploying your application
27 发布和部署应用程序

This chapter covers
本章涵盖

• Publishing an ASP.NET Core application
发布 ASP.NET Core 应用程序

• Hosting an ASP.NET Core application in IIS
在 IIS中托管 ASP.NET Core 应用程序

• Customizing the URLs for an ASP.NET Core app
自定义 ASP.NET Core 应用程序的 URL

We’ve covered a vast amount of ground so far in this book. We’ve gone over the basic mechanics of building an ASP.NET Core application, such as configuring dependency injection (DI), loading app settings, and building a middleware pipeline. We’ve looked at building APIs using minimal APIs and web API controllers. We’ve looked at the server-rendered UI side, using Razor templates and layouts to build an HTML response. And we’ve looked at higher-level abstractions, such as Entity Framework Core (EF Core) and ASP.NET Core Identity, that let you interact with a database and add users to your application. In this chapter we’re taking a slightly different route. Instead of looking at ways to build bigger and better applications, we’ll focus on what it means to deploy your application so that users can access it.
到目前为止,我们在这本书中已经涵盖了大量的内容。我们已经介绍了构建 ASP.NET Core 应用程序的基本机制,例如配置依赖项注入 (DI)、加载应用程序设置和构建中间件管道。我们已经研究了使用最少的 API 和 Web API 控制器来构建 API。我们已经了解了服务器呈现的 UI 端,使用 Razor 模板和布局来构建 HTML 响应。我们还研究了更高级别的抽象,例如 Entity Framework Core (EF Core) 和 ASP.NET Core Identity,它们允许您与数据库交互并将用户添加到您的应用程序。在本章中,我们采取了略有不同的路线。我们不会研究构建更大、更好的应用程序的方法,而是重点介绍部署应用程序以便用户可以访问它意味着什么。

We’ll start by looking again at the ASP.NET Core hosting model in section 27.1 and examining why you might want to host your application behind a reverse proxy instead of exposing your app directly to the internet. I show you the difference between running an ASP.NET Core app in development using dotnet run and publishing the app for use on a remote server. Finally, I describe some of the options available when you’re deciding how and where to deploy your app.
首先,我们将再次查看第 27.1 节中的 ASP.NET Core 托管模型,并研究为什么您可能希望将应用程序托管在反向代理后面,而不是直接向 Internet 公开应用程序。我将向您展示使用 dotnet run 在开发中运行 ASP.NET Core 应用程序与发布应用程序以在远程服务器上使用之间的区别。最后,我将介绍在决定如何以及在何处部署应用程序时可用的一些选项。

In section 27.2 I show you how to deploy your app to one such option: a Windows server running Internet Information Services (IIS). This is a typical deployment scenario for many developers who are familiar with the legacy .NET Framework version of ASP.NET, so it acts as a useful case study, but it’s certainly not the only possibility. I don’t go into all the technical details of configuring the venerable IIS system; instead, I show you the bare minimum required to get it up and running. If your focus is cross-platform development, don’t worry, because I don’t dwell on IIS for too long.
在第 27.2 节中,我将向您展示如何将您的应用程序部署到这样一个选项:运行 Internet Information Services (IIS) 的 Windows 服务器。对于许多熟悉旧版 ASP.NET 的 .NET Framework 版本的开发人员来说,这是一个典型的部署方案,因此它是一个有用的案例研究,但肯定不是唯一的可能性。我不会深入介绍配置古老的 IIS 系统的所有技术细节;相反,我向您展示了启动和运行它所需的最低限度。如果您的重点是跨平台开发,请不要担心,因为我不会在 IIS 上停留太久。

In section 27.3 I provide an introduction to hosting on Linux. You’ll see how it differs from hosting applications on Windows, learn the changes you need to make to your apps, and find out about some gotchas to look out for. I describe how reverse proxies on Linux differ from IIS and point you to some resources you can use to configure your environments rather than give exhaustive instructions in this book.
在 Section 27.3 中,我介绍了在 Linux 上托管。您将了解它与在 Windows 上托管应用程序有何不同,了解您需要对应用程序进行的更改,并了解一些需要注意的问题。我将介绍 Linux 上的反向代理与 IIS 的不同之处,并向您指出一些可用于配置环境的资源,而不是在本书中提供详尽的说明。

If you’re not hosting your application using IIS, you’ll likely need to set the URL that your ASP.NET Core app is using when you deploy your application. In section 27.4 I show two approaches: using the special ASPNETCORE_URLS environment variable and using command-line arguments. Although this task generally is not a problem during development, setting the correct URLs for your app is critical when you need to deploy it.
如果不使用 IIS 托管应用程序,则可能需要设置 ASP.NET Core 应用在部署应用程序时使用的 URL。在 Section 27.4 中,我展示了两种方法:使用特殊的 ASPNETCORE_URLS 环境变量和使用命令行参数。尽管此任务在开发过程中通常不是问题,但当您需要部署应用程序时,为应用程序设置正确的 URL 至关重要。

This chapter covers a relatively wide array of topics, all related to deploying your app. But before we get into the nitty-gritty, I’ll go over the hosting model for ASP.NET Core so that we’re on the same page. This is significantly different from the hosting model of the legacy version of ASP.NET, so if you’re coming from that background, it’s best to try to forget what you know!
本章涵盖了相对广泛的主题,所有主题都与部署应用程序有关。但在我们进入细节之前,我将介绍 ASP.NET Core 的托管模型,以便我们达成共识。这与旧版 ASP.NET 的托管模式有很大不同,因此如果您来自该背景,最好尝试忘记您所知道的!

27.1 Understanding the ASP.NET Core hosting model

27.1 了解 ASP.NET Core 托管模型

If you think back to part 1 of this book, you may remember that we discussed the hosting model of ASP.NET Core. ASP.NET Core applications are, essentially, console applications. They have a static void Main function that is the entry point for the application, as a standard .NET console app would.
如果您回想一下本书的第 1 部分,您可能还记得我们讨论了 ASP.NET Core 的托管模型。ASP.NET Core 应用程序本质上是控制台应用程序。它们具有一个静态 void Main 函数,该函数是应用程序的入口点,就像标准 .NET 控制台应用程序一样。

NOTE The entry point for programs using top-level statements is automatically generated by the compiler. It’s not called Main (it typically has an “invalid” name, such as <Main>$ ), but otherwise it has the same signature as the classic static void Main function you would write by hand.
注意使用 top-level 语句的程序的入口点由编译器自动生成。它不称为 Main (它通常具有“无效”名称,例如 <Main>$ ),但除此之外,它与你手动编写的经典静态 void Main 函数具有相同的签名。

What makes a .NET app an ASP.NET Core app is that it runs a web server, typically Kestrel, inside the console app process. Kestrel provides the HTTP functionality to receive requests and return responses to clients. Kestrel passes any requests it receives to the body of your application and generates a response, as shown in figure 27.1. This hosting model decouples the server and reverse proxy from the application itself so that the same application can run unchanged in multiple environments.
使 .NET 应用程序成为 ASP.NET Core 应用程序的原因是它在控制台应用程序进程中运行 Web 服务器,通常是 Kestrel。Kestrel 提供 HTTP 功能来接收请求并将响应返回给客户端。Kestrel 将其收到的任何请求传递到应用程序的主体并生成响应,如图 27.1 所示。此托管模型将服务器和反向代理与应用程序本身分离,以便同一应用程序可以在多个环境中保持不变地运行。

alt text

Figure 27.1 The hosting model for ASP.NET Core gives flexibility. The same application can run exposed directly to the network, behind various reverse proxies without modification, and even inside the IIS process.
图 27.1 ASP.NET Core 的托管模型提供了灵活性。同一个应用程序可以直接暴露在网络中,无需修改即可在各种反向代理后面运行,甚至可以在 IIS 进程内部运行。

In this book we’ve focused on the “application” part of figure 27.1—the ASP.NET Core application itself—but the reality is that sometimes you’ll want to place your ASP.NET Core apps behind a reverse proxy, such as IIS in Windows or NGINX or Apache in Linux. The reverse proxy is the program that listens for HTTP requests from the internet and then makes requests to your app as though the request came from the internet directly.
在本书中,我们重点介绍了图 27.1 的 “应用程序” 部分 — ASP.NET Core 应用程序本身 — 但现实情况是,有时您希望将 ASP.NET Core 应用程序放在反向代理后面,例如 Windows 中的 IIS,Linux 中的 NGINX 或 Apache。反向代理是侦听来自 Internet 的 HTTP 请求,然后向应用程序发出请求的程序,就像请求直接来自 Internet 一样。

DEFINITION A reverse proxy is software that’s responsible for receiving requests and forwarding them to the appropriate web server. The reverse proxy is exposed directly to the internet, whereas the underlying web server is exposed only to the proxy.
定义:反向代理是负责接收请求并将其转发到适当的 Web 服务器的软件。反向代理直接向 Internet 公开,而底层 Web 服务器仅向代理公开。

If you’re running your application using a Platform as a Service (PaaS) offering such as Azure App Service, you’re using a reverse proxy there too—one that is managed by Azure. Using a reverse proxy has many benefits:
如果使用平台即服务 (PaaS) 产品(如 Azure 应用服务)运行应用程序,则也会使用由 Azure 管理的反向代理。使用反向代理有很多好处:

• Security—Reverse proxies are specifically designed to be exposed to malicious internet traffic, so they’re typically extremely well-tested and battle-hardened.
安全性 - 反向代理专门设计用于暴露于恶意 Internet 流量,因此它们通常经过极其严格的测试和战斗。

• Performance—You can configure reverse proxies to provide performance improvements by aggressively caching responses to requests.
性能 - 您可以配置反向代理,通过主动缓存对请求的响应来提高性能。

• Process management—An unfortunate reality is that apps sometimes crash. Some reverse proxies can act as monitors/schedulers to ensure that if an app crashes, the proxy can automatically restart it.
进程管理 - 一个不幸的现实是,应用程序有时会崩溃。一些反向代理可以充当监视器/调度程序,以确保如果应用程序崩溃,代理可以自动重新启动它。

• Support for multiple apps—It’s common to have multiple apps running on a single server. Using a reverse proxy makes it easier to support this scenario by using the host name of a request to decide which app should receive the request.
支持多个应用程序 - 在单个服务器上运行多个应用程序是很常见的。使用反向代理可以更轻松地支持此方案,方法是使用请求的主机名来决定哪个应用应接收请求。

I don’t want to make it seem like using a reverse proxy is all sunshine and roses. There are some downsides:
我不想让使用反向代理看起来全是阳光和玫瑰。有一些缺点:

• Complexity—One of the biggest complaints is how complex reverse proxies can be. If you’re managing the proxy yourself (as opposed to relying on a PaaS implementation), there can be lots of proxy-specific pitfalls to look out for.
复杂性 - 最大的抱怨之一是反向代理的复杂程度。如果您自己管理代理(而不是依赖 PaaS 实现),则可能会有许多特定于代理的陷阱需要注意。

• Inter-process communication—Most reverse proxies require two processes: a reverse proxy and your web app. Communicating between the two is often slower than if you directly exposed your web app to requests from the internet.
进程间通信 - 大多数反向代理需要两个进程:反向代理和 Web 应用程序。两者之间的通信通常比直接将 Web 应用程序公开给来自 Internet 的请求要慢。

• Restricted features—Not all reverse proxies support all the same features as an ASP.NET Core app. For example, Kestrel supports HTTP/2, but if your reverse proxy doesn’t, you won’t see the benefits.
受限功能 - 并非所有反向代理都支持与 ASP.NET Core 应用程序相同的所有功能。例如,Kestrel 支持 HTTP/2,但如果您的反向代理不支持,您将看不到好处。

Whether you choose to use a reverse proxy or not, when the time comes to host your app, you can’t copy your code files directly to the server. First, you need to publish your ASP.NET Core app to optimize it for production. In section 27.1.1 we’ll look at building an ASP.NET Core app so that it can be run on your development machine, compared with publishing it so that it can be run on a server.
无论您是否选择使用反向代理,当需要托管您的应用程序时,您都无法将代码文件直接复制到服务器。首先,您需要发布 ASP.NET Core 应用程序以针对生产环境进行优化。在 Section 27.1.1 中,我们将介绍如何构建 ASP.NET Core 应用程序,使其可以在开发计算机上运行,而不是发布应用程序,使其可以在服务器上运行。

27.1.1 Running vs. publishing an ASP.NET Core app

27.1.1 运行与发布 ASP.NET Core 应用程序

One of the key changes in ASP.NET Core from previous versions of ASP.NET is making it easy to build apps using your favorite code editors and integrated development environments (IDEs). Previously, Visual Studio was required for ASP.NET development, but with the .NET command-line interface (CLI), you can build apps with the tools you’re comfortable with on any platform.
ASP.NET Core 与以前的 ASP.NET 版本相比,其中一项关键变化是,使用您最喜欢的代码编辑器和集成开发环境 (IDE) 可以轻松构建应用程序。以前,ASP.NET 开发需要 Visual Studio,但借助 .NET 命令行界面 (CLI),您可以使用熟悉的工具在任何平台上构建应用程序。

As a result, whether you build using Visual Studio or the .NET CLI, the same tools are being used under the hood. Visual Studio provides an additional graphical user interface (GUI), functionality, and wrappers for building your app, but it (mostly) executes the same commands as the .NET CLI behind the scenes.
因此,无论您是使用 Visual Studio 还是 .NET CLI 进行构建,都在后台使用相同的工具。Visual Studio 提供了用于构建应用程序的额外图形用户界面 (GUI)、功能和包装器,但它(主要)在后台执行与 .NET CLI 相同的命令。

As a refresher, you’ve used four main .NET CLI commands so far to build your apps:
作为复习,到目前为止,您已经使用了四个主要的 .NET CLI 命令来构建您的应用程序:

• dotnet new—Creates an ASP.NET Core application from a template
dotnet new - 从模板创建 ASP.NET Core 应用程序
• dotnet restore—Downloads and installs any referenced NuGet packages for your project
dotnet restore - 下载并安装项目的任何引用的 NuGet 包
• dotnet build—Compiles and builds your project
dotnet build - 编译和生成项目
• dotnet run—Executes your app so you can send requests to it
dotnet run - 执行应用程序,以便您可以向其发送请求

If you’ve ever built a .NET application, whether it’s a legacy ASP.NET app or a .NET Framework console app, you’ll know that the output of the build process is written to the bin folder by default. The same is true for ASP.NET Core applications.
如果您曾经构建过 .NET 应用程序,无论是旧版 ASP.NET 应用程序还是 .NET Framework 控制台应用程序,您都会知道默认情况下,构建过程的输出会写入 bin 文件夹。ASP.NET Core 应用程序也是如此。

If your project compiles successfully when you call dotnet build, the .NET CLI writes the artifacts to a bin folder in your project’s directory. Inside this bin folder are several files required to run your app, including a .dll file that contains the code for your application. Figure 27.2 shows the output of the bin folder for a basic ASP.NET Core application.
如果在调用 dotnet build 时项目编译成功,则 .NET CLI 会将项目写入项目目录中的 bin 文件夹。此 bin 文件夹中包含运行应用程序所需的多个文件,包括一个包含应用程序代码的 .dll 文件。图 27.2 显示了基本 ASP.NET Core 应用程序的 bin 文件夹的输出。

alt text

Figure 27.2 The bin folder for an ASP.NET Core app after running dotnet build. The application is compiled into a single .dll file, ExampleApp.dll.
图 27.2 运行 dotnet build 后 ASP.NET Core 应用的 bin 文件夹。该应用程序被编译成一个 .dll 文件 ExampleApp.dll。

NOTE In Windows you also have an executable .exe file, ExampleApp.exe. This is a simple wrapper file for convenience that makes it easier to run the application contained in ExampleApp.dll.
注意:在 Windows 中,您还有一个可执行的 .exe 文件 ExampleApp.exe。为方便起见,这是一个简单的包装文件,可以更轻松地运行 ExampleApp.dll 中包含的应用程序。

When you call dotnet run in your project folder (or run your application using Visual Studio), the .NET CLI uses the .dll to run your application. But this file doesn’t contain everything you need to deploy your app.
在项目文件夹中调用 dotnet run (或使用 Visual Studio 运行应用程序) 时,.NET CLI 会使用.dll运行应用程序。但此文件并不包含部署应用程序所需的一切。

To host and deploy your app on a server, you first need to publish it. You can publish your ASP.NET Core app from the command line using the dotnet publish command, which builds and packages everything your app needs to run. The following command packages the app from the current directory and builds it to a subfolder called publish. I’ve used the Release configuration instead of the default Debug configuration so that the output will be fully optimized for running in production:
要在服务器上托管和部署您的应用程序,您首先需要发布它。可以使用 dotnet publish 命令从命令行发布 ASP.NET Core 应用,该命令将生成和打包应用运行所需的一切。以下命令将当前目录中的应用程序打包,并将其构建到名为 publish 的子文件夹中。我使用了 Release 配置而不是默认的 Debug 配置,以便输出将得到全面优化,以便在生产环境中运行:

dotnet publish --output publish --configuration Release

TIP Always use the Release configuration when publishing your app for deployment. This ensures that the compiler generates optimized code for your app.
提示:在发布应用程序进行部署时,请始终使用 Release (发布) 配置。这可确保编译器为您的应用生成优化的代码。

Once the command completes, you’ll find your published application in the publish folder, as shown in figure 27.3.
命令完成后,您将在 publish 文件夹中找到已发布的应用程序,如图 27.3 所示。

alt text

Figure 27.3 The publish folder for the app after running dotnet publish. The app is still compiled into a single .dll file, but all the additional files, such as wwwroot, are also copied to the output.
图 27.3 运行 dotnet publish 后应用程序的 publish 文件夹。该应用程序仍编译为单个 .dll 文件,但所有其他文件(如 wwwroot)也会复制到输出中。

As you can see, the ExampleApp.dll file is still there, along with some additional files. Most notably, the publish process has copied across the wwwroot folder of static files. When running your application locally with dotnet run, the .NET CLI uses these files from your application’s project folder directly. Running dotnet publish copies the files to the output directory, so they’re included when you deploy your app to a server.
如您所见,ExampleApp.dll 文件仍然存在,还有一些其他文件。最值得注意的是,发布过程已复制静态文件的 wwwroot 文件夹。使用 dotnet run 在本地运行应用程序时,.NET CLI 会直接使用应用程序项目文件夹中的这些文件。运行 dotnet publish 会将文件复制到输出目录,以便在将应用部署到服务器时包含这些文件。

If your first instinct is to try running the application in the publish folder using the dotnet run command you already know and love, you’ll be disappointed. Instead of seeing the application starting up, you’ll see a somewhat confusing message: Couldn’t find a project to run.
如果您的第一反应是尝试使用您已经熟悉和喜爱的 dotnet run 命令在 publish 文件夹中运行应用程序,那么您会感到失望。您将看到一条有点令人困惑的消息,而不是看到应用程序启动:Couldn't find a project to run。

To run a published application, you need to use a slightly different command. Instead of calling dotnet run, you must pass the path to your application’s .dll file to the dotnet command. If you’re running the command from the publish folder, for the example app in figure 27.3, it would look something like
要运行已发布的应用程序,您需要使用略有不同的命令。必须将应用程序的 .dll 文件的路径传递给 dotnet 命令,而不是调用 dotnet run。如果您从 publish 文件夹运行命令,对于图 27.3 中的示例应用程序,它看起来类似于

dotnet ExampleApp.dll

This is the command that your server will run when running your application in production.
这是您的服务器在生产环境中运行应用程序时将运行的命令。

TIP You can also use the dotnet exec command to achieve the same thing, such as dotnet exec ExampleApp.dll. This makes some advanced runtime options available, as described in the docs at http://mng.bz/x4d8.
提示:您还可以使用 dotnet exec 命令来实现相同的作,例如 dotnet exec ExampleApp.dll。这使得一些高级运行时选项可用,如 http://mng.bz/x4d8 中的文档中所述。

When you’re developing, the dotnet run command does a whole load of work to make things easier on you. It makes sure that your application is built, looks for a project file in the current folder, works out where the corresponding .dlls will be (in the bin folder), and finally runs your app.
在开发时,dotnet run 命令会执行大量工作,以简化您的作。它确保您的应用程序已构建,在当前文件夹中查找项目文件,计算出相应的 .dll 将位于何处(在 bin 文件夹中),最后运行您的应用程序。

In production, you don’t need any of this extra work. Your app is already built; it only needs to be run. The dotnet <dll> syntax does this alone, so your app starts much faster.
在生产环境中,您不需要任何额外的工作。您的应用程序已构建完毕;它只需要运行。dotnet <dll>语法单独执行此作,因此您的应用启动速度要快得多。

NOTE The dotnet command used to run your published application is part of the .NET Runtime. The (identically named) dotnet command used to build and run your application during development is part of the .NET software development kit (SDK).
注意:用于运行已发布应用程序的 dotnet 命令是 .NET 运行时的一部分。在开发过程中用于生成和运行应用程序的(同名)dotnet 命令是 .NET 软件开发工具包 (SDK) 的一部分。

Framework-dependent deployments vs. self-contained deployments
依赖于框架的部署与独立部署:

.NET Core applications can be deployed in two ways: runtime-dependent deployments (RDD) and self-contained deployments (SCD).
.NET Core 应用程序可以通过两种方式进行部署:依赖于运行时的部署 (RDD) 和独立部署 (SCD)。

By default, you’ll use an RDD. This relies on the .NET 7 runtime being installed on the target machine that runs your published app, but you can run your app on any platform—Windows, Linux, or macOS—without having to recompile.
默认情况下,您将使用 RDD。这依赖于在运行已发布应用程序的目标计算机上安装的 .NET 7 运行时,但您可以在任何平台(Windows、Linux 或 macOS)上运行您的应用程序,而无需重新编译。

By contrast, an SCD contains all the code required to run your app, so the target machine doesn’t need to have .NET 7 installed. Instead, publishing your app packages up the .NET 7 runtime with your app’s code and libraries.
相比之下,SCD 包含运行应用程序所需的所有代码,因此目标计算机不需要安装 .NET 7。相反,将应用与应用的代码和库一起发布到 .NET 7 运行时中。

Each approach has its pros and cons, but in most cases I tend to create RDDs. The final size of RDDs is much smaller, as they contain only your app code instead of the whole .NET 7 framework, which SCDs contain. Also, you can deploy your RDD apps to any platform, whereas SCDs must be compiled specifically for the target machine’s operating system, such as Windows 10 64-bit or Red Hat Enterprise Linux 64-bit.
每种方法都有其优点和缺点,但在大多数情况下,我倾向于创建 RDD。RDD 的最终大小要小得多,因为它们仅包含您的应用程序代码,而不是 SCD 包含的整个 .NET 7 框架。此外,您可以将 RDD 应用程序部署到任何平台,而 SCD 必须专门为目标计算机的作系统(例如 Windows 10 64 位或 Red Hat Enterprise Linux 64 位)进行编译。

That said, SCDs are excellent for isolating your application from dependencies on the hosting machine. SCDs don’t rely on the version of .NET installed on a hosting provider, so you can (for example) use preview versions of .NET in Azure App Service without needing the preview version to be supported.
也就是说,SCD 非常适合将您的应用程序与主机上的依赖项隔离开来。SCD 不依赖于托管提供商上安装的 .NET 版本,因此你可以(例如)在 Azure 应用服务中使用 .NET 的预览版,而无需支持预览版。

Another advantage of SCDs is for regulated industries that require certification or procedure to change applications. In RDDs (such as in Azure App Service) the underlying runtime may be patched at any time without your intervention, potentially leading to noncompliance. With SCDs, your app contains a fixed runtime and can be considered an immutable snapshot of your app. Of course, that means you must make sure to patch the runtime of your SCDs manually, performing regular deployments. Patch versions of the .NET runtime are generally released every month, so make sure to plan for at least monthly releases of your SCD apps.
SCD 的另一个优势是适用于需要认证或程序来更改应用程序的受监管行业。在 RDD 中(例如在 Azure 应用服务中),可能随时修补基础运行时,而无需您的干预,这可能会导致不合规。使用 SCD 时,您的应用程序包含固定的运行时,并且可以被视为应用程序的不可变快照。当然,这意味着您必须确保手动修补 SCD 的运行时,并执行定期部署。.NET 运行时的补丁版本通常每个月发布一次,因此请确保至少每月发布一次 SCD 应用程序。

In this book I discuss RDDs only for simplicity, but if you want to create an SCD, provide a runtime identifier (in this case, Windows 10 64-bit) when you publish your app:
在本书中,我讨论 RDD 只是为了简单起见,但如果您想创建一个 SCD,请在发布应用程序时提供运行时标识符(在本例中为 Windows 10 64 位):

dotnet publish -c Release -r win10-x64 --self-contained -o publish_folder

The output will contain an .exe file, which is your application, and a ton of .dlls (about 100 MB of .dlls for a default sample app), which are the .NET 7 framework. You need to deploy this whole folder to the target machine to run your app. Note that you need to publish for a specific operating system and architecture. The list of available runtime identifiers is available in the documentation at http://mng.bz/Aolp.
输出将包含一个 .exe 文件(即您的应用程序)和大量 .dll(默认示例应用程序约为 100 MB 的 .dll),即 .NET 7 框架。您需要将整个文件夹部署到目标计算机才能运行您的应用程序。请注意,您需要针对特定的作系统和架构进行发布。可用运行时标识符的列表可在 http://mng.bz/Aolp 的文档中找到。

In .NET 7 it’s possible to trim these assemblies during the publish process, but this comes with risks in some scenarios. You can also bundle this folder into a single file automatically for easier deployments. For more details, see Microsoft’s “.NET application publishing overview” documentation at https://learn.microsoft.com/dotnet/core/deploying.
在 .NET 7 中,可以在发布过程中剪裁这些程序集,但在某些情况下,这会带来风险。您还可以自动将此文件夹捆绑到一个文件中,以便于部署。有关更多详细信息,请参阅 https://learn.microsoft.com/dotnet/core/deploying Microsoft的“.NET 应用程序发布概述”文档。

We’ve established that publishing your app is important for preparing it to run in production, but how do you go about deploying it? How do you get the files from your computer onto a server so that people can access your app? You have many, many options, so in the next section I’ll give you a brief list of approaches to consider.
我们已经确定,发布您的应用程序对于准备在生产环境中运行非常重要,但您如何部署它呢?如何将文件从计算机传输到服务器上,以便人们可以访问您的应用程序?您有很多很多选择,因此在下一节中,我将简要列出要考虑的方法。

27.1.2 Choosing a deployment method for your application

27.1.2 为应用程序选择部署方法

To deploy any application to production, you generally have two fundamental requirements:
要将任何应用程序部署到生产环境,您通常有两个基本要求:

• A server that can run your app
可以运行应用程序的服务器
• A means of loading your app onto the server
将应用程序加载到服务器上的方法

Historically, putting an app into production was a laborious and error-prone process. For many people, this is still true. If you’re working at a company that hasn’t changed practices in recent years, you may need to request a server or virtual machine for your app and provide your application to an operations team that will install it for you. If that’s the case, you may have your hands tied regarding how you deploy.
从历史上看,将应用程序投入生产是一个费力且容易出错的过程。对许多人来说,这仍然是正确的。如果你在一家近年来没有改变做法的公司工作,你可能需要为你的应用程序请求一个服务器或虚拟机,并将你的应用程序提供给运营团队,由他们为你安装它。如果是这种情况,您可能会在部署方式上束手无策。

For those who have embraced continuous integration (CI) or continuous delivery/deployment (CD), there are many more possibilities. CI/CD is the process of detecting changes in your version control system (for example, Git, SVN, Mercurial, or Team Foundation Version Control) and automatically building, and potentially deploying, your application to a server with little to no human intervention.
对于那些已经接受持续集成 (CI) 或持续交付/部署 (CD) 的人来说,还有更多的可能性。CI/CD 是检测版本控制系统(例如 Git、SVN、Mercurial 或 Team Foundation 版本控制)中的更改并自动构建应用程序并可能将应用程序部署到服务器的过程,几乎不需要人工干预。

NOTE There are important but subtle differences between these terms. Atlassian has a good comparison article, “Continuous integration vs. continuous delivery vs. continuous deployment,” at http://mng.bz/vzp4.
注意:这些术语之间存在重要但细微的差异。Atlassian 在 http://mng.bz/vzp4 上有一篇很好的比较文章“持续集成、持续交付与持续部署”。

There are many CI/CD systems out there—Azure DevOps, GitHub Actions, Jenkins, TeamCity, AppVeyor, Travis, and Octopus Deploy, to name a few. Each can manage some or all of the CI/CD process and can integrate with many systems.
有许多 CI/CD 系统,包括 Azure DevOps、GitHub Actions、Jenkins、TeamCity、AppVeyor、Travis 和 Octopus Deploy 等。每个系统都可以管理部分或全部 CI/CD 流程,并且可以与许多系统集成。

Rather than push any particular system, I suggest trying some of the services available and seeing which works best for you. Some are better suited to open-source projects, and some are better when you’re deploying to cloud services; it all depends on your particular situation.
与其推动任何特定的系统,我建议尝试一些可用的服务,看看哪种最适合您。有些更适合开源项目,有些更适合部署到云服务;这完全取决于您的具体情况。

If you’re getting started with ASP.NET Core and don’t want to have to go through the setup process of getting CI working, you still have lots of options. The easiest way to get started with Visual Studio is to use the built-in deployment options. These are available from Visual Studio via the Build > Publish <AppName> command, which presents the screen shown in figure 27.4.
如果您刚开始使用 ASP.NET Core,并且不想完成让 CI 正常工作的设置过程,那么您仍然有很多选择。开始使用 Visual Studio 的最简单方法是使用内置的部署选项。这些可以通过命令从 Visual Studio Build > Publish <AppName> 获得,该命令显示图 27.4 所示的屏幕。

alt text

Figure 27.4 The Publish application screen in Visual Studio 2022. This provides easy options for publishing your application directly to Azure App Service, to IIS, to an FTP site, or to a folder on the local machine.
图 27.4 Visual Studio 2022 中的 Publish application(发布应用程序)屏幕。这为将应用程序直接发布到 Azure 应用服务、IIS、FTP 站点或本地计算机上的文件夹提供了简单的选项。

From here, you can publish your application directly from Visual Studio to many locations. This is great when you’re getting started, though I recommend looking at a more automated and controlled approach when you have a larger application or a whole team working on a single app.
在这里,您可以直接从 Visual Studio 将应用程序发布到许多位置。这在您开始时非常有用,但当您有更大的应用程序或整个团队在开发单个应用程序时,我建议您考虑一种更加自动化和可控的方法。

TIP For guidance on choosing your Visual Studio publishing options, see Microsoft’s “Deploy your app to a folder, IIS, Azure, or another destination” documentation at http://mng.bz/4Z8j.
提示:有关选择 Visual Studio 发布选项的指导,请参阅 http://mng.bz/4Z8j Microsoft的“将应用程序部署到文件夹、IIS、Azure 或其他目标”文档。

Given the number of possibilities available in this space and the speed with which these options change, I’m going to focus on one specific scenario in this chapter: you’ve built an ASP.NET Core application, and you need to deploy it. You have access to a Windows server that’s already serving legacy .NET Framework ASP.NET applications using IIS, and you want to run your ASP.NET Core app alongside them.
考虑到此领域中可用的可能性数量以及这些选项变化的速度,我将在本章中重点介绍一个特定场景:您已经构建了一个 ASP.NET Core 应用程序,并且需要部署它。您可以访问已使用 IIS 为旧版 .NET Framework ASP.NET 应用程序提供服务的 Windows 服务器,并且您希望与它们一起运行 ASP.NET Core 应用程序。

In the next section you’ll see an overview of the steps required to run an ASP.NET Core application in production, using IIS as a reverse proxy. It won’t be a master class in configuring IIS (there’s so much depth to the 25-year-old product that I wouldn’t know where to start!), but I’ll cover the basics needed to get your application serving requests.
在下一部分中,你将看到使用 IIS 作为反向代理在生产中运行 ASP.NET Core 应用程序所需步骤的概述。它不会是配置 IIS 的大师课程(这个 25 年前的产品有太多的深度,我不知道从哪里开始!),但我将介绍让您的应用程序为请求提供服务所需的基础知识。

27.2 Publishing your app to IIS

27.2 将应用程序发布到 IIS

In this section I briefly show you how to publish your first app to IIS. You’ll add an application pool and website to IIS and ensure that your app has the necessary configuration to work with IIS as a reverse proxy. The deployment itself will be as simple as copying your published app to IIS’s hosting folder.
在本节中,我将简要介绍如何将第一个应用程序发布到 IIS。你将向 IIS 添加应用程序池和网站,并确保你的应用程序具有将 IIS 用作反向代理所需的配置。部署本身就像将您发布的应用程序复制到 IIS 的托管文件夹一样简单。

In section 27.1 you learned about the need to publish an app before you deploy it and the benefits of using a reverse proxy when you run an ASP.NET Core app in production. If you’re deploying your application to Windows, IIS will likely be your reverse proxy and will be responsible for managing your application.
在第 27.1 节中,您了解了在部署应用程序之前发布应用程序的必要性,以及在生产环境中运行 ASP.NET Core 应用程序时使用反向代理的好处。如果要将应用程序部署到 Windows,IIS 可能是您的反向代理,并负责管理您的应用程序。

IIS is an old and complex beast, and I can’t possibly cover everything related to configuring it in this book. Neither would you want me to; that discussion would be boring! Instead, in this section I’ll provide an overview of the basic requirements for running ASP.NET Core behind IIS, along with the changes you may need to make to your application to support IIS.
IIS 是一个古老而复杂的野兽,我不可能在本书中涵盖与配置它相关的所有内容。你也不希望我这样做;那个讨论会很无聊!相反,在本节中,我将概述在 IIS 后面运行 ASP.NET Core 的基本要求,以及您可能需要对应用程序进行的更改以支持 IIS。

If you’re on Windows and want to try these steps locally, you’ll need to enable IIS manually on your development machine. If you’ve done this with older versions of Windows, nothing much has changed. You can find a step-by-step guide to configuring IIS and troubleshooting tips in the ASP.NET Core documentation at http://mng.bz/6g2R.
如果您使用的是 Windows 并希望在本地尝试这些步骤,则需要在开发计算机上手动启用 IIS。如果你在旧版本的 Windows 上这样做了,那么没有什么太大的变化。您可以在 http://mng.bz/6g2R 的 ASP.NET Core 文档中找到配置 IIS 的分步指南和故障排除提示。

27.2.1 Configuring IIS for ASP.NET Core

27.2.1 为 ASP.NET Core 配置 IIS

The first step in preparing IIS to host ASP.NET Core applications is installing the ASP.NET Core Windows Hosting Bundle (http://mng.bz/opED). This includes several components needed to run .NET apps:
准备 IIS 以托管 ASP.NET Core 应用程序的第一步是安装 ASP.NET Core Windows 托管捆绑包 (http://mng.bz/opED)。这包括运行 .NET 应用所需的几个组件:

• The .NET Runtime—Runs your .NET 7 application
.NET 运行时 - 运行 .NET 7 应用程序

• The ASP.NET Core Runtime—Required to run ASP.NET Core apps
ASP.NET Core 运行时 - 运行 ASP.NET Core 应用程序所需

• The IIS AspNetCore Module—Provides the link between IIS and your app so that IIS can act as a reverse proxy
IIS AspNetCore 模块 - 提供 IIS 和应用程序之间的链接,以便 IIS 可以充当反向代理

If you’re going to be running IIS on your development machine, make sure that you install the bundle as well; otherwise, you’ll get strange errors from IIS.
如果要在开发计算机上运行 IIS,请确保同时安装捆绑包;否则,您将从 IIS 收到奇怪的错误。

TIP The Windows Hosting Bundle provides everything you need for running ASP.NET Core behind IIS in Windows. If you’re hosting your application in Linux or Mac, or aren’t using IIS in Windows, you need to install only the .NET Runtime and ASP.NET Core Runtime to run runtime-dependent ASP.NET Core apps. Note that you need to install the IIS AspNetCore Module even if you are using SCDs.
提示:Windows 托管捆绑包提供了在 Windows 中在 IIS 后面运行 ASP.NET Core 所需的一切。如果您在 Linux 或 Mac 中托管应用程序,或者不在 Windows 中使用 IIS,则只需安装 .NET 运行时和 ASP.NET Core 运行时即可运行依赖于运行时的 ASP.NET Core 应用程序。请注意,即使您使用的是 SCD,也需要安装 IIS AspNetCore 模块。

Once you’ve installed the bundle, you need to configure an application pool in IIS for your ASP.NET Core apps. Previous versions of ASP.NET would run in a managed app pool that used .NET Framework, but for ASP.NET Core you should create a No Managed Code pool. The native ASP.NET Core Module runs inside the pool, which boots the .NET 7 Runtime itself.
安装捆绑包后,您需要在 IIS 中为 ASP.NET Core 应用程序配置应用程序池。早期版本的 ASP.NET 将在使用 .NET Framework 的托管应用程序池中运行,但对于 ASP.NET Core,您应该创建一个无托管代码池。本机 ASP.NET Core Module 在池内运行,该池启动 .NET 7 运行时本身。

DEFINITION An application pool in IIS represents an application process. You can run each app in IIS in a separate application pool to keep the apps isolated from one another.
定义:IIS 中的应用程序池表示应用程序进程。您可以在 IIS 中的单独应用程序池中运行每个应用程序,以使应用程序彼此隔离。

To create an unmanaged application pool, right-click Application Pools in IIS and choose Add Application Pool from the contextual menu. Provide a name for the app pool in the resulting dialog box, such as dotnet7, and set the .NET CLR version to No Managed Code, as shown in figure 27.5.
要创建非托管应用程序池,请右键单击 IIS 中的应用程序池,然后从上下文菜单中选择添加应用程序池。在生成的对话框中为应用程序池提供一个名称,例如 dotnet7,并将 .NET CLR 版本设置为“无托管代码”,如图 27.5 所示。

alt text

Figure 27.5 Creating an app pool in IIS for your ASP.NET Core app. The .NET CLR version should be set to No Managed Code.
图 27.5 在 IIS 中为您的 ASP.NET Core 应用程序创建应用程序池。.NET CLR 版本应设置为无托管代码。

Now that you have an app pool, you can add a new website to IIS. Right-click the Sites node, and choose Add Website from the contextual menu. In the Add Website dialog box, shown in figure 27.6, you provide a name for the website and the path to the folder where you’ll publish your website. I created a folder that I’ll use to deploy the Recipe app from previous chapters. It’s important to change the Application Pool for the app to the new dotnet7 app pool you created. In production, you’d also provide a hostname for the application, but I’ve left it blank for now in this example and changed the port to 81 so the application will bind to the URL http://localhost:81.
现在,您已拥有应用程序池,可以向 IIS 添加新网站。右键单击 Sites 节点,然后从上下文菜单中选择 Add Website。在 Add Website 对话框中(如图 27.6 所示),您需要提供网站的名称以及要发布网站的文件夹的路径。我创建了一个文件夹,我将使用它来部署前面章节中的 Recipe 应用程序。请务必将应用的 Application Pool(应用程序池)更改为您创建的新 dotnet7 应用程序池。在生产环境中,您还需要为应用程序提供主机名,但在此示例中,我暂时将其留空,并将端口更改为 81,以便应用程序将绑定到 URL http://localhost:81

NOTE When you deploy an application to production, you need to register a hostname with a domain registrar so that your site is accessible by people on the internet. Use that hostname when configuring your application in IIS, as shown in figure 27.6.
注意:当您将应用程序部署到生产环境时,您需要向域注册商注册主机名,以便 Internet 上的用户可以访问您的站点。在 IIS 中配置应用程序时,请使用该主机名,如图 27.6 所示。

alt text

Figure 27.6 Adding a new website to IIS for your app. Be sure to change the Application Pool to the No Managed Code pool created in the previous step. You also provide a name, the path where you’ll publish your app files, and the URL that IIS will use for your app.
图 27.6 为您的应用程序向 IIS 添加新网站。请务必将 Application Pool(应用程序池)更改为在上一步中创建的 No Managed Code (无托管代码) 池。您还提供名称、发布应用程序文件的路径以及 IIS 将用于应用程序的 URL。

Once you click OK, IIS creates the application and attempts to start it. But you haven’t published your app to the folder, so you won’t be able to view it in a browser yet.
单击“确定”后,IIS 将创建应用程序并尝试启动它。但是您尚未将应用程序发布到该文件夹,因此您尚无法在浏览器中查看它。

You need to carry out one more critical setup step before you can publish and run your app: grant permissions for the dotnet7 app pool to access the path where you’ll publish your app. To do this, right-click the folder that will host your app in Windows File Explorer, and choose Properties from the contextual menu. In the Properties dialog box, choose Security > Edit > Add. Enter IIS AppPool\dotnet7 in the text box, as shown in figure 27.7, where dotnet7 is the name of your app pool; then choose OK. Close all the dialog boxes by choosing OK, and you’re all set.
在发布和运行应用之前,还需要执行一个关键的设置步骤:为 dotnet7 应用池授予访问将发布应用的路径的权限。为此,请右键单击将在 Windows 文件资源管理器中托管您的应用程序的文件夹,然后从上下文菜单中选择 Properties (属性)。在 Properties (属性) 对话框中,选择 Security > Edit > Add。在文本框中输入 IIS AppPool\dotnet7,如图 27.7 所示,其中 dotnet7 是应用程序池的名称;然后选择 OK (确定)。选择 OK (确定) 关闭所有对话框,一切就绪。Security > Edit > Add

alt text

Figure 27.7 Adding permission for the dotnet7 app pool to the website’s publish folder
图 27.7 将 dotnet7 应用池的权限添加到网站的发布文件夹

Out of the box, the ASP.NET Core templates are configured to work seamlessly with IIS, but if you’ve created an app from scratch, you may need to make a couple of changes. In the next section I’ll briefly show the changes you need to make and explain why they’re necessary.
开箱即用的 ASP.NET Core 模板配置为与 IIS 无缝协作,但如果您从头开始创建应用程序,则可能需要进行一些更改。在下一节中,我将简要介绍您需要进行的更改,并解释为什么这些更改是必需的。

27.2.2 Preparing and publishing your application to IIS

27.2.2 准备应用程序并将其发布到 IIS

As I discussed in section 27.1, IIS acts as a reverse proxy for your ASP.NET Core app. That means IIS needs to be able to communicate directly with your app to forward incoming requests to and outgoing responses from your app.
正如我在 Section 27.1 中所讨论的,IIS 充当 ASP.NET Core 应用程序的反向代理。这意味着 IIS 需要能够直接与你的应用程序通信,以将传入请求转发到你的应用程序以及从你的应用程序转发传出响应。

IIS handles this with the ASP.NET Core Module, but a certain degree of negotiation is required between IIS and your app. For this to work correctly, you need to configure your app to use IIS integration.
IIS 使用 ASP.NET 核心模块处理此问题,但 IIS 和你的应用程序之间需要一定程度的协商。要使其正常工作,您需要将应用程序配置为使用 IIS 集成。

IIS integration is added automatically when you use WebApplicationBuilder, so there’s typically nothing more to do. However, in chapter 30 you’ll learn about the generic host and how to create custom application builders using HostBuilder. If your app uses a customer application builder and you want to use IIS, you need to ensure that you add IIS integration with the UseIIS() or UseIISIntegration() extension methods:
当您使用 WebApplicationBuilder 时,会自动添加 IIS 集成,因此通常无需执行更多作。但是,在第 30 章中,您将了解通用主机以及如何使用 HostBuilder 创建自定义应用程序构建器。如果您的应用程序使用客户应用程序构建器,并且您想要使用 IIS,则需要确保添加与 UseIIS() 或 UseIISIntegration() 扩展方法的 IIS 集成:

• UseIIS() configures your application to support IIS with an in-process hosting model.
UseIIS() 将应用程序配置为使用进程内托管模型支持 IIS。

• UseIISIntegration() configures your application to support IIS with an out-of-process hosting model.
UseIISIntegration() 将应用程序配置为使用进程外托管模型支持 IIS。

These methods are automatically called by WebApplicationBuilder, but if you’re not using your application with IIS, the UseIIS() and UseIISIntegration() methods will have no effect on your app, so it’s safe to include them anyway.
这些方法由 WebApplicationBuilder 自动调用,但如果您未将应用程序与 IIS 一起使用,则 UseIIS() 和 UseIISIntegration() 方法将对您的应用程序没有影响,因此无论如何都包含它们是安全的。

In-process vs. out-of-process hosting in IIS
IIS中的进程内托管与进程外托管

The common reverse-proxy description assumes that your application is running in a separate process from the reverse proxy itself. That is the case if you’re running on Linux and was the default for IIS up until ASP.NET Core 3.0.
常见的反向代理描述假定应用程序在与反向代理本身不同的进程中运行。如果您在 Linux 上运行,并且在 ASP.NET Core 3.0 之前是 IIS 的默认设置,则会出现这种情况。

In ASP.NET Core 3.0, ASP.NET Core switched to using an in-process hosting model by default for applications deployed to IIS. In this model, IIS hosts your application directly inside the IIS process, reducing interprocess communication and boosting performance.
在 ASP.NET Core 3.0 中,ASP.NET Core 默认切换到对部署到 IIS 的应用程序使用进程内托管模型。在此模型中,IIS 直接在 IIS 进程内托管您的应用程序,从而减少进程间通信并提高性能。

You can switch to the out-of-process hosting model with IIS if you wish, which can sometimes be useful for troubleshooting problems. Rick Strahl has an excellent post on the differences between the hosting models, how to switch between them, and the advantages of each: “ASP.NET Core In Process Hosting on IIS with ASP.NET Core” at http://mng.bz/QmEv.
如果需要,可以使用 IIS 切换到进程外托管模型,这有时可用于解决问题。Rick Strahl 有一篇关于托管模型之间的差异、如何在它们之间切换以及每种模型的优点的优秀文章:http://mng.bz/QmEv 上的“使用 ASP.NET Core 在 IIS 上进行 ASP.NET Core In Process 托管”。

In general, you shouldn’t need to worry about the differences between the hosting models, but it’s something to be aware of if you’re deploying to IIS. If you choose to use the out-of-process hosting model, you should use the UseIISIntegration() extension method. If you use the in-process model, use UseIIS(). Alternatively, play it safe and use both; the correct extension method is activated based on the hosting model used in production. Neither extension does anything if you don’t use IIS.
通常,您不需要担心托管模型之间的差异,但如果您要部署到 IIS,则需要注意这一点。如果您选择使用进程外托管模型,则应使用 UseIISIntegration() 扩展方法。如果使用进程内模型,请使用 UseIIS()。或者,安全起见,两者兼而有之;根据生产中使用的托管模型激活正确的扩展方法。如果您不使用 IIS,则两个扩展都不会执行任何作。

When running behind IIS, these extension methods configure your app to pair with IIS so that it can seamlessly accept requests. Among other things, the extensions do the following:
在 IIS 后面运行时,这些扩展方法将你的应用配置为与 IIS 配对,以便它可以无缝接受请求。此外,扩展还执行以下作:

• Define the URL that IIS uses to forward requests to your app and configures your app to listen on this URL
定义 IIS 用于将请求转发到应用程序的 URL,并将应用程序配置为侦听此 URL

• Configure your app to interpret requests coming from IIS as coming from the client by setting up header forwarding
通过设置标头转发,将应用程序配置为将来自 IIS 的请求解释为来自客户端的请求

• Enable Windows authentication if required
根据需要启用 Windows 身份验证

Adding the IIS extension methods is the only change you need to make to your application to host in IIS (and even then, only when using a custom application builder). But there’s one additional aspect to be aware of when you publish your app. As with legacy .NET Framework ASP.NET, IIS relies on a web.config file to configure the applications it runs. It’s important that your application include a web.config file when it’s published to IIS; otherwise you could get broken behavior or even expose files that shouldn’t be exposed.
添加 IIS 扩展方法是您需要对要在 IIS 中托管的应用程序进行的唯一更改(即使这样,也仅在使用自定义应用程序构建器时)。但是,在发布应用程序时,还有一个方面需要注意。与旧版 .NET Framework ASP.NET 一样,IIS 依赖于 web.config 文件来配置它运行的应用程序。应用程序在发布到 IIS 时必须包含 web.config 文件;否则,您可能会破坏行为,甚至暴露不应公开的文件。

TIP For details on using web.config to customize the IIS AspNetCore Module, see Microsoft’s “ASP.NET Core Module” documentation: http://mng.bz/Xdna.
提示:有关使用 web.config 自定义 IIS AspNetCore 模块的详细信息,请参阅 Microsoft 的“ASP.NET Core Module”文档:http://mng.bz/Xdna

If your ASP.NET Core project already includes a web.config file, the .NET CLI or Visual Studio copies it to the publish directory when you publish your app. If your app doesn’t include a web.config file, the publish command creates the correct one for you. If you don’t need to customize the web.config file, it’s generally best not to include one in your project and let the CLI create the correct file for you.
如果您的 ASP.NET Core 项目已包含 web.config 文件,则 .NET CLI 或 Visual Studio 会在您发布应用程序时将其复制到发布目录。如果您的应用程序不包含 web.config 文件,则 publish 命令将为您创建正确的文件。如果您不需要自定义 web.config 文件,通常最好不要在项目中包含一个文件,并让 CLI 为您创建正确的文件。

With these changes, you’re finally in a position to publish your application to IIS. Publish your ASP.NET Core app to a folder, either from Visual Studio or with the .NET CLI, by running
通过这些更改,您终于可以将应用程序发布到 IIS。通过运行 Visual Studio 或使用 .NET CLI 将 ASP.NET Core 应用发布到文件夹

dotnet publish --output publish_folder --configuration Release

This will publish your application to the publish_folder folder. You can then copy your application to the path specified in IIS, as shown in figure 27.6. At this point, if all has gone smoothly, you should be able to navigate to the URL you specified for your app (http://localhost:81, in my case) and see it running, as shown in figure 27.8.
这会将您的应用程序发布到 publish_folder 文件夹。然后,您可以将应用程序复制到 IIS 中指定的路径,如图 27.6 所示。此时,如果一切顺利,您应该能够导航到您为应用程序指定的 URL(在本例中为 http://localhost:81 URL)并看到它正在运行,如图 27.8 所示。

alt text

Figure 27.8 The published application, using IIS as a reverse proxy listening at the URL http://localhost:81
图 27.8 已发布的应用程序,使用 IIS 作为反向代理侦听 URL http://localhost:81

And there you have it—your first application running behind a reverse proxy. Even though ASP.NET Core uses a different hosting model from previous versions of ASP.NET, the process of configuring IIS is similar.
这就是 - 在反向代理后面运行的第一个应用程序。尽管 ASP.NET Core 使用的托管模型与以前版本的 ASP.NET 不同,但配置 IIS 的过程是相似的。

As is often the case when it comes to deployment, the success you have is highly dependent on your precise environment and your app itself. If, after following these steps, you find that you can’t get your application to start, I highly recommend checking out the documentation at http://mng.bz/Zqom. This contains many troubleshooting steps to get you back on track if IIS decides to throw a hissy fit.
与部署时的情况一样,您获得的成功在很大程度上取决于您的精确环境和应用程序本身。如果在执行这些步骤后,您发现无法启动应用程序,我强烈建议您查看 http://mng.bz/Zqom 上的文档。这包含许多故障排除步骤,以便在 IIS 决定发出嘶嘶声时让您回到正轨。

This section was deliberately tailored to deploying to IIS, as it provides a great segue for developers who are used to deploying legacy ASP.NET apps and want to deploy their first ASP.NET Core app. But that’s not to say that IIS is the only, or best, place to host your application.
本部分专为部署到 IIS 而定制,因为它为习惯于部署旧版 ASP.NET 应用并希望部署其第一个 ASP.NET Core 应用的开发人员提供了一个很好的 segue。但这并不是说 IIS 是托管应用程序的唯一或最佳位置。

In the next section I provide a brief introduction to hosting your app on Linux, behind a reverse proxy like NGINX or Apache. I won’t go into configuration of the reverse proxy itself, but I will provide an overview of things to consider and resources you can use to run your applications on Linux.
在下一节中,我将简要介绍如何在 Linux 上托管您的应用程序,在 NGINX 或 Apache 等反向代理后面。我不会介绍反向代理本身的配置,但我将概述需要考虑的事项以及可用于在 Linux 上运行应用程序的资源。

27.3 Hosting an application in Linux

27.3 在 Linux 中托管应用程序

One of the great new features in ASP.NET Core is the ability to develop and deploy applications cross-platform, whether on Windows, Linux, or macOS. The ability to run on Linux in particular opens the possibility of cheaper deployments to cloud hosting, deploying to small devices like a Raspberry Pi or to Docker containers.
ASP.NET Core 中的一项出色的新功能是能够跨平台开发和部署应用程序,无论是在 Windows、Linux 还是 macOS 上。特别是在 Linux 上运行的能力为以更便宜的价格部署到云托管、部署到 Raspberry Pi 等小型设备或 Docker 容器提供了可能性。

One of the characteristics of Linux is that it’s almost infinitely configurable. Although that’s definitely a feature, it can also be extremely daunting, especially if you’re coming from the Windows world of wizards and GUIs. This section provides an overview of what it takes to run an application on Linux. It focuses on the broad steps you need to take rather than the somewhat-tedious details of the configuration itself. Instead, I point to resources you can refer to as necessary.
Linux 的一个特点是它几乎可以无限配置。虽然这绝对是一个功能,但它也可能非常令人生畏,特别是如果您来自向导和 GUI 的 Windows 世界。本节概述了在 Linux 上运行应用程序所需的条件。它侧重于您需要采取的广泛步骤,而不是配置本身的有点乏味的细节。相反,我指出了您可以根据需要参考的资源。

27.3.1 Running an ASP.NET Core app behind a reverse proxy in Linux

27.3.1 在 Linux 中的反向代理后面运行 ASP.NET Core 应用程序

You’ll be glad to hear that running your application on Linux is broadly the same as running your application on Windows with IIS:
您会很高兴地听到,在 Linux 上运行应用程序与使用 IIS 在 Windows 上运行应用程序大致相同:

  1. Publish your app using dotnet publish. If you’re creating an RDD, the output is the same as you’d use with IIS. For an SCD, you must provide the runtime identifier, as described in section 27.1.1.
    使用 dotnet publish 发布应用。如果要创建 RDD,则输出与用于 IIS 的输出相同。对于 SCD,您必须提供运行时标识符,如第 27.1.1 节中所述。

  2. Install the necessary prerequisites on the server. For an RDD deployment, you must install the .NET 7 Runtime and the necessary prerequisites. You can find details on this in Microsoft’s “Install .NET on Linux” documentation at http://mng.bz/Rxlj.
    在服务器上安装必要的先决条件。对于 RDD 部署,您必须安装 .NET 7 运行时和必要的先决条件。您可以在 http://mng.bz/Rxlj 的 Microsoft 的“在 Linux 上安装 .NET”文档中找到有关此内容的详细信息。

  3. Copy your app to the server. You can use any mechanism you like: FTP, USB stick, or whatever you need to get your files onto the server!
    将您的应用复制到服务器。您可以使用任何您喜欢的机制:FTP、U 盘或任何您需要将文件放到服务器上的机制!

  4. Configure a reverse proxy, and point it to your app. As you know by now, you may want to run your app behind a reverse proxy, for the reasons described in section 27.1. In Windows you’d use IIS, but in Linux you have more options. NGINX, Apache, and HAProxy are commonly used options. The ASP.NET Core-based YARP is also an option (https://microsoft.github.io/reverse-proxy). Alternatively, go without, and expose your app directly to the network.
    配置反向代理,并将其指向您的应用。正如您现在所知,出于第 27.1 节中描述的原因,您可能希望在反向代理后面运行您的应用程序。在 Windows 中,您将使用 IIS,但在 Linux 中,您有更多选择。NGINX、Apache 和 HAProxy 是常用的选项。基于 ASP.NET Core 的 YARP 也是一个选项 (https://microsoft.github.io/reverse-proxy)。或者,不这样做,直接将你的应用公开给网络

  5. Configure a process-management tool for your app. In Windows, IIS acts as both a reverse proxy and a process manager, restarting your app if it crashes or stops responding. In Linux, you typically need to configure a separate process manager to handle these duties; the reverse proxies won’t do them for you.
    为您的应用程序配置进程管理工具。在 Windows 中,IIS 既充当反向代理又充当进程管理器,如果应用程序崩溃或停止响应,则会重新启动应用程序。在 Linux 中,通常需要配置单独的进程管理器来处理这些任务;反向代理不会为您做这些事情。

The first three steps are generally the same, whether you’re running in Windows with IIS or in Linux, but the last two steps are more interesting. By contrast with the monolithic IIS, Linux has a philosophy of small applications, each with a single responsibility.
无论您是在带有 IIS 的 Windows 中运行,还是在 Linux 中运行,前三个步骤通常是相同的,但最后两个步骤更有趣。与整体式 IIS 相比,Linux 的理念是小型应用程序,每个应用程序都有单一的责任。

IIS runs on the same server as your app and takes on multiple duties—proxying traffic from the internet to your app, but also monitoring the app process itself. If your app crashes or stops responding, IIS restarts the process to ensure that you can keep handling requests.
IIS 与您的应用程序运行在同一台服务器上,并承担多项任务 — 将流量从 Internet 代理到您的应用程序,同时还要监控应用程序进程本身。如果您的应用程序崩溃或停止响应,IIS 将重新启动该过程以确保您可以继续处理请求。

In Linux, the reverse proxy might be running on the same server as your app, but it’s also common for it to be running on a different server, as shown in figure 27.9. This is similarly true if you choose to deploy your app to Docker; your app would typically be deployed in a container without a reverse proxy, and a reverse proxy on a server would point to your Docker container.
在 Linux 中,反向代理可能与您的应用程序运行在同一台服务器上,但它在不同的服务器上运行也很常见,如图 27.9 所示。如果您选择将应用程序部署到 Docker,则情况类似;您的应用程序通常会部署在没有反向代理的容器中,而服务器上的反向代理将指向您的 Docker 容器。

alt text

Figure 27.9 In Linux, it’s common for a reverse proxy to be on a different server from your app. The reverse proxy forwards incoming requests to your app, while a process manager, such as systemd, monitors your apps for crashes and restarts it as appropriate.
图 27.9 在 Linux 中,反向代理与你的应用程序位于不同的服务器上是很常见的。反向代理将传入请求转发到您的应用程序,而进程管理器(如 systemd)会监控您的应用程序是否崩溃并根据需要重新启动它。

As the reverse proxies aren’t necessarily on the same server as your app, they can’t be used to restart your app if it crashes. Instead, you need to use a process manager such as systemd to monitor your app. If you’re using Docker, you typically use a container orchestrator such as Kubernetes (https://kubernetes.io) to monitor the health of your containers.
由于反向代理不一定与您的应用程序位于同一服务器上,因此如果应用程序崩溃,它们不能用于重新启动应用程序。相反,您需要使用进程管理器(如 systemd)来监控您的应用程序。如果您使用的是 Docker,则通常使用 Kubernetes (https://kubernetes.io) 等容器编排器来监控容器的运行状况。

Running ASP.NET Core applications in Docker
在 Docker中运行 ASP.NET Core 应用程序

Docker is the most commonly used engine for containerizing your applications. A container is like a small, lightweight virtual machine, specific to your app. It contains an operating system, your app, and any dependencies for your app. This container can then be run on any machine that runs Docker, and your app will run exactly the same, regardless of the host operating system and what’s installed on it. This makes deployments highly repeatable: you can be confident that if the container runs on your machine, it will run on the server too.
Docker 是容器化应用程序最常用的引擎。容器类似于特定于您的应用程序的小型轻量级虚拟机。它包含作系统、应用程序以及应用程序的任何依赖项。然后,此容器可以在任何运行 Docker 的计算机上运行,并且无论主机作系统及其上安装的内容如何,您的应用程序都将以完全相同的方式运行。这使得部署具有高度的可重复性:您可以确信,如果容器在您的计算机上运行,它也将在服务器上运行。

All the major cloud vendors have support for running containers, either standalone or as part of an orchestration service. For example, in Azure, you can run containers in Azure App Service, Azure Container Instances, Azure Container Apps, and Azure Kubernetes Service. One advantage of containers is that you can easily use the same container in all these services or even move to a different cloud provider, and your app will run the same.
所有主要的云供应商都支持运行容器,无论是独立的还是作为编排服务的一部分。例如,在 Azure 中,可以在 Azure 应用服务、Azure 容器实例、Azure 容器应用和 Azure Kubernetes 服务中运行容器。容器的一个优点是,您可以轻松地在所有这些服务中使用相同的容器,甚至可以迁移到不同的云提供商,并且您的应用程序将以相同的方式运行。

ASP.NET Core is well suited to container deployments, but moving to Docker involves a big shift in your deployment methodology and may or may not be right for you and your apps. If you’re interested in the possibilities afforded by Docker and want to learn more, I suggest checking out the following resources:
ASP.NET Core 非常适合容器部署,但迁移到 Docker 涉及部署方法的重大转变,并且可能适合也可能不适合您和您的应用程序。如果您对 Docker 提供的可能性感兴趣并想了解更多信息,我建议您查看以下资源:

• Docker in Practice, 2nd ed., by Ian Miell and Aidan Hobson Sayers (Manning, 2019) provides a vast array of practical techniques to help you get the most out of Docker (http://mng.bz/nM8d).
Docker in Practice,第 2 版,由 Ian Miell 和 Aidan Hobson Sayers 编写(Manning,2019 年),提供了大量实用技术来帮助您充分利用 Docker (http://mng.bz/nM8d)。

• Even if you’re not deploying to Linux, you can use Docker with Docker for Windows. Check out the free e-book Introduction to Windows Containers, by John McCabe and Michael Friis (Microsoft Press, 2017), at https://aka.ms/containersebook.
即使您没有部署到 Linux,也可以将 Docker 与适用于 Windows 的 Docker 结合使用。查看 John McCabe 和 Michael Friis 合著的免费电子书 Introduction to Windows Containers(Microsoft出版社,2017 年),网址为 https://aka.ms/containersebook

• You can find a lot of details on building and running your ASP.NET Core applications on Docker in the .NET documentation at http://mng.bz/vz5a.
您可以在 http://mng.bz/vz5a 的 .NET 文档中找到有关在 Docker 上构建和运行 ASP.NET Core 应用程序的许多详细信息。

• Steve Gordon has an excellent blog post series on Docker for ASP.NET Core developers at http://mng.bz/2Da8.
Steve Gordon 在 http://mng.bz/2Da8 上为 ASP.NET Core 开发人员撰写了一篇关于 Docker 的优秀博客文章系列。

Configuring a reverse proxy and process manager on Linux is a laborious task that makes for dry reading, so I won’t detail it here. Instead, I recommend checking out the ASP.NET Core docs. They have a guide for NGINX and systemd, “Host ASP.NET Core on Linux with Nginx” (http://mng.bz/yYGd), and a guide for configuring Apache with systemd, “Host ASP.NET Core on Linux with Apache” (http://mng.bz/MXVB).
在 Linux 上配置反向代理和进程管理器是一项费力的任务,因此会枯燥阅读,因此我不会在这里详细说明。相反,我建议查看 ASP.NET Core 文档。他们有 NGINX 和 systemd 指南“使用 Nginx 在 Linux 上托管 ASP.NET Core”(http://mng.bz/yYGd),以及使用 systemd 配置 Apache 的指南“使用 Apache 在 Linux 上托管 ASP.NET Core”(http://mng.bz/MXVB)。

Both guides cover the basic configuration of the respective reverse proxies and systemd supervisors, but more important, they also show how to configure them securely. The reverse proxy sits between your app and the unfettered internet, so it’s important to get it right!
这两个指南都涵盖了相应的反向代理和 systemd 监控器的基本配置,但更重要的是,它们还展示了如何安全地配置它们。反向代理位于您的应用程序和不受限制的互联网之间,因此正确使用很重要!

Configuring the reverse proxy and the process manager is typically the most complex part of deploying to Linux, and that isn’t specific to .NET development: the same would be true if you were deploying a Node.js web app. But you need to consider a few things inside your application when you’re going to be deploying to Linux, as you’ll see in the next section.
配置反向代理和进程管理器通常是部署到 Linux 的最复杂的部分,这并非特定于 .NET 开发:如果要部署 Node.js Web 应用程序,情况也是如此。但是,当您要部署到 Linux 时,您需要在应用程序中考虑一些事项,您将在下一节中看到。

27.3.2 Preparing your app for deployment to Linux

27.3.2 准备将应用程序部署到 Linux

Generally speaking, your app doesn’t care which reverse proxy it sits behind, whether it’s NGINX, Apache, or IIS; your app receives requests and responds to them without the reverse proxy affecting things. When you’re hosting behind IIS, you need UseIISIntegration() to tell your app about IIS’s configuration; when you’re hosting on Linux, you need a similar method.
一般来说,你的应用并不关心它位于哪个反向代理后面,无论是 NGINX、Apache 还是 IIS;你的应用接收请求并响应这些请求,而反向代理不会影响事情。当您在 IIS 后面托管时,您需要 UseIISIntegration() 来告知您的应用程序 IIS 的配置;当您在 Linux 上托管时,您需要类似的方法。

When a request arrives at the reverse proxy, it contains some information that is lost after the request is forwarded to your app. For example, the original request comes with the IP address of the client/browser connecting to your app; once the request is forwarded from the reverse proxy, the IP address is that of the reverse proxy, not the browser. Also, if the reverse proxy is used for SSL/TLS offloading (see chapter 28), then a request that was originally made using HTTPS may arrive at your app as an HTTP request.
当请求到达反向代理时,它包含一些在请求转发到您的应用后丢失的信息。例如,原始请求附带连接到您的应用程序的客户端/浏览器的 IP 地址;从反向代理转发请求后,IP 地址是反向代理的 IP 地址,而不是浏览器的 IP 地址。此外,如果反向代理用于 SSL/TLS 卸载(参见第 28 章),那么最初使用 HTTPS 发出的请求可能会作为 HTTP 请求到达您的应用程序。

The standard solution to these problems is for the reverse proxy to add more headers before forwarding requests to your app. For example, the X-Forwarded-For header identifies the original client’s IP address, whereas the X-Forwarded-Proto header indicates the original scheme of the request (http or https).
这些问题的标准解决方案是让反向代理在将请求转发到您的应用程序之前添加更多标头。例如,X-Forwarded-For 标头标识原始客户端的 IP 地址,而 X-Forwarded-Proto 标头指示请求的原始方案(http 或 https)。

For your app to behave correctly, it needs to look for these headers in incoming requests and modify the request as appropriate. A request to http://localhost with the X-Forwarded-Proto header set to https should be treated the same as if the request were to https://localhost.
为了使您的应用程序正常运行,它需要在传入请求中查找这些标头并根据需要修改请求。如果 X-Forwarded-Proto 标头设置为 https http://localhost,则请求的处理方式应与请求 https://localhost 相同。

You can use ForwardedHeadersMiddleware in your middleware pipeline to achieve this. This middleware overrides Request.Scheme and other properties on HttpContext to correspond to the forwarded headers. WebApplicationBuilder partially handles this for you; the middleware is automatically added to the pipeline in a disabled state. To enable it, set the environment variable ASPNETCORE_FORWARDEDHEADERS_ENABLED=true.
您可以在中间件管道中使用 ForwardedHeadersMiddleware 来实现此目的。此中间件将覆盖 HttpContext 上的 Request.Scheme 和其他属性,以对应于转发的标头。WebApplicationBuilder 为您部分处理此问题;中间件会自动以 Disabled 状态添加到管道中。要启用它,请将环境变量设置为 ASPNETCORE_FORWARDEDHEADERS_ENABLED=true。

If you don’t want to use the automatically added middleware for some reason, or if you’re using the generic host (which you’ll learn about in chapter 30), you can add the middleware to the start of your middleware pipeline manually, as shown in listing 27.1, and configure it with the headers to look for.
如果由于某种原因不想使用自动添加的中间件,或者正在使用通用主机(您将在第 30 章中学习),则可以手动将中间件添加到中间件管道的开头,如清单 27.1 所示,并使用要查找的标头对其进行配置。

WARNING It’s important that ForwardedHeadersMiddleware be placed early in the middleware pipeline to correct Request.Scheme before any middleware that depends on the scheme runs.
警告:请务必将 ForwardedHeadersMiddleware 放在中间件管道的早期,以便在任何依赖于 scheme 的中间件运行之前更正 Request.Scheme。

Listing 27.1 Configuring an app to use forwarded headers in Startup.cs
清单 27.1 配置应用程序以在 Startup.cs 中使用转发的 Headers

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
WebApplication app = builder.Build();

app.UseForwardedHeaders(new ForwardedHeadersOptions      #A
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor |     #B
                       ForwardedHeaders.XForwardedProto     #B
});
app.UseHttpsRedirection();    #C
app.UseRouting();             #C
app.MapGet("/", () => "Hello world!");
app.Run();

❶ Adds ForwardedHeadersMiddleware early in your pipeline
在管道的早期添加 ForwardedHeadersMiddleware
❷ Configures the headers the middleware should look for and use
配置中间件应该查找和使用的标头
❸ The forwarded headers middleware must be placed before all other middleware.
转发的标头中间件必须放在所有其他中间件之前。

NOTE This behavior isn’t specific to reverse proxies on Linux; the UseIis() extension adds ForwardedHeadersMiddleware under the hood as part of its configuration when your app is running behind IIS.
注意:此行为并非特定于 Linux 上的反向代理;当应用在 IIS 后面运行时,UseIis() 扩展会在后台添加 ForwardedHeadersMiddleware 作为其配置的一部分。

Aside from considering the forwarded headers, you need to consider a few minor things when deploying your app to Linux that may trip you up if you’re used to deploying to Windows alone:
除了考虑转发的标头之外,在将应用程序部署到 Linux 时,还需要考虑一些小事项,如果您习惯于单独部署到 Windows,这些事项可能会让您感到困惑:

• Line endings (LF in Linux versus CRLF in Windows)—Windows and Linux use different character codes in text to indicate the end of a line. This isn’t often a problem for ASP.NET Core apps, but if you’re writing text files on one platform and reading them on a different platform, it’s something to bear in mind.
行尾 (Linux 中的 LF 与 Windows 中的 CRLF) - Windows 和 Linux 在文本中使用不同的字符代码来指示行尾。对于 ASP.NET Core 应用程序来说,这通常不是问题,但如果您在一个平台上编写文本文件并在另一个平台上阅读它们,则需要记住这一点。

• Path directory separator ("\" on Windows, "/" on Linux)—This is one of the most common bugs I see when Windows developers move to Linux. Each platform uses a different separator in file paths, so although loading a file using the "subdir\myfile.json" path will work fine in Windows, it won’t in Linux. Instead, you should use Path.Combine to create the appropriate separator for the current platform, such as Path.Combine("subdir", "myfile.json").
路径目录分隔符 (Windows 上为 “\”,Linux 上为 “/”) - 这是我在 Windows 开发人员迁移到 Linux 时看到的最常见的错误之一。每个平台在文件路径中使用不同的分隔符,因此尽管使用 “subdir\myfile.json” 路径加载文件在 Windows 中可以正常工作,但在 Linux 中则无法。相反,您应该使用 Path.Combine 为当前平台创建适当的分隔符,例如 Path.Combine(“subdir”, “myfile.json”)。

• ":" in environment variables—In some Linux distributions, the colon character (:) isn’t allowed in environment variables. As you saw in chapter 10, this character is typically used to denote different sections in ASP.NET Core configuration, so you often need to use it in environment variables. Instead, you can use a double underscore in your environment variables (); ASP.NET Core will treat it the same as though you’d used a colon.
环境变量中的 “:” - 在某些 Linux 发行版中,环境变量中不允许使用冒号字符 (:)。正如你在第 10 章中看到的,这个字符通常用于表示 ASP.NET Core 配置中的不同部分,因此你经常需要在环境变量中使用它。相反,您可以在环境变量 (
) 中使用双下划线;ASP.NET Core 会像使用冒号一样对待它。

• Missing time zone and culture data—Linux distributions don’t always come with time zone or culture data, which can cause localization problems and exceptions at runtime. You can install the time zone data using your distribution’s package manager.[1] It also may be organized differently. The hierarchy of Norwegian cultures is different in Linux, for example.
缺少时区和文化数据 - Linux 发行版并不总是附带时区或文化数据,这可能会导致运行时出现本地化问题和异常。您可以使用分配的软件包管理器安装时区数据。[1] 它的组织方式也可能不同。例如,挪威文化的层次结构在 Linux 中是不同的。

• Different directory structures—Linux distributions use a different folder structure from Windows, so you need to bear that in mind if your app hardcodes paths. In particular, consider differences in temporary/cache folders.
不同的目录结构 - Linux 发行版使用与 Windows 不同的文件夹结构,因此,如果您的应用程序对路径进行硬编码,则需要记住这一点。特别是,请考虑临时/缓存文件夹的差异。

The preceding list is not exhaustive by any means, but as long as you set up ForwardedHeadersMiddleware and take care to use cross-platform constructs like Path.Combine, you shouldn’t have too many problems running your applications on Linux. But configuring a reverse proxy isn’t the simplest of activities, so wherever you’re planning on hosting your app, I suggest checking the documentation for guidance at http://mng.bz/1qM1.
前面的列表无论如何都不是详尽无遗的,但只要您设置了 ForwardedHeadersMiddleware 并注意使用像 Path.Combine 这样的跨平台结构,在 Linux 上运行应用程序应该不会有太多问题。但是配置反向代理并不是最简单的活动,因此无论您打算在哪里托管您的应用程序,我建议您查看 http://mng.bz/1qM1 上的文档以获取指导。

27.4 Configuring the URLs for your application

27.4 为应用程序配置 URL

At this point, you’ve deployed an application, but there’s one aspect you haven’t configured: the URLs for your application. When you’re using IIS as a reverse proxy, you don’t have to worry about this inside your app. IIS integration with the ASP.NET Core Module works by dynamically creating a URL that’s used to forward requests between IIS and your app. The hostname you configure in IIS (in figure 27.6) is the URL that external users see for your app; the internal URL that IIS uses when forwarding requests is never exposed.
此时,您已经部署了一个应用程序,但有一个方面尚未配置:应用程序的 URL。当您使用 IIS 作为反向代理时,您不必担心应用程序内部的这个问题。IIS 与 ASP.NET 核心模块的集成的工作原理是动态创建一个 URL,该 URL 用于在 IIS 和您的应用程序之间转发请求。您在 IIS 中配置的主机名(如图 27.6 所示)是外部用户看到的应用程序的 URL;IIS 在转发请求时使用的内部 URL 永远不会公开。

If you’re not using IIS as a reverse proxy—maybe you’re using NGINX or exposing your app directly to the internet—you may find you need to configure the URLs your application listens to directly.
如果您没有将 IIS 用作反向代理(也许您使用的是 NGINX 或直接向 Internet 公开您的应用程序),您可能会发现您需要配置应用程序直接侦听的 URL。

By default, ASP.NET Core listens for requests on the URL http://localhost:5000. There are lots of ways to set this URL, but in this section I describe two: using environment variables or using command-line arguments. These are the two most common approaches I see (outside of IIS) for controlling which URLs your app uses.
默认情况下,ASP.NET Core 侦听 URL http://localhost:5000 上的请求。设置此 URL 的方法有很多种,但在本节中,我将介绍两种方法:使用环境变量或使用命令行参数。这是我看到的两种最常见的方法(在 IIS 之外),用于控制您的应用程序使用的 URL。

TIP For further ways to set your application’s URL, see my “5 ways to set the URLs for an ASP.NET Core app” blog post: http://mng.bz/go0v.
提示:有关设置应用程序 URL 的更多方法,请参阅我的“为 ASP.NET Core 应用程序设置 URL 的 5 种方法”博客文章:http://mng.bz/go0v

In chapter 10 you learned about configuration in ASP.NET Core, and in particular about the concept of hosting environments so that you can use different settings when running in development compared with production. You choose the hosting environment by setting an environment variable on your machine called ASPNETCORE_ENVIRONMENT. The ASP.NET Core framework magically picks up this variable when your app starts and uses it to set the hosting environment.
在第 10 章中,您了解了 ASP.NET Core 中的配置,特别是托管环境的概念,以便在开发环境中运行时与在生产环境中运行时可以使用不同的设置。您可以通过在计算机上设置名为 ASPNETCORE_ENVIRONMENT 的环境变量来选择托管环境。ASP.NET Core 框架会在您的应用启动时神奇地选取此变量,并使用它来设置托管环境。

You can use a similar special environment variable to specify the URL that your app uses; this variable is called ASPNETCORE_URLS. When your app starts up, it looks for this value and uses it as the application’s URL. By changing this value, you can change the default URL used by all ASP.NET Core apps on the machine. For example, you could set a temporary environment variable in Windows from the command window using
您可以使用类似的特殊环境变量来指定您的应用程序使用的 URL;此变量称为 ASPNETCORE_URLS。当您的应用程序启动时,它会查找此值并将其用作应用程序的 URL。通过更改此值,您可以更改计算机上所有 ASP.NET Core 应用程序使用的默认 URL。例如,您可以在命令窗口中使用

set ASPNETCORE_URLS=http://localhost:8000

Running a published application using dotnet <app.dll> within the same command window, as shown in figure 27.10, shows that the app is now listening on the URL provided in the ASPNETCORE_URLS variable.
如图 27.10 所示,在同一命令窗口中使用 dotnet <app.dll> 运行已发布的应用程序,显示该应用程序现在正在侦听 ASPNETCORE_URLS 变量中提供的 URL。

alt text
Figure 27.10 Change the ASPNETCORE_URLS environment variable to change the URL used by ASP.NET Core apps.
图 27.10 更改 ASPNETCORE_URLS 环境变量以更改 ASP.NET Core 应用程序使用的 URL。

You can instruct an app to listen on multiple URLs by separating them with a semicolon, or you can listen to a specific port without specifying the localhost hostname. If you set the ASPNETCORE_URLS environment variable to
您可以通过用分号分隔多个 URL 来指示应用程序侦听这些 URL,也可以在不指定 localhost 主机名的情况下侦听特定端口。如果将 ASPNETCORE_URLS 环境变量设置为
http://localhost:5001;http://*:5002

your ASP.NET Core apps will listen for requests sent to the following:
您的 ASP.NET Core 应用程序将侦听发送到以下各项的请求:

http://localhost:5001 — This address is accessible only on your local computer, so it will not accept requests from the wider internet.
http://localhost:5001 - 此地址只能在您的本地计算机上访问,因此它不会接受来自更广泛 Internet 的请求。

http://*:5002—Any URL on port 5002. External requests from the internet can access the app on port 5002, using any URL that maps to your computer.
• http://*:5002 - 端口 5002 上的任何 URL。来自 Internet 的外部请求可以使用映射到您计算机的任何 URL 访问端口 5002 上的应用程序。

Note that you can’t specify a different hostname, like tastyrecipes.com. ASP.NET Core listens to all requests on a given port; it doesn’t listen for specific domain names. The exception is the localhost hostname, which allows only requests that came from your own computer.
请注意,您不能指定其他主机名,例如 tastyrecipes.com。ASP.NET Core 侦听给定端口上的所有请求;它不侦听特定的域名。localhost 主机名例外,它只允许来自您自己的计算机的请求。

NOTE If you find the ASPNETCORE_URLS variable isn’t working properly, ensure that you don’t have a launchSettings.json file in the directory. When present, the values in this file take precedence. By default, launchSettings.json isn’t included in the publish output, so this generally won’t be a problem in production.
注意:如果您发现 ASPNETCORE_URLS 变量无法正常工作,请确保目录中没有 launchSettings.json 文件。如果存在,则此文件中的值优先。默认情况下,launchSettings.json 不包含在发布输出中,因此这在生产中通常不会成为问题。

Setting the URL of an app using a single environment variable works great for some scenarios, most notably when you’re running a single application in a virtual machine, or within a Docker container.
使用单个环境变量设置应用程序的 URL 适用于某些场景,尤其是当您在虚拟机或 Docker 容器中运行单个应用程序时。

TIP ASP.NET Core is well suited to running in containers but working with containers is a separate book in its own right. For details on hosting and publishing apps using Docker, see Microsoft’s “Host ASP.NET Core in Docker containers” documentation: http://mng.bz/e5GV.
提示: ASP.NET Core 非常适合在容器中运行,但使用容器本身是一本单独的书。有关使用 Docker 托管和发布应用程序的详细信息,请参阅 Microsoft 的“在 Docker 容器中托管 ASP.NET Core”文档:http://mng.bz/e5GV

If you’re not using Docker containers or a PaaS offering, chances are that you’re hosting multiple apps side-by-side on the same machine. A single environment variable is no good for setting URLs in this case, as it would change the URL of every app.
如果您没有使用 Docker 容器或 PaaS 产品,则很可能在同一台计算机上并排托管多个应用程序。在这种情况下,单个环境变量不适合设置 URL,因为它会更改每个应用程序的 URL。

In chapter 10 you saw that you could set the hosting environment using the ASPNETCORE_ENVIRONMENT variable, but you could also set the environment using the --environment flag when calling dotnet run:
在第 10 章中,您看到可以使用 ASPNETCORE_ENVIRONMENT 变量设置托管环境,但也可以在调用 dotnet run 时使用 --environment 标志设置环境:

dotnet run --no-launch-profile --environment Staging

You can set the URLs for your application in a similar way, using the --urls parameter. Using command-line arguments enables you to have multiple ASP.NET Core applications running on the same machine, listening to different ports. For example, the following command would run the recipe application, set it to listen on port 8081, and set the environment to staging (figure 27.11):
您可以使用 --urls 参数以类似的方式为应用程序设置 URL。使用命令行参数,您可以在同一台计算机上运行多个 ASP.NET Core 应用程序,侦听不同的端口。例如,以下命令将运行 recipe 应用程序,将其设置为侦听端口 8081,并将环境设置为 staging (图 27.11):

dotnet RecipeApplication.dll --urls "http://*:8081" --environment Staging

alt text

Figure 27.11 Setting the hosting environment and URLs for an application using command-line arguments. The values passed at the command line override values provided from appSettings.json or environment variables.
图 27.11 使用命令行参数设置应用程序的托管环境和 URL。在命令行中传递的值将覆盖appSettings.json或环境变量提供的值。

Remember that you don’t need to set your URLs in this way if you’re using IIS as a reverse proxy; IIS integration handles this for you. Setting the URLs is necessary only when you’re manually configuring the URL your app is listening on, such as if you’re using NGINX or are exposing Kestrel directly to clients.
请记住,如果您使用 IIS 作为反向代理,则无需以这种方式设置 URL;IIS 集成会为您处理此问题。只有当您手动配置应用程序正在侦听的 URL 时(例如,如果您使用的是 NGINX 或将 Kestrel 直接公开给客户端),才需要设置 URL。

WARNING If you are running your ASP.NET Core application without a reverse proxy, you should use host filtering for security reasons to ensure that your app only responds to requests for hostnames you expect. For more details, see my “Adding host filtering to Kestrel in ASP.NET Core” blog entry: http://mng.bz/pVXK.
警告:如果您在没有反向代理的情况下运行 ASP.NET Core 应用程序,则出于安全原因,您应该使用主机筛选,以确保您的应用程序仅响应您期望的主机名请求。有关更多详细信息,请参阅我的“在 ASP.NET Core 中向 Kestrel 添加主机筛选”博客文章:http://mng.bz/pVXK

That brings us to the end of this chapter on publishing your app. This last mile of app development—deploying an application to a server where users can access it—is a notoriously thorny problem. Publishing an ASP.NET Core application is easy enough, but the multitude of hosting options available makes providing concise steps for every situation difficult.
到此,我们来到了本章关于发布您的应用的结尾。众所周知,应用程序开发的最后一英里 — 将应用程序部署到用户可以访问它的服务器上 — 是一个非常棘手的问题。发布 ASP.NET Core 应用程序非常简单,但可用的大量托管选项使得为每种情况提供简洁的步骤变得困难。

Whichever hosting option you choose, there’s one critical topic that you mustn’t overlook: security. In the next chapter you’ll learn about HTTPS, how to use it when testing locally, and why it’s important your production apps all use HTTPS.
无论您选择哪种托管选项,都有一个关键主题是您不容忽视的:安全性。在下一章中,您将了解 HTTPS,在本地测试时如何使用 HTTPS,以及为什么您的生产应用程序都使用 HTTPS 很重要。

27.5 Summary

27.5 总结

ASP.NET Core apps are console applications that self-host a web server. In production, you may use a reverse proxy, which handles the initial request and passes it to your app. Reverse proxies can provide additional security, operations, and performance benefits, but they can also add complexity to your deployments.
ASP.NET Core 应用程序是自托管 Web 服务器的控制台应用程序。在生产环境中,您可以使用反向代理来处理初始请求并将其传递给您的应用程序。反向代理可以提供额外的安全性、作和性能优势,但它们也会增加部署的复杂性。

.NET has two parts: the .NET SDK (also known as the .NET CLI) and the .NET Runtime. When you’re developing an application, you use the .NET CLI to restore, build, and run your application. Visual Studio uses the same .NET CLI commands from the IDE.
.NET 有两个部分:.NET SDK(也称为 .NET CLI)和 .NET 运行时。在开发应用程序时,您可以使用 .NET CLI 来还原、构建和运行应用程序。Visual Studio 使用与 IDE 相同的 .NET CLI 命令。

When you want to deploy your app to production, you need to publish your application, using dotnet publish. This creates a folder containing your application as a DLL, along with all its dependencies.
如果要将应用部署到生产环境,则需要使用 dotnet publish 发布应用程序。这将创建一个文件夹,其中包含作为 DLL 的应用程序及其所有依赖项。

To run a published application, you don’t need the .NET CLI because you won’t be building the app. You need only the .NET Runtime to run a published app. You can run a published application using the dotnet app.dll command, where app.dll is the application .dll created by the dotnet publish command.
要运行已发布的应用程序,您不需要 .NET CLI,因为您不会构建应用程序。只需 .NET 运行时即可运行已发布的应用程序。可以使用 dotnet app.dll 命令运行已发布的应用程序,其中 app.dll 是由 dotnet publish 命令创建的应用程序.dll。

To host ASP.NET Core applications in IIS, you must install the ASP.NET Core Module. This allows IIS to act as a reverse proxy for your ASP.NET Core app. You must also install the .NET Runtime and the ASP.NET Core Runtime, which are installed as part of the ASP.NET Core Windows Hosting Bundle.
要在 IIS 中托管 ASP.NET Core 应用程序,必须安装 ASP.NET Core Module。这允许 IIS 充当 ASP.NET Core 应用程序的反向代理。您还必须安装 .NET 运行时和 ASP.NET Core 运行时,它们作为 ASP.NET Core Windows 托管捆绑包的一部分安装。

IIS can host ASP.NET Core applications using one of two modes: in-process and out-of-process. The out-of-process mode runs your application as a separate process, as is typical for most reverse proxies. The in-process mode runs your application as part of the IIS process. This has performance benefits, as no interprocess communication is required.
IIS 可以使用以下两种模式之一托管 ASP.NET Core 应用程序:进程内和进程外。进程外模式将您的应用程序作为单独的进程运行,这是大多数反向代理的典型特征。进程内模式将应用程序作为 IIS 进程的一部分运行。这具有性能优势,因为不需要进程间通信。

If you are using a custom web application builder with IIS, ensure that you call UseIISIntegration() and UseIIS() so that IIS forwards the request to your app correctly. If you’re using the default WebApplicationBuilder, these methods are called automatically for you.
如果将自定义 Web 应用程序生成器与 IIS 一起使用,请确保调用 UseIISIntegration() 和 UseIIS(),以便 IIS 将请求正确转发到应用。如果您使用的是默认的 WebApplicationBuilder,则会自动为您调用这些方法。

When you publish your application using the .NET CLI, a web.config file is added to the output folder. It’s important that this file be deployed with your application when publishing to IIS, as it defines how your application should run.
当您使用 .NET CLI 发布应用程序时,web.config 文件将添加到输出文件夹中。在发布到 IIS 时,此文件必须与应用程序一起部署,因为它定义了应用程序的运行方式。

The URL that your app listens on is specified by default using the environment variable ASPNETCORE_URLS. Setting this value changes the URL for all the apps on your machine. Alternatively, pass the --urls command-line argument when running your app, as in this example: dotnet app.dll --urls http://localhost:80.
默认情况下,您的应用程序侦听的 URL 是使用环境变量 ASPNETCORE_URLS 指定的。设置此值会更改计算机上所有应用程序的 URL。或者,在运行应用时传递 --urls 命令行参数,如以下示例所示:dotnet app.dll --urls http://localhost:80

[1] I ran into this problem myself. You can read about it in detail and how I solved it on my blog: http://mng.bz/aoem.
[1] 我自己也遇到过这个问题。您可以在我的博客上详细阅读它以及我是如何解决的:http://mng.bz/aoem

ASP.NET Core in Action 26 Monitoring and troubleshooting errors with logging

26 Monitoring and troubleshooting errors with logging
26 使用日志记录监控和排除错误

This chapter covers
本章涵盖

• Understanding the components of a log message
了解日志消息的组成部分

• Writing logs to multiple output locations
将日志写入多个输出位置

• Controlling log verbosity in different environments using filtering
使用筛选控制不同环境中的日志详细程度

• Using structured logging to make logs searchable
使用结构化日志记录使日志可搜索

Logging is one of those topics that seems unnecessary, right up until you desperately need it! There’s nothing more frustrating than finding a problem that you can reproduce only in production and then discovering there are no logs to help you debug it.
日志记录是那些似乎不必要的主题之一,直到您迫切需要它为止!没有什么比找到只能在生产环境中重现的问题,然后发现没有日志可以帮助您调试它更令人沮丧的了。

Logging is the process of recording events or activities in an app, and it often involves writing a record to a console, a file, the Windows Event Log, or some other system. You can record anything in a log message, though there are generally two different types of messages:
日志记录是在应用程序中记录事件或活动的过程,它通常涉及将记录写入控制台、文件、Windows 事件日志或其他系统。您可以在日志消息中记录任何内容,但通常有两种不同类型的消息:

• Informational messages—A standard event occurred: a user logged in, a product was placed in a shopping cart, or a new post was created on a blogging app.
信息性消息 - 发生标准事件:用户登录、产品放入购物车或在博客应用程序上创建新帖子。

• Warnings and errors—An error or unexpected condition occurred: a user had a negative total in the shopping cart, or an exception occurred.
警告和错误 - 发生错误或意外情况:用户购物车中的总数为负数,或发生异常。

Historically, a common problem with logging in larger applications was that each library and framework would generate logs in a slightly different format, if at all. When an error occurred in your app and you were trying to diagnose it, this inconsistency made it harder to connect the dots in your app to get the full picture and understand the problem.
从历史上看,在大型应用程序中登录的一个常见问题是,每个库和框架都会以略有不同的格式生成日志(如果有的话)。当您的应用程序发生错误并且您尝试诊断它时,这种不一致使得您更难将应用程序中的各个点连接起来以获得完整的图片并了解问题。

Luckily, ASP.NET Core includes a new generic logging interface that you can plug into. It’s used throughout the ASP.NET Core framework code itself, as well as by third-party libraries, and you can easily use it to create logs in your own code. With the ASP.NET Core logging framework, you can control the verbosity of logs coming from each part of your code, including the framework and libraries, and you can write the log output to any destination that plugs into the framework.
幸运的是,ASP.NET Core 包含一个可以插入的新通用日志记录接口。它在整个 ASP.NET Core 框架代码本身以及第三方库中使用,您可以轻松地使用它在自己的代码中创建日志。使用 ASP.NET Core 日志记录框架,您可以控制来自代码每个部分(包括框架和库)的日志的详细程度,并且可以将日志输出写入插入框架的任何目标。

In this chapter I cover the .NET logging framework ASP.NET Core uses in detail, and I explain how you can use it to record events and diagnose errors in your own apps. In section 26.1 I’ll describe the architecture of the logging framework. You’ll learn how dependency injection (DI) makes it easy for both libraries and apps to create log messages, as well as to write those logs to multiple destinations.
在本章中,我将详细介绍 Core 使用的 .NET 日志记录框架 ASP.NET 并说明如何使用它来记录事件和诊断您自己的应用程序中的错误。在 Section 26.1 中,我将描述 logging 框架的架构。您将了解依赖关系注入 (DI) 如何使库和应用程序轻松创建日志消息,以及将这些日志写入多个目标。

In section 26.2 you’ll learn how to write your own log messages in your apps with the ILogger interface. We’ll break down the anatomy of a typical log record and look at its properties, such as the log level, category, and message.
在第 26.2 节中,您将学习如何使用 ILogger 接口在应用程序中编写自己的日志消息。我们将分解典型日志记录的剖析,并查看其属性,例如日志级别、类别和消息。

Writing logs is useful only if you can read them, so in section 26.3 you’ll learn how to add logging providers to your application. Logging providers control where your app writes your log messages, such as to the console, to a file, or even to an external service.
只有当您可以阅读日志时,编写日志才有用,因此在 Section 26.3 中,您将学习如何将日志记录提供程序添加到您的应用程序中。日志记录提供程序控制应用程序将日志消息写入何处,例如写入控制台、文件甚至外部服务。

Logging is an important part of any application, but determining how much logging is enough can be a tricky question. On one hand, you want to provide sufficient information to be able to diagnose any problems. On the other hand, you don’t want to fill your logs with data that makes it hard to find the important information when you need it. Even worse, what is sufficient in development might be far too much once you’re running in production.
日志记录是任何应用程序的重要组成部分,但确定多少日志记录就足够了可能是一个棘手的问题。一方面,您希望提供足够的信息以便能够诊断任何问题。另一方面,您不希望在日志中填充数据,从而在需要时难以找到重要信息。更糟糕的是,一旦你在生产环境中运行,开发中足够的资源可能就太多了。

In section 26.4 I’ll explain how you can filter log messages from various sections of your app, such as the ASP.NET Core infrastructure libraries, so that your logging providers write only the important messages. This lets you keep that balance between extensive logging in development and writing only important logs in production.
在 Section 26.4 中,我将解释如何过滤来自应用程序各个部分的日志消息,例如 ASP.NET Core 基础设施库,以便日志记录提供程序仅写入重要消息。这使您可以在 development 中的大量日志记录和在 production 中仅写入重要日志之间保持平衡。

In the final section of this chapter I’ll touch on some of the benefits of structured logging, an approach to logging that you can use with some providers for the ASP.NET Core logging framework. Structured logging involves attaching data to log messages as key-value pairs to make it easier to search and query logs. You might attach a unique customer ID to every log message generated by your app, for example. Finding all the log messages associated with a user is much simpler with this approach, compared with recording the customer ID in an inconsistent manner as part of the log message.
在本章的最后一节中,我将介绍结构化日志记录的一些好处,结构化日志记录是一种日志记录方法,您可以将其与 ASP.NET Core 日志记录框架的某些提供程序一起使用。结构化日志记录涉及将数据作为键值对附加到日志消息中,以便更轻松地搜索和查询日志。例如,您可以将唯一的客户 ID 附加到应用程序生成的每条日志消息中。与以不一致的方式将客户 ID 记录为日志消息的一部分相比,使用此方法查找与用户关联的所有日志消息要简单得多。

We’ll start this chapter by digging into what logging involves and why your future self will thank you for using logging effectively in your application. Then we’ll look at the pieces of the ASP.NET Core logging framework you’ll use directly in your apps and how they fit together.
在本章开始时,我们将深入研究日志记录涉及什么,以及为什么未来的自己会感谢您在应用程序中有效地使用日志记录。然后,我们将了解您将直接在应用程序中使用的 ASP.NET Core 日志记录框架的各个部分,以及它们如何组合在一起。

26.1 Using logging effectively in a production app

26.1 在 生产应用程序中有效地使用日志记录

Imagine you’ve just deployed a new app to production when a customer calls saying that they’re getting an error message using your app. How would you identify what caused the problem? You could ask the customer what steps they were taking and potentially try to re-create the error yourself, but if that doesn’t work, you’re left trawling through the code, trying to spot errors with nothing else to go on.
假设您刚刚将一个新应用程序部署到生产环境中,这时客户打电话说他们在使用您的应用时收到了一条错误消息。您将如何确定导致问题的原因?您可以询问客户他们正在采取哪些步骤,并可能尝试自己重新创建错误,但如果这不起作用,您就只能浏览代码,试图发现错误,而没有其他事情可做。

Logging can provide the extra context you need to quickly diagnose a problem. Arguably, the most important logs capture the details about the error itself, but the events that led to the error can be equally useful in diagnosing the cause of an error.
日志记录可以提供快速诊断问题所需的额外上下文。可以说,最重要的日志捕获了有关错误本身的详细信息,但导致错误的事件在诊断错误原因方面同样有用。

There are many reasons for adding logging to an application, but typically, the reasons fall into one of three categories:
向应用程序添加日志记录的原因有很多,但通常,原因分为以下三类之一:

• Logging for auditing or analytics reasons, to trace when events have occurred
出于审核或分析原因进行日志记录,以跟踪事件发生的时间

• Logging errors
记录错误

• Logging nonerror events to provide a breadcrumb trail of events when an error does occur
记录非错误事件,以便在发生错误时提供事件的痕迹导航跟踪

The first of these reasons is simple. You may be required to keep a record of every time a user logs in, for example, or you may want to keep track of how many times a particular API method is called. Logging is an easy way to record the behavior of your app by writing a message to the log every time an interesting event occurs.
第一个原因很简单。例如,您可能需要保留用户每次登录的记录,或者您可能希望跟踪特定 API 方法被调用的次数。日志记录是一种记录应用程序行为的简单方法,每次发生有趣的事件时,都会向日志写入一条消息。

I find the second reason for logging to be the most common. When an app is working perfectly, logs often go completely untouched. It’s when there’s a problem and a customer comes calling that logs become invaluable. A good set of logs can help you understand the conditions in your app that caused an error, including the context of the error itself, but also the context in previous requests.
我发现日志记录的第二个原因是最常见的。当应用程序完美运行时,日志通常完全保持不变。当出现问题并且客户打电话时,日志就会变得非常宝贵。一组好的日志可以帮助您了解应用中导致错误的条件,包括错误本身的上下文,以及之前请求中的上下文。

TIP Even with extensive logging in place, you may not realize you have a problem in your app unless you look through your logs regularly. For any medium-size to large app, this becomes impractical, so monitoring services such as Sentry (https://sentry.io) can be invaluable for notifying you of problems quickly.
提示:即使有大量的日志记录,除非您定期查看日志,否则您也可能不会意识到您的应用程序存在问题。对于任何大中型应用程序,这都变得不切实际,因此 Sentry (https://sentry.io) 等监控服务对于快速通知您问题非常宝贵。

If this sounds like a lot of work, you’re in luck. ASP.NET Core does a ton of the “breadcrumb logging” for you so that you can focus on creating high-quality log messages that provide the most value when diagnosing problems.
如果这听起来像是很多工作,那么您很幸运。ASP.NET Core 为您执行了大量的“痕迹导航日志记录”,以便您可以专注于创建高质量的日志消息,从而在诊断问题时提供最大价值。

26.1.1 Highlighting problems using custom log messages

26.1.1 使用自定义日志消息高亮显示问题

ASP.NET Core uses logging throughout its libraries. Depending on how you configure your app, you’ll have access to the details of each request and EF Core query, even without adding logging messages to your own code. In figure 26.1 you can see the log messages created when you view a single recipe in the recipe application.
ASP.NET Core 在其整个库中使用日志记录。根据你的应用配置方式,你将有权访问每个请求和 EF Core 查询的详细信息,即使不向你自己的代码添加日志记录消息也是如此。在图 26.1 中,您可以看到在配方应用程序中查看单个配方时创建的日志消息。

alt text

Figure 26.1 The ASP.NET Core Framework libraries use logging throughout. A single request generates multiple log messages that describe the flow of the request through your application.
图 26.1 ASP.NET Core Framework 库全程使用日志记录。单个请求会生成多条日志消息,用于描述请求通过应用程序的流向。

This gives you a lot of useful information. You can see which URL was requested, the Razor Page and page handler that were invoked (for a Razor Pages app), the Entity Framework Core (EF Core )database command, the action result executed, and the response. This information can be invaluable when you’re trying to isolate a problem, whether it’s a bug in a production app or a feature in development when you’re working locally.
这为您提供了很多有用的信息。您可以查看请求的 URL、调用的 Razor Page 和页面处理程序(针对 Razor Pages 应用)、Entity Framework Core (EF Core) 数据库命令、执行的作结果和响应。当您尝试隔离问题时,无论是生产应用程序中的错误还是本地工作时开发中的功能,这些信息都非常宝贵。

This infrastructure logging can be useful, but log messages that you create yourself can have even greater value. For example, you may be able to spot the cause of the error from the log messages in figure 26.1; we’re attempting to view a recipe with an unknown RecipeId of 5, but it’s far from obvious. If you explicitly add a log message to your app when this happens, as in figure 26.2, the problem is much more apparent.
此基础结构日志记录可能很有用,但您自己创建的日志消息可能具有更大的价值。例如,您可能能够从图 26.1 中的日志消息中发现错误的原因;我们正在尝试查看 RecipeId 未知为 5 的配方,但这远非显而易见。如果你在发生这种情况时显式地向你的应用程序添加一条日志消息,如图 26.2 所示,问题会更加明显。

alt text

Figure 26.2 You can write your own logs. These are often more useful for identifying problems and interesting events in your apps.
图 26.2 您可以编写自己的日志。这些通常对于识别应用程序中的问题和有趣的事件更有用。

This custom log message easily stands out and clearly states both the problem (the recipe with the requested ID doesn’t exist) and the parameters/variables that led to it (the ID value of 5). Adding similar log messages to your own applications will make it easier for you to diagnose problems, track important events, and generally know what your app is doing.
此自定义日志消息很容易脱颖而出,并清楚地说明了问题(具有请求 ID 的配方不存在)和导致问题的参数/变量(ID 值为 5)。将类似的日志消息添加到您自己的应用程序将使您更容易诊断问题、跟踪重要事件,并且通常可以了解应用程序正在做什么。

I hope you’re now motivated to add logging to your apps, so we’ll dig into the details of what that involves. In section 26.1.2 you’ll see how to create a log message and how to define where the log messages are written. We’ll look in detail at these two aspects in sections 26.2 and 26.3; first, though, we’ll look at where they fit in terms of the ASP.NET Core logging framework as a whole.
我希望您现在有动力将日志记录添加到您的应用程序中,因此我们将深入研究其中涉及的细节。在 Section 26.1.2 中,您将看到如何创建日志消息以及如何定义日志消息的写入位置。我们将在 26.2 和 26.3 节中详细介绍这两个方面;不过,首先,我们将看看它们在整个 ASP.NET Core 日志记录框架中的位置。

26.1.2 The ASP.NET Core logging abstractions

26.1.2 ASP.NET Core 日志记录抽象

The ASP.NET Core logging framework consists of several abstractions (interfaces, implementations, and helper classes), the most important of which are shown in figure 26.3:
ASP.NET Core 日志记录框架由几个抽象(接口、实现和帮助程序类)组成,其中最重要的如图 26.3 所示:

• ILogger—This is the interface you’ll interact with in your code. It has a Log() method, which is used to write a log message.
ILogger - 这是您将在代码中与之交互的界面。它有一个 Log() 方法,用于编写日志消息。

• ILoggerProvider—This is used to create a custom instance of an ILogger, depending on the provider. A console ILoggerProvider would create an ILogger that writes to the console, whereas a file ILoggerProvider would create an ILogger that writes to a file.
ILoggerProvider - 用于创建 ILogger 的自定义实例,具体取决于提供程序。控制台 ILoggerProvider 将创建写入控制台的 ILogger,而文件 ILoggerProvider 将创建写入文件的 ILogger。

• ILoggerFactory—This is the glue between the ILoggerProvider instances and the ILogger you use in your code. You register ILoggerProvider instances with an ILoggerFactory and call CreateLogger() on the ILoggerFactory when you need an ILogger. The factory creates an ILogger that wraps each of the providers, so when you call the Log() method, the log is written to every provider.
ILoggerFactory - 这是 ILoggerProvider 实例和您在代码中使用的 ILogger 之间的粘附。使用 ILoggerFactory 注册 ILoggerProvider 实例,并在需要 ILogger 时对 ILoggerFactory 调用 CreateLogger () 。工厂会创建一个 ILogger 来包装每个提供程序,因此当您调用 Log() 方法时,日志将写入每个提供程序。

alt text

Figure 26.3 The components of the ASP.NET Core logging framework. You register logging providers with an ILoggerFactory, which creates implementations of ILogger. You write logs to the ILogger, which delegates to the ILogger implementations that write logs to the console or a file. You can send logs to multiple locations with this design without having to configure the locations when you create a log message.
图 26.3 ASP.NET Core 日志记录框架的组件。向 ILoggerFactory 注册日志记录提供程序,这将创建 ILogger 的实现。将日志写入 ILogger,ILogger 将委托给将日志写入控制台或文件的 ILogger 实现。您可以使用此设计将日志发送到多个位置,而无需在创建日志消息时配置位置。

The design in figure 26.3 makes it easy to add or change where your application writes the log messages without having to change your application code. The following listing shows all the code required to add an ILoggerProvider that writes logs to the console.
图 26.3 中的设计使添加或更改应用程序写入日志消息的位置变得容易,而无需更改应用程序代码。下面的清单显示了添加将日志写入控制台的 ILoggerProvider 所需的所有代码。

Listing 26.1 Adding a console log provider in Program.cs
清单 26.1 在 Program.cs 中添加控制台日志提供程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole() ❶

WebApplication app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

❶ Adds a new provider using the Logging property on WebApplicationBuilder
使用 WebApplicationBuilder 上的 Logging 属性添加新的提供程序

NOTE The console logger is added by default by WebApplicationBuilder, as you’ll see in section 26.3.
注意:默认情况下,控制台 Logger 由 WebApplicationBuilder 添加,如第 26.3 节所示。

Other than this configuration on WebApplicationBuilder, you don’t interact with ILoggerProvider instances directly. Instead, you write logs using an instance of ILogger, as you’ll see in the next section.
除了 WebApplicationBuilder 上的此配置之外,您不直接与 ILoggerProvider 实例交互。相反,您可以使用 ILogger 的实例编写日志,如下一节所示。

26.2 Adding log messages to your application

26.2 向应用程序添加日志消息

In this section we’ll look in detail at how to create log messages in your own application. You’ll learn how to create an instance of ILogger, and how to use it to add logging to an existing application. Finally, we’ll look at the properties that make up a logging record, what they mean, and what you can use them for.
在本节中,我们将详细介绍如何在您自己的应用程序中创建日志消息。您将学习如何创建 ILogger 的实例,以及如何使用它来向现有应用程序添加日志记录。最后,我们将了解构成日志记录的属性、它们的含义以及您可以使用它们的用途。

Logging, like almost everything in ASP.NET Core, is available through DI. To add logging to your own services, you need only inject an instance of ILogger<T>, where T is the type of your service.
与 ASP.NET Core 中的几乎所有内容一样,日志记录可通过 DI 获得。要将日志记录添加到您自己的服务中,您只需注入ILogger<T>的实例,其中 T 是您的服务类型。

NOTE When you inject ILogger<T>, the DI container indirectly calls ILoggerFactory.CreateLogger() to create the wrapped ILogger of figure 26.3. In section 26.2.2 you’ll see how to work directly with ILoggerFactory if you prefer. The ILogger<T> interface also implements the nongeneric ILogger interface but includes additional convenience methods.
注意当您注入 ILogger<T> 时,DI 容器会间接调用 ILoggerFactory.CreateLogger() 来创建图 26.3 中包装的 ILogger。在第 26.2.2 节中,如果您愿意,您将看到如何直接使用 ILoggerFactory。ILogger<T>接口还实现非泛型 ILogger 接口,但包含其他便捷方法。

You can use the injected ILogger instance to create log messages, which it writes to each configured ILoggerProvider. The following listing shows how to inject an ILogger<> instance into the PageModel of the Index.cshtml Razor Page for the recipe application from previous chapters and how to write a log message indicating how many recipes were found.
您可以使用注入的 ILogger 实例创建日志消息,并将其写入每个配置的 ILoggerProvider。以下列表显示了如何将 ILogger<> 实例注入前面章节中配方应用程序的 Index.cshtml Razor Page 的 PageModel,以及如何编写指示找到的配方数的日志消息。

Listing 26.2 Injecting ILogger into a class and writing a log message
清单 26.2 将 ILogger 注入到类中并编写日志消息

public class IndexModel : PageModel
{
    private readonly RecipeService _service;
    private readonly ILogger<IndexModel> _log;      #A

    public ICollection<RecipeSummaryViewModel> Recipes { get; set; }

    public IndexModel(
        RecipeService service,
        ILogger<IndexModel> log)                   #A
    {
        _service = service;
        _log = log;                                #A
    }

    public void OnGet()
    {
        Recipes = _service.GetRecipes();
        _log.LogInformation(                                  #B
            "Loaded {RecipeCount} recipes", Recipes.Count);   #B
    }
}

❶ Injects the generic ILogger using DI, which implements ILogger
使用 DI 注入泛型 ILogger,它实现 ILogger

❷ Writes an Information-level log. The RecipeCount variable is substituted in the message.
写入信息级日志。RecipeCount 变量在消息中被替换。

In this example you’re using one of the many extension methods on ILogger to create the log message, LogInformation(). There are many extension methods on ILogger that let you easily specify a LogLevel for the message.
在此示例中,您将使用 ILogger 上的众多扩展方法之一来创建日志消息 LogInformation()。ILogger 上有许多扩展方法,可让您轻松指定消息的 LogLevel。

DEFINITION The log level of a log is how important it is and is defined by the LogLevel enum. Every log message has a log level.
定义:日志的日志级别是它的重要性,由 LogLevel 枚举定义。每条日志消息都有一个日志级别。

You can also see that the message you pass to the LogInformation method has a placeholder indicated by braces, {RecipeCount}, and you pass an additional parameter, Recipes.Count, to the logger. The logger replaces the placeholder with the parameter at runtime. Placeholders are matched with parameters by position, so if you include two placeholders, for example, the second placeholder is matched with the second parameter.
您还可以看到,传递给 LogInformation 方法的消息具有由大括号指示的占位符 {RecipeCount},并且您将附加参数 Recipes.Count 传递给记录器。记录器在运行时将占位符替换为参数。占位符按位置与参数匹配,因此,例如,如果您包括两个占位符,则第二个占位符将与第二个参数匹配。

TIP You could have used normal string interpolation to create the log message, as in $"Loaded {Recipes.Count} recipes". But I recommend always using placeholders, as they provide additional information for the logger that can be used for structured logging, as you’ll see in section 26.5.
提示:您可以使用普通字符串插值来创建日志消息,如 $“Loaded {Recipes.Count} recipes”。但是我建议始终使用占位符,因为它们为 Logger 提供了可用于结构化日志记录的附加信息,如您将在Section 26.5中看到的那样。

When the OnGet page handler in the IndexModel executes, ILogger writes a message to any configured logging providers. The exact format of the log message varies from provider to provider, but figure 26.4 shows how the console provider displays the log message from listing 26.2.
当 IndexModel 中的 OnGet 页面处理程序执行时, ILogger 会将消息写入任何已配置的日志记录提供程序。日志消息的确切格式因提供者而异,但图 26.4 显示了控制台提供者如何显示清单 26.2 中的日志消息。

alt text

Figure 26.4 An example log message as it’s written to the default console provider. The log-level category provides information about how important the message is and where it was generated. The EventId provides a way to identify similar log messages.
图 26.4 写入默认控制台提供程序时的日志消息示例。日志级别类别提供有关消息的重要性以及消息生成位置的信息。EventId 提供了一种识别类似日志消息的方法。

The exact presentation of the message will vary depending on where the log is written, but each log record includes up to six common elements:
消息的确切表示方式会因日志的写入位置而异,但每条日志记录最多包含六个常见元素:

• Log level—The log level of the log is how important it is and is defined by the LogLevel enum.
日志级别 - 日志的日志级别是它的重要性,由 LogLevel 枚举定义。

• Event category—The category may be any string value, but it’s typically set to the name of the class creating the log. For ILogger<T>, the full name of the type T is the category.
事件类别 - 类别可以是任何字符串值,但通常设置为创建日志的类的名称。对于ILogger<T>,类型 T 的全名是类别。

• Message—This is the content of the log message. It can be a static string, or it can contain placeholders for variables, as shown in listing 26.2. Placeholders are indicated by braces, {} and are replaced by the provided parameter values.
Message - 这是日志消息的内容。它可以是一个静态字符串,也可以包含变量的占位符,如清单 26.2 所示。占位符由大括号 {} 表示,并替换为提供的参数值。

• Parameters—If the message contains placeholders, they’re associated with the provided parameters. For the example in listing 26.2, the value of Recipes.Count is assigned to the placeholder called RecipeCount. Some loggers can extract these values and expose them in your logs, as you’ll see in section 26.5.
参数 - 如果消息包含占位符,则它们与提供的参数相关联。对于清单 26.2 中的示例,Recipes.Count 的值被分配给名为 RecipeCount 的占位符。一些 Logger 可以提取这些值并在您的日志中公开它们,正如您将在Section 26.5中看到的那样。

• Exception—If an exception occurs, you can pass the exception object to the logging function along with the message and other parameters. The logger records the exception in addition to the message itself.
Exception - 如果发生异常,可以将 exception 对象与消息和其他参数一起传递给日志记录函数。除了消息本身之外,Logger 还会记录异常。

• EventId—This is an optional integer identifier for the error, which can be used to quickly find all similar logs in a series of log messages. You might use an EventId of 1000 when a user attempts to load a non-existent recipe and an EventId of 1001 when a user attempts to access a recipe they don’t have permission to access. If you don’t provide an EventId, the value 0 is used.
EventId - 这是错误的可选整数标识符,可用于在一系列日志消息中快速查找所有相似日志。当用户尝试加载不存在的配方时,您可以使用 EventId 1000,当用户尝试访问他们无权访问的配方时,您可以使用 EventId 1001。如果您未提供 EventId,则使用值 0。

High-performance logging with source generators
使用源生成器进行高性能日志记录源

Source generators are a compiler feature introduced in C# 9. Using this feature, you can automatically generate boilerplate code when your project compiles. .NET 7 includes several built-in source generators, such as the Regex generator I described in chapter 14. There’s also a source generator that works with ILogger, which can help you avoid pitfalls such as accidentally using interpolated strings, and makes more advanced and performant logging patterns easy to use.
生成器是 C# 9 中引入的一项编译器功能。使用此功能,您可以在项目编译时自动生成样板代码。.NET 7 包括几个内置的源生成器,例如我在第 14 章中描述的 Regex 生成器。还有一个与 ILogger 配合使用的源生成器,它可以帮助您避免误区,例如意外使用内插字符串,并使更高级和高性能的日志记录模式易于使用。

To use the logging source generator in the OnGet handler from listing 26.2, define a partial method in the IndexModel class, decorate it with a [LoggerMessage] attribute, and invoke the method inside the OnGet handler method:
要在清单 26.2 中的 OnGet 处理程序中使用日志记录源生成器,请在 IndexModel 类中定义一个分部方法,用 [LoggerMessage] 属性修饰它,并在 OnGet 处理程序方法中调用该方法:

[LoggerMessage(10, LogLevel.Information, "Loaded {RecipeCount} recipes")]
partial void LogLoadedRecipes(int recipeCount);

public void OnGet()
{
Recipes = _service.GetRecipes();
LogLoadedRecipes(Recipes.Count);
}

The [LoggerMessage] attribute defines the event ID, log level, and message the log message uses, and the parameters of the partial method it decorates are substituted into the message at runtime. This pattern also comes with several analyzers to make sure you use it correctly in your code while optimizing the generated code behind the scenes to prevent allocations where possible.
该 [LoggerMessage] 属性定义日志消息使用的事件 ID、日志级别和消息,并且它修饰的分部方法的参数在运行时替换为消息。此模式还附带了多个分析器,以确保您在代码中正确使用它,同时在后台优化生成的代码,以尽可能防止分配。

The logging source generator is optional, so it’s up to you whether to use it. You can read more about the source generator, the extra configuration options, and how it works on my blog at http://mng.bz/vn14 and in the documentation at http://mng.bz/4D1j.
日志记录源生成器是可选的,因此是否使用它取决于您。您可以在我的博客 http://mng.bz/vn14 和文档 http://mng.bz/4D1j 中阅读有关源生成器、额外配置选项及其工作原理的更多信息。

Not every log message will have all the possible elements. You won’t always have an Exception or parameters, for example, and it’s common to omit the EventId. There are various overloads to the logging methods that take these elements as additional method parameters. Besides these optional elements, each message has, at very least, a level, category, and message. These are the key features of the log, so we’ll look at each in turn.
并非每条日志消息都包含所有可能的元素。例如,您不会总是有 Exception 或参数,省略 EventId 是很常见的。日志记录方法存在各种重载,这些重载将这些元素作为附加方法参数。除了这些可选元素之外,每条消息至少具有 level、category 和 message。这些是日志的主要功能,因此我们将依次查看每个功能。

26.2.1 Log level: How important is the log message?

26.2.1 日志级别:日志消息有多重要?

Whenever you create a log using ILogger, you must specify the log level. This indicates how serious or important the log message is, and it’s an important factor when it comes to filtering which logs are written by a provider, as well as finding the important log messages after the fact.
每当使用 ILogger 创建日志时,都必须指定日志级别。这表明日志消息的严重性或重要性,在筛选提供商写入的日志以及事后查找重要日志消息时,这是一个重要因素。

You might create an Information level log when a user starts to edit a recipe. This is useful for tracing the application’s flow and behavior, but it’s not important, because everything is normal. But if an exception is thrown when the user attempts to save the recipe, you might create a Warning or Error level log.
当用户开始编辑配方时,您可以创建 Information level log(信息级别日志)。这对于跟踪应用程序的流和行为很有用,但并不重要,因为一切都很正常。但是,如果在用户尝试保存配方时引发异常,则可以创建 Warning (警告) 或 Error (错误) 级别日志。

The log level is typically set by using one of several extension methods on the ILogger interface, as shown in listing 26.3. This example creates an Information level log when the View method executes and a Warning level error if the requested recipe isn’t found.
日志级别通常是通过使用 ILogger 接口上的几种扩展方法之一来设置的,如清单 26.3 所示。此示例在执行 View 方法时创建 Information 级别日志,如果未找到请求的配方,则创建 Warning 级别错误。

Listing 26.3 Specifying the log level using extension methods on ILogger
清单 26.3 在 ILogger 上使用扩展方法指定日志级别

private readonly ILogger _log;          #A
public async IActionResult OnGet(int id)
{
    _log.LogInformation(                            #B
        "Loading recipe with id {RecipeId}", id);   #B

    Recipe = _service.GetRecipeDetail(id);
    if (Recipe is null)
    {
        _log.LogWarning(                                      #C
            "Could not find recipe with id {RecipeId}", id);  #C
        return NotFound();
    }
    return Page();
}

❶ An ILogger instance is injected into the Razor Page using constructor injection.
使用构造函数注入将 ILogger 实例注入 Razor 页面。

❷ Writes an Information level log message
写入信息级别日志消息

❸ Writes a Warning level log message
写入警告级别日志消息

The LogInformation and LogWarning extension methods create log messages with a log level of Information and Warning, respectively. There are six log levels to choose among, ordered here from most to least serious:
LogInformation 和 LogWarning 扩展方法分别创建日志级别为 Information 和 Warning 的日志消息。有六个日志级别可供选择,此处按从最严重到最不严重的顺序排序:

• Critical—For disastrous failures that may leave the app unable to function correctly, such as out-of-memory exceptions or if the hard drive is out of disk space or the server is on fire.
严重 - 对于可能导致应用程序无法正常工作的灾难性故障,例如内存不足异常、硬盘驱动器磁盘空间不足或服务器着火。

• Error—For errors and exceptions that you can’t handle gracefully, such as exceptions thrown when saving an edited entity in EF Core. The operation failed, but the app can continue to function for other requests and users.
错误 - 对于无法正常处理的错误和异常,例如在 EF Core 中保存已编辑的实体时引发的异常。作失败,但应用程序可以继续为其他请求和用户运行。

• Warning—For when an unexpected or error condition arises that you can work around. You might log a Warning for handled exceptions or when an entity isn’t found, as in listing 26.3.
“警告”(Warning) - 当出现可解决的意外或错误情况时。对于已处理的异常或未找到实体,您可以记录 Warning,如清单 26.3 所示。

• Information—For tracking normal application flow, such as logging when a user signs in or when they view a specific page in your app. Typically these log messages provide context when you need to understand the steps leading up to an error message.
信息 - 用于跟踪正常的应用程序流,例如在用户登录或查看应用程序中的特定页面时进行日志记录。通常,当您需要了解导致错误消息的步骤时,这些日志消息会提供上下文。

• Debug—For tracking detailed information that’s particularly useful during development. Generally, this level has only short-term usefulness.
调试 (Debug) - 用于跟踪开发过程中特别有用的详细信息。一般来说,这个级别只有短期的用处。

• Trace—For tracking extremely detailed information, which may contain sensitive information like passwords or keys. It’s rarely used and not used at all by the framework libraries.
跟踪 - 用于跟踪极其详细的信息,其中可能包含密码或密钥等敏感信息。它很少被框架库使用,也根本不被使用。

Think of these log levels in terms of a pyramid, as shown in figure 26.5. As you progress down the log levels, the importance of the messages goes down, but the frequency goes up. Typically, you’ll find many Debug level log messages in your application, but (I hope) few Critical- or Error-level messages.
将这些对数级别想象成金字塔,如图 26.5 所示。随着日志级别的降低,消息的重要性会下降,但频率会上升。通常,您会在应用程序中找到许多 Debug 级别的日志消息,但 (我希望) 很少的 Critical- 或 Error 级别的消息。

alt text

Figure 26.5 The pyramid of log levels. Logs with a level near the base of the pyramid are used more frequently but are less important. Logs with a level near the top should be rare but are important.
图 26.5 对数水平的金字塔。水平仪靠近金字塔底部的原木使用频率更高,但不太重要。级别接近顶部的日志应该很少见,但很重要。

This pyramid shape will become more meaningful when we look at filtering in section 26.4. When an app is in production, you typically don’t want to record all the Debug-level messages generated by your application. The sheer volume of messages would be overwhelming to sort through and could end up filling your disk with messages that say “Everything’s OK!” Additionally, Trace messages shouldn’t be enabled in production, as they may leak sensitive data. By filtering out the lower log levels, you can ensure that you generate a sane number of logs in production but have access to all the log levels in development.
当我们查看 26.4 节中的过滤时,这个金字塔形状将变得更加有意义。当应用程序处于生产环境中时,您通常不希望记录应用程序生成的所有 Debug 级别消息。庞大的消息量会让人不知所措,最终可能会用“一切都很好”的消息填满您的磁盘。此外,不应在生产环境中启用 Trace 消息,因为它们可能会泄露敏感数据。通过筛选出较低的日志级别,您可以确保在生产环境中生成一定数量的日志,但可以访问开发中的所有日志级别。

In general, higher-level logs are more important than lower-level logs, so a Warning log is more important than an Information log, but there’s another aspect to consider. Where the log came from, or who created the log, is a key piece of information that’s recorded with each log message and is called the category.
通常,较高级别的日志比较低级别的日志更重要,因此 Warning 日志比 Information 日志更重要,但还有另一个方面需要考虑。日志的来源或日志的创建者是每条日志消息中记录的关键信息,称为类别。

26.2.2 Log category: Which component created the log

26.2.2 日志类别:哪个组件创建了日志

As well as a log level, every log message also has a category. You set the log level independently for every log message, but the category is set when you create the ILogger instance. Like log levels, the category is particularly useful for filtering, as you’ll see in section 26.4. It’s written to every log message, as shown in figure 26.6.
除了日志级别外,每条日志消息也有一个类别。您可以为每个日志消息单独设置日志级别,但类别是在创建 ILogger 实例时设置的。与日志级别一样,该类别对于过滤特别有用,如第 26.4 节所示。它被写入每个日志消息,如图 26.6 所示。

alt text

Figure 26.6 Every log message has an associated category, which is typically the class name of the component creating the log. The default console logging provider outputs the log category for every log.
图 26.6 每条日志消息都有一个关联的类别,通常是创建日志的组件的类名。默认控制台日志记录提供程序输出每个日志的日志类别。

The category is a string, so you can set it to anything, but the convention is to set it to the fully qualified name of the type that’s using ILogger. In section 26.2 I achieved this by injecting ILogger into RecipeController; the generic parameter T is used to set the category of the ILogger.
category 是一个字符串,因此您可以将其设置为任何值,但惯例是将其设置为使用 ILogger 的类型的完全限定名称。在第 26.2 节中,我通过将 ILogger 注入 RecipeController 来实现这一点;泛型参数 T 用于设置 ILogger 的类别。

Alternatively, you can inject ILoggerFactory into your methods and pass an explicit category when creating an ILogger instance, as shown in the following listing. This lets you change the category to an arbitrary string.
或者,可以将 ILoggerFactory 注入到方法中,并在创建 ILogger 实例时传递显式类别,如下面的清单所示。这允许您将类别更改为任意字符串。

Listing 26.4 Injecting ILoggerFactory to use a custom category
列表 26.4 注入 ILoggerFactory 以使用自定义类别

public class RecipeService
{
    private readonly ILogger _log;
    public RecipeService(ILoggerFactory factory)    #A
    {
        _log = factory.CreateLogger("RecipeApp.RecipeService");     #B
    }
}

❶ Injects an ILoggerFactory instead of an ILogger directly
直接注入 ILoggerFactory 而不是 ILogger

❷ Passes a category as a string when calling CreateLogger
调用 CreateLogger 时将类别作为字符串传递

There is also an overload of CreateLogger() with a generic parameter that uses the provided class to set the category. If the RecipeService in listing 26.4 were in the RecipeApp namespace, the CreateLogger call could be written equivalently as
还有一个 CreateLogger() 的重载,其中包含一个泛型参数,该参数使用提供的类来设置类别。如果清单 26.4 中的 RecipeService 位于 RecipeApp 命名空间中,则 CreateLogger 调用可以等效地写为

_log = factory.CreateLogger<RecipeService>();

Similarly, the final ILogger instance created by this call would be the same as if you’d directly injected ILogger<RecipeService> instead of ILoggerFactory.
同样,此调用创建的最终 ILogger 实例与直接注入 ILogger<RecipeService> 而不是 ILoggerFactory 时相同。

TIP Unless you’re using heavily customized categories for some reason, favor injecting ILogger<T> into your methods over ILoggerFactory.
提示:除非出于某种原因使用高度自定义的类别,否则最好将 ILogger<T> 注入到方法中,而不是 ILoggerFactory。

The final compulsory part of every log entry is fairly obvious: the log message. At the simplest level, this can be any string, but it’s worth thinking carefully about what information would be useful to record—anything that will help you diagnose problems later on.
每个日志条目的最后一个强制部分相当明显:日志消息。在最简单的级别上,这可以是任何字符串,但值得仔细考虑记录哪些信息是有用的 — 任何有助于您稍后诊断问题的信息。

26.2.3 Formatting messages and capturing parameter values

26.2.3 格式化消息和捕获参数值

Whenever you create a log entry, you must provide a message. This can be any string you like, but as you saw in listing 26.2, you can also include placeholders indicated by braces, {}, in the message string:
无论何时创建日志条目,都必须提供一条消息。这可以是你喜欢的任何字符串,但正如你在 清单 26.2 中看到的,你也可以在消息字符串中包含由大括号 {} 指示的占位符:

_log.LogInformation("Loaded {RecipeCount} recipes", Recipes.Count);

Including a placeholder and a parameter value in your log message effectively creates a key-value pair, which some logging providers can store as additional information associated with the log. The previous log message would assign the value of Recipes.Count to a key, RecipeCount, and the log message itself is generated by replacing the placeholder with the parameter value, to give the following (where Recipes.Count=3):
在日志消息中包含占位符和参数值可以有效地创建一个键值对,一些日志记录提供商可以将其存储为与日志关联的其他信息。前面的日志消息会将 Recipes.Count 的值分配给键 RecipeCount,并且日志消息本身是通过将占位符替换为参数值来生成的,以给出以下内容(其中 Recipes.Count=3):

"Loaded 3 recipes"

You can include multiple placeholders in a log message, and they’re associated with the additional parameters passed to the log method. The order of the placeholders in the format string must match the order of the parameters you provide.
您可以在日志消息中包含多个占位符,它们与传递给 log 方法的其他参数相关联。格式字符串中占位符的顺序必须与您提供的参数的顺序匹配。

WARNING You must pass at least as many parameters to the log method as there are placeholders in the message. If you don’t pass enough parameters, you’ll get an exception at runtime.
警告:向 log 方法传递的参数必须至少与消息中的占位符数量相同。如果您没有传递足够的参数,您将在运行时收到异常。

For example, the log message
例如,日志消息

_log.LogInformation("User {UserId} loaded recipe {RecipeId}", 123, 456)

would create the parameters UserId=123 and RecipeId=456. Structured logging providers could store these values, in addition to the formatted log message "User 123 loaded recipe 456". This makes it easier to search the logs for a particular UserId or RecipeId.
将创建参数 UserId=123 和 RecipeId=456。结构化日志记录提供程序可以存储这些值,以及格式化的日志消息“User 123 loaded recipe 456”。这样可以更轻松地在日志中搜索特定 UserId 或 RecipeId。

DEFINITION Structured or semantic logging attaches additional structure to log messages to make them more easily searchable and filterable. Rather than storing only text, it stores additional contextual information, typically as key-value pairs. JavaScript Object Notation (JSON) is a common format used for structured log messages.
定义:结构化或语义日志记录将其他结构附加到日志消息,使其更易于搜索和筛选。它不仅存储文本,还存储其他上下文信息,通常作为键值对。JavaScript 对象表示法 (JSON) 是用于结构化日志消息的常用格式。

Not all logging providers use semantic logging. The default console logging provider format doesn’t, for example; the message is formatted to replace the placeholders, but there’s no way of searching the console by key-value.
并非所有日志记录提供程序都使用语义日志记录。例如,默认的控制台日志记录提供程序格式不会;消息的格式设置为替换占位符,但无法按 Key-Value 搜索控制台。

TIP You can enable JSON output for the console provider by calling WebApplicationBuilder.Logging.AddJsonConsole(). You can further customize the format of the provider, as described in the documentation at http://mng.bz/QP8v.
提示:您可以通过调用 WebApplicationBuilder.Logging.AddJsonConsole() 为控制台提供程序启用 JSON 输出。您可以进一步自定义提供程序的格式,如 http://mng.bz/QP8v 中的文档中所述。

Even if you’re not using structured logging initially, I recommend writing your log messages as though you are, with explicit placeholders and parameters. That way, if you decide to add a structured logging provider later, you’ll immediately see the benefits. Additionally, I find that thinking about the parameters that you can log in this way prompts you to record more parameter values instead of only a log message. There’s nothing more frustrating than seeing a message like "Cannot insert record due to duplicate key" but not having the key value logged!
即使您最初没有使用结构化日志记录,我也建议您像使用结构化日志记录一样编写日志消息,并使用明确的占位符和参数。这样,如果您决定稍后添加结构化日志记录提供商,您将立即看到好处。此外,我发现,考虑可以以这种方式记录的参数会提示您记录更多参数值,而不仅仅是日志消息。没有什么比看到类似 “Cannot insert record due to duplicate key” 的消息但没有记录键值更令人沮丧的了!

TIP Generally speaking, I’m a fan of C#’s interpolated strings, but don’t use them for your log messages when a placeholder and parameter would also make sense. Using placeholders instead of interpolated strings gives you the same output message but also creates key-value pairs that can be searched later.
提示:一般来说,我是 C# 的插值字符串的粉丝,但当占位符和参数也有意义时,不要将它们用于日志消息。使用占位符而不是内插字符串会为您提供相同的输出消息,但也会创建稍后可搜索的键值对。

We’ve looked a lot at how you can create log messages in your app, but we haven’t focused on where those logs are written. In the next section we’ll look at the built-in ASP.NET Core logging providers, how they’re configured, and how you can add a third-party provider.
我们已经研究了很多关于如何在应用程序中创建日志消息的研究,但我们没有关注这些日志的写入位置。在下一节中,我们将了解内置的 ASP.NET Core 日志记录提供程序、它们的配置方式以及如何添加第三方提供程序。

26.3 Controlling where logs are written using logging providers

26.3 使用日志记录提供程序控制日志的写入位置

In this section you’ll learn how to control where your log messages are written by adding ILoggerProviders to your application. As an example, you’ll see how to add a simple file logger provider that writes your log messages to a file, in addition to the existing console logger provider.
在本节中,您将了解如何通过将 ILoggerProviders 添加到您的应用程序来控制日志消息的写入位置。例如,除了现有的控制台记录器提供程序之外,您还将了解如何添加一个简单的文件记录器提供程序,用于将日志消息写入文件。

Up to this point, we’ve been writing all our log messages to the console. If you’ve run any ASP.NET Core sample apps locally, you’ll probably have seen the log messages written to the console window.
到目前为止,我们一直在将所有日志消息写入控制台。如果您在本地运行了任何 ASP.NET Core 示例应用程序,则可能已经看到写入控制台窗口的日志消息。

NOTE If you’re using Visual Studio and debugging by using the Internet Information Services (IIS) Express option, you won’t see the console window (though the log messages are written to the Debug Output window instead).
注意:如果您使用的是 Visual Studio 并使用 Internet Information Services (IIS) Express 选项进行调试,则不会看到控制台窗口(尽管日志消息会写入“调试输出”窗口)。

Writing log messages to the console is great when you’re debugging, but it’s not much use for production. No one’s going to be monitoring a console window on a server, and the logs wouldn’t be saved anywhere or be searchable. Clearly, you’ll need to write your production logs somewhere else.
在调试时,将日志消息写入控制台非常有用,但对生产没有多大用处。没有人会监控服务器上的控制台窗口,日志不会保存在任何地方,也无法搜索。显然,您需要将生产日志写入其他位置。

As you saw in section 26.1, logging providers control the destination of your log messages in ASP.NET Core. They take the messages you create using the ILogger interface and write them to an output location, which varies depending on the provider.
正如您在第 26.1 节中看到的那样,日志记录提供程序控制 ASP.NET Core 中日志消息的目的地。它们获取您使用 ILogger 接口创建的消息,并将其写入输出位置,该位置因提供商而异。

NOTE This name always gets to me: the log provider effectively consumes the log messages you create and outputs them to a destination. You can probably see the origin of the name from figure 26.3, but I still find it somewhat counterintuitive.
注意:我总是能想到这个名字:日志提供程序会有效地使用您创建的日志消息,并将它们输出到目标。你可能可以从图 26.3 中看到这个名字的由来,但我仍然觉得它有点违反直觉。

Microsoft has written several first-party log providers for ASP.NET Core that are available out of the box in ASP.NET Core. These providers include
Microsoft 为 ASP.NET Core 编写了几个第一方日志提供程序,这些提供程序在 ASP.NET Core 中开箱即用。这些提供商包括

• Console provider—Writes messages to the console, as you’ve already seen
控制台提供程序 - 如您所见,将消息写入控制台

• Debug provider—Writes messages to the debug window when you’re debugging an app in Visual Studio or Visual Studio Code, for example
调试提供程序 - 例如,当您在 Visual Studio 或 Visual Studio Code 中调试应用程序时,将消息写入调试窗口

• EventLog provider—Writes messages to the Windows Event Log and outputs log messages only when running in Windows, as it requires Windows-specific APIs
EventLog 提供程序 - 仅在 Windows 中运行时将消息写入 Windows 事件日志并输出日志消息,因为它需要特定于 Windows 的 API

• EventSource provider—Writes messages using Event Tracing for Windows (ETW) or LTTng tracing on Linux
EventSource 提供程序 - 使用 Windows 事件跟踪 (ETW) 或 Linux 上的 LTTng 跟踪编写消息

There are also many third-party logging provider implementations, such as an Azure App Service provider, an elmah.io provider, and an Elasticsearch provider. On top of that, there are integrations with other existing logging frameworks like NLog and Serilog. It’s always worth looking to see whether your favorite .NET logging library or service has a provider for ASP.NET Core, as most do.
还有许多第三方日志记录提供程序实现,例如 Azure 应用服务提供商、elmah.io 提供程序和 Elasticsearch 提供程序。最重要的是,它还与其他现有的日志记录框架(如 NLog 和 Serilog)集成。看看您最喜欢的 .NET 日志记录库或服务是否像大多数一样具有 ASP.NET Core 的提供程序,始终值得一试。

TIP Serilog (https://serilog.net) is my go-to logging framework. It’s a mature framework with a huge number of supported destinations for writing logs. See Serilog’s ASP.NET Core integration repository for details on how to use Serilog with ASP.NET Core apps: https://github.com/serilog/serilog-aspnetcore.
提示:Serilog (https://serilog.net) 是我的首选日志记录框架。它是一个成熟的框架,具有大量支持写入日志的目标。有关如何将 Serilog 与 ASP.NET Core 应用程序结合使用的详细信息,请参阅 Serilog 的 ASP.NET Core 集成存储库:https://github.com/serilog/serilog-aspnetcore

You configure the logging providers for your app in Program.cs. WebApplicationBuilder configures the console and debug providers for your application automatically, but it’s likely that you’ll want to change or add to these.
您可以在 Program.cs 中为您的应用程序配置日志记录提供程序。WebApplicationBuilder 会自动为您的应用程序配置控制台和调试提供程序,但您可能希望更改或添加这些提供程序。

In this section I show how to add a simple third-party logging provider that writes to a rolling file so our application writes logs to a new file each day. We’ll continue to log using the console and debug providers as well, because they’re more useful than the file provider when developing locally.
在本节中,我将介绍如何添加一个简单的第三方日志记录提供程序,该提供程序将写入滚动文件,以便我们的应用程序每天将日志写入新文件。我们还将继续使用控制台和调试提供程序进行日志记录,因为在本地开发时,它们比文件提供程序更有用。

To add a third-party logging provider in ASP.NET Core, follow these steps:
要在 ASP.NET Core 中添加第三方日志记录提供程序,请执行以下步骤:

  1. Add the logging provider NuGet package to the solution. I’m going to be using a provider called NetEscapades.Extensions.Logging.RollingFile, which is available on NuGet and GitHub. You can add it to your solution using the NuGet Package Manager in Visual Studio or using the .NET command-line interface (CLI) by running
    将日志记录提供程序 NuGet 包添加到解决方案中。我将使用一个名为 NetEscapades.Extensions.Logging.RollingFile 的提供程序,该提供程序可在 NuGet 和 GitHub 上找到。您可以使用 Visual Studio 中的 NuGet 包管理器或使用 .NET 命令行界面 (CLI) 将其添加到解决方案中,方法是运行
dotnet add package NetEscapades.Extensions.Logging.RollingFile

from your application’s project folder.
从应用程序的 Project 文件夹中。

  1. Add the logging provider to WebApplicationBuilder.Logging. You can add the file provider by calling AddFile(), as shown in the next listing. AddFile() is an extension method provided by the logging provider package to simplify adding the provider to your app.
    将日志记录提供程序添加到 WebApplicationBuilder.Logging。您可以通过调用 AddFile() 来添加文件提供程序,如下一个清单所示。AddFile() 是日志记录提供程序包提供的扩展方法,用于简化向应用程序添加提供程序的过程。

NOTE This package is a simple file logging provider, available at http://mng.bz/XN5a. It’s based on the Azure App Service logging provider. If you need a more robust package, consider using Serilog’s file providers instead.
注意:此包是一个简单的文件日志记录提供程序,可从 http://mng.bz/XN5a 获取。它基于 Azure 应用服务日志记录提供程序。如果您需要更健壮的包,请考虑改用 Serilog 的文件提供程序。

Listing 26.5 Adding a third-party logging provider to WebApplicationBuilder
清单 26.5 向 WebApplicationBuilder 添加第三方日志提供程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args); ❶
builder.Logging.AddFile(); ❷

WebApplication app = builder.Build();

app.MapGet("/", () => "Hello world!");

app.Run();

❶ The WebApplicationBuilder configures the console and debug providers as normal.
WebApplicationBuilder 照常配置控制台和调试提供程序。

❷ Adds the new file logging provider to the logger factory
将新的文件日志记录提供程序添加到 Logger 工厂

NOTE Adding a new provider doesn’t replace existing providers. WebApplicationBuilder automatically adds the console and debug logging providers in listing 26.5. To remove them, call builder.Logging.ClearProviders() before adding the file provider.
注意:添加新的提供商不会替换现有的提供商。WebApplicationBuilder 在清单 26.5 中自动添加控制台和调试日志提供程序。要删除它们,请调用 builder。Logging.ClearProviders() 的调用。

With the file logging provider configured, you can run the application and generate logs. Every time your application writes a log using an ILogger instance, ILogger writes the message to all configured providers, as shown in figure 26.7. The console messages are conveniently available, but you also have a persistent record of the logs stored in a file.
配置文件日志记录提供程序后,您可以运行应用程序并生成日志。每次您的应用程序使用 ILogger 实例写入日志时,ILogger 都会将消息写入所有配置的提供程序,如图 26.7 所示。控制台消息非常方便,但您也有存储在文件中的日志的持久记录。

alt text

Figure 26.7 Logging a message with ILogger writes the log using all the configured providers. This lets you, for example, log a convenient message to the console while also persisting the logs to a file.
图 26.7 使用 ILogger 记录消息会使用所有配置的提供程序写入日志。例如,这样,您就可以将方便的消息记录到控制台,同时将日志保存到文件中。

TIP By default, the rolling file provider writes logs to a subdirectory of your application. You can specify additional options such as filenames and file size limits using overloads of AddFile(). For production, I recommend using a more established logging provider, such as Serilog.
提示:默认情况下,滚动文件提供程序将日志写入应用程序的子目录。您可以使用 AddFile() 的重载指定其他选项,例如文件名和文件大小限制。对于生产环境,我建议使用更成熟的日志记录提供程序,例如 Serilog。

The key takeaway from listing 26.5 is that the provider system makes it easy to integrate existing logging frameworks and providers with the ASP.NET Core logging abstractions. Whichever logging provider you choose to use in your application, the principles are the same: add a new logging provider to WebApplicationBuilder.Logging using extension methods like AddConsole(), or AddFile() in this case.
清单 26.5 的关键要点是,provider 系统可以轻松地将现有的日志框架和提供程序与 ASP.NET Core 日志抽象集成。无论您选择在应用程序中使用哪种日志记录提供程序,原则都是相同的:使用AddConsole()或AddFile()等扩展方法向WebApplicationBuilder.Logging添加新的日志记录提供程序。

Logging your application messages to a file can be useful in some scenarios, and it’s certainly better than logging to a nonexistent console window in production, but it may still not be the best option.
在某些情况下,将应用程序消息记录到文件中可能很有用,这肯定比在生产环境中记录到不存在的控制台窗口要好,但它可能仍然不是最佳选择。

If you discovered a bug in production and needed to look at the logs quickly to see what happened, for example, you’d need to log on to the remote server, find the log files on disk, and trawl through them to find the problem. If you have multiple web servers, you’d have a mammoth job to fetch all the logs before you could even start to tackle the bug—assuming that you even have remote access to the production servers! Not fun. Add to that the possibility of file permission or drive space problems, and file logging seems less attractive.
例如,如果您在生产中发现了一个错误,并且需要快速查看日志以了解发生了什么,则需要登录到远程服务器,在磁盘上找到日志文件,然后浏览它们以查找问题。如果您有多个 Web 服务器,那么您将面临一项艰巨的工作来获取所有日志,然后才能开始处理错误 — 假设您甚至可以远程访问生产服务器!不好玩。再加上文件权限或驱动器空间问题的可能性,文件日志记录似乎不那么有吸引力。

Instead, it’s often better to send your logs to a centralized location, separate from your application. Exactly where this location may be is up to you; the key is that each instance of your app sends its logs to the same location, separate from the app itself.
相反,通常最好将日志发送到与应用程序分开的集中位置。这个位置的确切位置取决于您;关键是应用程序的每个实例都将其日志发送到同一位置,与应用程序本身分开。

If you’re running your app on Microsoft Azure, you get centralized logging for free because you can collect logs using the Azure App Service provider. Alternatively, you could send your logs to a third-party log aggregator service such as elmah.io (https://elmah.io) or Seq (https://getseq.net). You can find ASP.NET Core logging providers for each of these services on NuGet, so adding them is the same process as adding the file provider you’ve seen already.
如果您在 Microsoft Azure 上运行应用程序,则可以免费获得集中式日志记录,因为您可以使用 Azure 应用程序服务提供商收集日志。或者,您也可以将日志发送到第三方日志聚合器服务,例如 elmah.io (https://elmah.io) 或 Seq (https://getseq.net)。可以在 NuGet 上找到每个服务的 ASP.NET Core 日志记录提供程序,因此添加它们的过程与添加已看到的文件提供程序的过程相同。

Whichever providers you add, once you start running your apps in production, you’ll quickly discover a new problem: the sheer number of log messages your app generates! In the next section you’ll learn how to keep this under control without affecting your local development.
无论您添加哪个提供商,一旦您开始在生产环境中运行应用程序,您很快就会发现一个新问题:您的应用程序生成的日志消息数量庞大!在下一节中,您将学习如何在不影响本地开发的情况下控制这种情况。

26.4 Changing log verbosity with filtering

26.4 通过过滤更改日志详细程度

In this section you’ll see how to reduce the number of log messages written to the logger providers. You’ll learn how to apply a base level filter, filter out messages from specific namespaces, and use logging provider-specific filters.
在本节中,您将了解如何减少写入 Logger 提供程序的日志消息数量。您将学习如何应用基本级别的过滤器、过滤掉来自特定命名空间的消息以及使用特定于日志记录提供商的过滤器。

If you’ve been playing around with the logging samples, you’ll probably have noticed that you get a lot of log messages, even for a single request like the one in figure 26.2: messages from the Kestrel server and messages from EF Core, not to mention your own custom messages. When you’re debugging locally, having access to all that detailed information is extremely useful, but in production you’ll be so swamped by noise that picking out the important messages will be difficult.
如果您一直在使用日志记录示例,则可能已经注意到,即使对于如图 26.2 所示的单个请求:来自 Kestrel 服务器的消息和来自 EF Core 的消息,您也会收到大量日志消息,更不用说您自己的自定义消息了。在本地调试时,访问所有这些详细信息非常有用,但在生产环境中,您将被噪音所淹没,以至于很难挑选出重要的消息。

ASP.NET Core includes the ability to filter out log messages before they’re written, based on a combination of three things:
ASP.NET Core 包括在写入日志消息之前根据以下三项组合筛选出日志消息的功能:

• The log level of the message
消息的日志级别

• The category of the logger (who created the log)
记录器的类别(创建日志的人)

• The logger provider (where the log will be written)
记录器提供程序(将写入日志的位置)

You can create multiple rules using these properties, and for each log that’s created, the most specific rule is applied to determine whether the log should be written to the output. You could create the following three rules:
您可以使用这些属性创建多个规则,对于创建的每个日志,将应用最具体的规则来确定是否应将日志写入输出。您可以创建以下三条规则:

• The default minimum log level is Information. If no other rules apply, only logs with a log level of Information or above will be written to providers.
默认的最小日志级别为 Information。如果没有其他规则适用,则只会将日志级别为 Information 或更高的日志写入提供程序。

• For categories that start with Microsoft, the minimum log level is Warning. Any logger created in a namespace that starts with Microsoft will write only logs that have a log level of Warning or above. This would filter out the noisy framework messages you saw in figure 26.6.
对于以 Microsoft 开头的类别,最低日志级别为“警告”。在以 Microsoft 开头的命名空间中创建的任何 Logger 都将仅写入日志级别为 Warning 或更高的日志。这将过滤掉你在图 26.6 中看到的嘈杂的框架消息。

• For the console provider, the minimum log level is Error. Logs written to the console provider must have a minimum log level of Error. Logs with a lower level won’t be written to the console, though they might be written using other providers.
对于控制台提供商,最低日志级别为 Error。写入控制台提供程序的日志必须具有 Error (错误) 的最低日志级别。具有较低级别的日志不会写入控制台,尽管它们可能是使用其他提供程序写入的。

Typically, the goal with log filtering is to reduce the number of logs written to certain providers or from certain namespaces (based on the log category). Figure 26.8 shows a possible set of filtering rules that apply to the console and file logging providers.
通常,日志筛选的目标是减少写入某些提供程序或某些命名空间(基于日志类别)的日志数量。图 26.8 显示了一组可能的过滤规则,这些规则适用于控制台和文件日志记录提供程序。

alt text

Figure 26.8 Applying filtering rules to a log message to determine whether a log should be written. For each provider, the most specific rule is selected. If the log exceeds the rule’s required minimum level, the provider writes the log; otherwise, it discards it.
图 26.8 将过滤规则应用于日志消息以确定是否应写入日志。对于每个提供商,将选择最具体的规则。如果日志超过规则所需的最低级别,则提供程序将写入日志;否则,它将丢弃它。

In this example, the console logger explicitly restricts logs written in the Microsoft namespace to Warning or above, so the console logger ignores the log message shown. Conversely, the file logger doesn’t have a rule that explicitly restricts the Microsoft namespace, so it uses the configured minimum level of Information and writes the log to the output.
在此示例中,控制台记录器明确将 Microsoft 命名空间中写入的日志限制为 Warning 或更高级别,因此控制台记录器会忽略显示的日志消息。相反,文件记录器没有明确限制 Microsoft 命名空间的规则,因此它使用配置的最低级别 Information 并将日志写入输出。

TIP Only a single rule is chosen when deciding whether a log message should be written; rules aren’t combined. In figure 26.8, rule 1 is considered to be more specific than rule 5, so the log is written to the file provider, even though technically, both rules could apply.
提示:在决定是否应写入日志消息时,只选择一个规则;规则不会合并。在图 26.8 中,规则 1 被认为比规则 5 更具体,因此日志被写入文件提供程序,即使从技术上讲,这两个规则都适用。

You typically define your app’s set of logging rules using the layered configuration approach discussed in chapter 10, because this lets you easily have different rules when running in development and production.
您通常使用第 10 章中讨论的分层配置方法来定义应用程序的日志记录规则集,因为这可以让您在开发和生产环境中运行时轻松拥有不同的规则。

TIP As you saw in chapter 11, you can load configuration settings from multiple sources, like JSON files and environment variables, and can load them conditionally based on the IHostingEnvironment. A common practice is to include logging settings for your production environment in appsettings.json and overrides for your local development environment in appsettings.Development.json.
提示:正如您在第 11 章中所看到的,您可以从多个源(如 JSON 文件和环境变量)加载配置设置,并且可以基于 IHostingEnvironment 有条件地加载它们。一种常见的做法是在 appsettings.json 中包含生产环境的日志记录设置,并在 appsettings 中包含本地开发环境的覆盖。Development.json。

WebApplicationBuilder automatically loads configuration rules from the "Logging" section of the IConfiguration object. This happens automatically, and you rarely need to customize it, but listing 26.6 shows how you could also add configuration rules from the "LoggingRules" section using AddConfiguration().
WebApplicationBuilder 会自动从 IConfiguration 对象的 “Logging” 部分加载配置规则。这是自动发生的,您很少需要自定义它,但是清单 26.6 显示了如何使用 AddConfiguration() 从 “LoggingRules” 部分添加配置规则。

NOTE WebApplicationBuilder always adds the configuration to load from the "Logging" section; you can’t remove this. For this reason, it’s rarely worth adding configuration yourself; instead, use the default "Logging" configuration section where possible.
注意:WebApplicationBuilder 始终从 “Logging” 部分添加要加载的配置;您无法删除此内容。因此,您自己添加配置很少值得;相反,请尽可能使用默认的 “Logging” 配置部分。

Listing 26.6 Loading logging configuration using AddConfiguration()
列表 26.6 使用 AddConfiguration() 加载日志配置

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConfiguration(
    builder.Configuration.GetSection("LoggingRules")); ❶

var app = builder.Build();

app.MapGet("/", () => "Hello world!");
app.Run();

❶ Loads the log filtering configuration from the LoggingRules section
从 LoggingRules 部分加载日志过滤配置

Assuming that you don’t override the configuration section, your appsettings.json will typically contain a "Logging" section, which defines the configuration rules for your app. Listing 26.8 shows how this might look to define all the rules shown in figure 26.8.
假设你没有覆盖 configuration 部分,你的 appsettings.json 通常会包含一个 “Logging” 部分,它定义了你的应用程序的配置规则。清单 26.8 显示了如何定义图 26.8 中所示的所有规则。

Listing 26.7 The log filtering configuration section of appsettings.json
清单 26.7 appsettings.json 的日志过滤配置部分

{
  "Logging": {
    "LogLevel": {             #A
      "Default": "Debug",     #A
      "System": "Warning",    #A
      "Microsoft": "Warning"  #A
    },
    "File": {                      #B
      "LogLevel": {                #B
        "Default": "Information"   #B
      }
    },
    "Console": {                 #C
      "LogLevel": {              #C
        "Default": "Debug",      #C
        "Microsoft": "Warning"   #C
      }
    }
  }
}

❶ Rules to apply if there are no specific rules for a provider
如果提供程序没有特定规则,则要应用的规则

❷ Rules to apply to the File provider
应用于文件提供程序的规则

❸ Rules to apply to the Console provider
应用于控制台提供程序的规则

When creating your logging rules, the important thing to bear in mind is that if you have any provider-specific rules, these will take precedence over the category-based rules defined in the "LogLevel" section. Therefore, for the configuration defined in listing 26.7, if your app uses only the file or console logging providers, the rules in the "LogLevel" section will effectively never apply.
在创建日志记录规则时,要记住的重要一点是,如果您有任何特定于提供程序的规则,这些规则将优先于 “LogLevel” 部分中定义的基于类别的规则。因此,对于清单 26.7 中定义的配置,如果您的应用程序仅使用文件或控制台日志记录提供程序,则“LogLevel”部分中的规则实际上将永远不会适用。

If you find this confusing, don’t worry; so do I. Whenever I’m setting up logging, I check the algorithm used to determine which rule applies for a given provider and category, which is as follows:
如果您觉得这令人困惑,请不要担心;我也是。每当我设置日志记录时,我都会检查用于确定哪个规则适用于给定提供程序和类别的算法,如下所示:

  1. Select all rules for the given provider. If no rules apply, select all rules that don’t define a provider (the top "LogLevel" section from listing 26.7).
    选择给定提供程序的所有规则。如果没有适用的规则,请选择所有未定义提供程序的规则(清单 26.7 中的顶部“LogLevel”部分)。

  2. From the selected rules, select rules with the longest matching category prefix. If no selected rules match the category prefix, select the "Default" if present.
    从所选规则中,选择具有最长匹配类别前缀的规则。如果没有选定的规则与类别前缀匹配,请选择“Default”(默认)(如果存在)。

  3. If multiple rules are selected, use the last one.
    如果选择了多个规则,请使用最后一个规则。

  4. If no rules are selected, use the global minimum level, "LogLevel:Default" (Debug in listing 26.7).
    如果未选择任何规则,请使用全局最小级别 “LogLevel:Default” (列表 26.7 中的 Debug)。

Each of these steps except the last narrows down the applicable rules for a log message until you’re left with a single rule. You saw this in effect for a "Microsoft" category log in figure 26.8. Figure 26.9 shows the process in more detail.
除最后一个步骤外,这些步骤中的每一个步骤都会缩小日志消息的适用规则范围,直到您只剩下一条规则。您在图 26.8 中看到了 “Microsoft” 类别日志的效果。图 26.9 更详细地显示了该过程。

alt text

Figure 26.9 Selecting a rule to apply from the available set for the console provider and an Information level log. Each step reduces the number of rules that apply until you’re left with only one.
图 26.9 从控制台提供程序的可用集和信息级别日志中选择要应用的规则。每个步骤都会减少适用的规则数,直到只剩下一个规则。

WARNING Log filtering rules aren’t merged; a single rule is selected. Including provider-specific rules will override global category-specific rules, so I tend to stick to category-specific rules where possible to make the overall set of rules easier to understand.
警告:日志筛选规则不会合并;将选择单个规则。包含特定于提供商的规则将覆盖全局特定于类别的规则,因此我倾向于尽可能坚持使用特定于类别的规则,以使整个规则集更易于理解。

With some effective filtering in place, your production logs should be much more manageable, as shown in figure 26.10. Generally, I find it’s best to limit the logs from the ASP.NET Core infrastructure and referenced libraries to Warning or above while keeping logs that my app writes to Debug in development and Information in production.
通过一些有效的过滤,您的 生产日志应该更易于管理,如图 26.10 所示。通常,我发现最好将来自 ASP.NET Core 基础设施和引用库的日志限制为 Warning 或更高级别,同时保留我的应用程序写入 Debug in development 和 Information in production的日志。

alt text

Figure 26.10 Using filtering to reduce the number of logs written. In this example, category filters have been added to the Microsoft and System namespaces, so only logs of Warning and above are recorded. That increases the proportion of logs that are directly relevant to your application.
图 26.10 使用过滤来减少写入的日志数量。在此示例中,类别筛选器已添加到 Microsoft 和 System 命名空间,因此仅记录 Warning 及以上的日志。这会增加与您的应用程序直接相关的日志的比例。

This is close to the default configuration used in the ASP.NET Core templates. You may find you need to add additional category-specific filters, depending on which NuGet libraries you use and the categories they write to. The best way to find out is generally to run your app and see whether you get flooded with uninteresting log messages.
这接近 ASP.NET Core 模板中使用的默认配置。你可能会发现需要添加其他特定于类别的筛选器,具体取决于你使用的 NuGet 库以及它们写入的类别。找出答案的最佳方法通常是运行您的应用程序,看看您是否被不感兴趣的日志消息淹没。

TIP Most logging providers listen for configuration changes and update their filters dynamically. That means you should be able to modify your appsettings.json or appsettings.Development.json file and check the effect on the log messages, iterating quickly without restarting your app.
提示:大多数日志记录提供程序侦听配置更改并动态更新其过滤器。这意味着您应该能够修改 appsettings.json 或 appsettings。Development.json文件并检查对日志消息的影响,在不重新启动应用程序的情况下快速迭代。

Even with your log verbosity under control, if you stick to the default logging providers like the file or console loggers, you’ll probably regret it in the long run. These log providers work perfectly well, but when it comes to finding specific error messages or analyzing your logs, you’ll have your work cut out for you. In the next section you’ll see how structured logging can help you tackle this problem.
即使你的日志详细程度得到控制,如果你坚持使用默认的日志提供程序,如文件或控制台记录器,从长远来看,你可能会后悔。这些日志提供程序运行良好,但是在查找特定错误消息或分析日志时,您将需要完成大量工作。在下一节中,您将了解结构化日志记录如何帮助您解决这个问题。

26.5 Structured logging: Creating searchable, useful logs

26.5 结构化日志记录:创建可搜索的有用日志

In this section you’ll learn how structured logging makes working with log messages easier. You’ll learn to attach key-value pairs to log messages and how to store and query for key values using the structured logging provider Seq. Finally, you’ll learn how to use scopes to attach key-value pairs to all log messages within a block.
在本节中,您将了解结构化日志记录如何更轻松地处理日志消息。您将学习如何将键值对附加到日志消息,以及如何使用结构化日志记录提供程序 Seq 存储和查询键值。最后,您将学习如何使用范围将键值对附加到块中的所有日志消息。

Let’s imagine you’ve rolled out the recipe application we’ve been working on to production. You’ve added logging to the app so that you can keep track of any errors in your application, and you’re storing the logs in a file.
假设您已经将我们一直在开发的配方应用程序推广到生产环境中。您已将日志记录添加到应用程序,以便您可以跟踪应用程序中的任何错误,并且您将日志存储在文件中。

One day, a customer calls and says they can’t view their recipe. Sure enough, when you look through the log messages, you a see a warning:
有一天,一位客户打电话说他们无法查看他们的配方。果然,当您查看日志消息时,您会看到一条警告:

warn: RecipeApplication.Pages.Recipes.ViewModel [12]
      Could not find recipe with id 3245

This piques your interest. Why did this happen? Has it happened before for this customer? Has it happened before for this recipe? Has it happened for other recipes? Does it happen regularly?
这激起了您的兴趣。为什么会这样?此客户以前发生过吗?这个食谱以前发生过吗?其他食谱也发生过吗?它经常发生吗?

How would you go about answering these questions? Given that the logs are stored in a text file, you might start doing basic text searches in your editor of choice, looking for the phrase "Could not find recipe with id". Depending on your notepad-fu skills, you could probably get a fair way in answering your questions, but it would likely be a laborious, error-prone, and painful process.
您将如何回答这些问题?鉴于日志存储在文本文件中,您可以开始在所选编辑器中进行基本文本搜索,查找短语“Could not find recipe with id”。根据你的记事本技能,你可能会得到一个公平的方式来回答你的问题,但这可能是一个费力、容易出错和痛苦的过程。

The limiting factor is that the logs are stored as unstructured text, so text processing is the only option available to you. A better approach is to store the logs in a structured format so that you can easily query the logs, filter them, and create analytics. Structured logs could be stored in any format, but these days they’re typically represented as JSON. A structured version of the same recipe warning log might look something like this:
限制因素是日志存储为非结构化文本,因此文本处理是您唯一可用的选项。更好的方法是以结构化格式存储日志,以便您可以轻松查询日志、筛选日志并创建分析。结构化日志可以以任何格式存储,但现在它们通常表示为 JSON。同一配方警告日志的结构化版本可能如下所示:

{
  "eventLevel": "Warning",
  "category": "RecipeApplication.Pages.Recipes.ViewModel",
  "eventId": "12",
  "messageTemplate": "Could not find recipe with {recipeId}",
  "message": "Could not find recipe with id 3245",
  "recipeId": "3245"
}

This structured log message contains all the same details as the unstructured version, but in a format that would easily let you search for specific log entries. It makes it simple to filter logs by their EventLevel or to show only those logs relating to a specific recipe ID.
此结构化日志消息包含与非结构化版本相同的所有详细信息,但格式可让您轻松搜索特定日志条目。它使按日志的 EventLevel 筛选日志或仅显示与特定配方 ID 相关的日志变得简单。

NOTE This is only an example of what a structured log could look like. The format used for the logs will vary depending on the logging provider used and could be anything. The main point is that properties of the log are available as key-value pairs.
注意:这只是结构化日志的一个示例。用于日志的格式将根据所使用的日志记录提供程序而有所不同,可以是任何内容。要点是日志的属性可用作键值对。

Adding structured logging to your app requires a logging provider that can create and store structured logs. Elasticsearch is a popular general search and analytics engine that can be used to store and query your logs. One big advantage of using a central store such as Elasticsearch is the ability to aggregate the logs from all your apps in one place and analyze them together. You can add the Elasticsearch.Extensions.Logging provider to your app in the same way as you added the file sink in section 26.3.
向应用添加结构化日志记录需要可以创建和存储结构化日志的日志记录提供程序。Elasticsearch 是一种流行的通用搜索和分析引擎,可用于存储和查询您的日志。使用 Elasticsearch 等中央存储的一大优势是能够将来自所有应用程序的日志聚合到一个位置并一起分析它们。您可以按照在第 26.3 节中添加文件接收器的相同方式将 Elasticsearch.Extensions.Logging 提供程序添加到您的应用程序中。

NOTE Elasticsearch is a REST-based search engine that’s often used for aggregating logs. You can find out more at https://www.elastic.co/elasticsearch.
注意:Elasticsearch 是一个基于 REST 的搜索引擎,通常用于聚合日志。您可以在 https://www.elastic.co/elasticsearch 了解更多信息。

Elasticsearch is a powerful production-scale engine for storing your logs, but setting it up and running it in production isn’t easy. Even after you’ve got it up and running, there’s a somewhat steep learning curve associated with the query syntax. If you’re interested in something more user-friendly for your structured logging needs, Seq (https://getseq.net) is a great option. In the next section I’ll show you how adding Seq as a structured logging provider makes analyzing your logs that much easier.
Elasticsearch 是一个强大的生产规模引擎,用于存储您的日志,但在生产环境中设置和运行它并不容易。即使在您启动并运行它之后,与查询语法相关的学习曲线也会有些陡峭。如果您对更用户友好的结构化日志记录需求感兴趣,Seq (https://getseq.net) 是一个不错的选择。在下一节中,我将向您展示将 Seq 添加为结构化日志记录提供程序如何使分析日志变得更加容易。

26.5.1 Adding a structured logging provider to your app

26.5.1 向应用程序添加结构化日志记录提供程序

To demonstrate the advantages of structured logging, in this section you’ll configure an app to write logs to Seq. You’ll see that the configuration is essentially identical to unstructured providers, but the possibilities afforded by structured logging make considering it a no-brainer.
为了演示结构化日志记录的优势,在本节中,您将配置一个应用程序以将日志写入 Seq。您将看到该配置与非结构化提供程序基本相同,但结构化日志记录提供的可能性使得考虑它变得轻而易举。

Seq is installed on a server or your local machine and collects structured log messages over HTTP, providing a web interface for you to view and analyze your logs. It is currently available as a Windows app or a Linux Docker container. You can install a free version for development, which allows you to experiment with structured logging in general.
Seq 安装在服务器或本地计算机上,通过 HTTP 收集结构化日志消息,为您提供一个 Web 界面来查看和分析您的日志。它目前以 Windows 应用程序或 Linux Docker 容器的形式提供。您可以安装用于开发的免费版本,这样您就可以尝试一般的结构化日志记录。

TIP You can download Seq from https://getseq.net/Download.
提示:您可以从 https://getseq.net/Download 下载 Seq。

From the point of view of your app, the process for adding the Seq provider should be familiar:
从应用程序的角度来看,添加 Seq 提供程序的过程应该很熟悉:

  1. Install the Seq logging provider using Visual Studio or the .NET CLI with
    使用 Visual Studio 或 .NET CLI 安装 Seq 日志记录提供程序
dotnet add package Seq.Extensions.Logging
  1. Add the Seq logging provider in Program.cs. To add the Seq provider call AddSeq():
    在 Program.cs 中添加 Seq 日志记录提供程序。要添加 Seq 提供程序,请调用 AddSeq():
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Logging.AddSeq();

That’s all you need to add Seq to your app. This will send logs to the default local URL when you have Seq installed in your local environment. The AddSeq() extension method includes additional overloads to customize Seq when you move to production, but this is all you need to start experimenting locally.
这就是将 Seq 添加到您的应用程序所需的全部内容。当您在本地环境中安装了 Seq 时,这会将日志发送到默认的本地 URL。AddSeq() 扩展方法包括额外的重载,以便在您迁移到生产环境时自定义 Seq,但这就是您开始在本地实验所需的全部内容。

If you haven’t already, install Seq on your development machine (or run the Docker container) and navigate to the Seq app at http://localhost:5341. In a different tab, open your app, and start browsing your app and generating logs. Back in Seq, if you refresh the page, you’ll see a list of logs, something like figure 26.11. Clicking a log expands it and shows you the structured data recorded for the log.
如果您尚未在开发计算机上安装 Seq(或运行 Docker 容器),并在 http://localhost:5341 导航到 Seq 应用程序。在另一个选项卡中,打开您的应用,然后开始浏览您的应用并生成日志。回到 Seq,如果你刷新页面,你会看到一个日志列表,类似于图 26.11。单击日志可将其展开,并显示为该日志记录的结构化数据。

alt text

Figure 26.11 The Seq UI. Logs are presented as a list. You can view the structured logging details of individual logs, view analytics for logs in aggregate, and search by log properties.
图 26.11 Seq UI。日志以列表形式显示。您可以查看单个日志的结构化日志记录详细信息,查看聚合日志的分析,并按日志属性进行搜索。

ASP.NET Core supports structured logging by treating each captured parameter from your message format string as a key-value pair. If you create a log message using the following format string,
ASP.NET Core 通过将消息格式字符串中捕获的每个参数视为键值对来支持结构化日志记录。如果使用以下格式字符串创建日志消息,

_log.LogInformation("Loaded {RecipeCount} recipes", Recipes.Count);

the Seq logging provider creates a RecipeCount parameter with a value of Recipes.Count. These parameters are added as properties to each structured log, as you can see in figure 26.11.
Seq 日志记录提供程序会创建一个值为 Recipes.Count 的 RecipeCount 参数。这些参数作为属性添加到每个结构化日志中,如图 26.11 所示。

Structured logs are generally easier to read than your standard-issue console output, but their real power comes when you need to answer a specific question. Consider the problem from before, where you see this error:
结构化日志通常比标准问题控制台输出更易于阅读,但当您需要回答特定问题时,它们的真正功能就来了。考虑之前的问题,您会看到以下错误:

Could not find recipe with id 3245

You want to get a feel for how widespread the problem is. The first step would be to identify how many times this error has occurred and to see whether it’s happened to any other recipes. Seq lets you filter your logs, but it also lets you craft SQL queries to analyze your data, so finding the answer to the question takes a matter of seconds, as shown in figure 26.12.
您想了解这个问题的普遍性。第一步是确定此错误发生了多少次,并查看任何其他配方是否发生过此错误。Seq 允许您过滤日志,但它也允许您制作 SQL 查询来分析数据,因此找到问题的答案需要几秒钟,如图 26.12 所示。

alt text

Figure 26.12 Querying logs in Seq. Structured logging makes log analysis like this example easy.
图 26.12 在 Seq 中查询日志。结构化日志记录使像这个例子一样的日志分析变得容易。

NOTE You don’t need query languages like SQL for simple queries, but they make digging into the data easier. Other structured logging providers may provide query languages other than SQL, but the principle is the same as in this Seq example.
注意:您不需要像 SQL 这样的查询语言进行简单的查询,但它们可以更轻松地挖掘数据。其他结构化日志记录提供程序可能会提供 SQL 以外的查询语言,但原理与此 Seq 示例中的相同。

A quick search shows that you’ve recorded the log message with EventId.Id=12 (the EventId of the warning we’re interested in) 13 times, and every time, the offending RecipeId was 3245. This suggests that there may be something wrong with that recipe specifically, which points you in the right direction to find the problem.
快速搜索显示,您已经使用 EventId.Id=12(我们感兴趣的警告的 EventId)记录了 13 次日志消息,每次违规的 RecipeId 都是 3245。这表明该配方可能存在问题,这为您指明了查找问题的正确方向。

More often than not, figuring out errors in production involves logging detective work like this to isolate where the problem occurred. Structured logging makes this process significantly easier, so it’s well worth considering, whether you choose Seq, Elasticsearch, or a different provider.
通常情况下,找出生产中的错误涉及记录此类侦探工作以隔离问题发生的位置。结构化日志记录使此过程变得更加容易,因此,无论您选择 Seq、Elasticsearch 还是其他提供商,都值得考虑。

I’ve already described how you can add structured properties to your log messages using variables and parameters from the message. But as you can see in figure 26.11, there are far more properties visible than exist in the message alone.
我已经介绍了如何使用消息中的变量和参数将结构化属性添加到日志消息中。但是正如你在图 26.11 中看到的,可见的属性比单独的消息中要多得多。

Scopes provide a way to add arbitrary data to your log messages. They’re available in some unstructured logging providers, but they shine when used with structured logging providers. In the final section of this chapter I’ll demonstrate how you can use them to add data to your log messages.
范围提供了一种将任意数据添加到日志消息的方法。它们在一些非结构化日志记录提供商中可用,但在与结构化日志记录提供商一起使用时,它们会大放异彩。在本章的最后一节中,我将演示如何使用它们向日志消息添加数据。

26.5.2 Using scopes to add properties to your logs

26.5.2 使用范围向日志添加属性

You’ll often find in your apps that you have a group of operations that all use the same data, which would be useful to attach to logs. For example, you might have a series of database operations that all use the same transaction ID, or you might be performing multiple operations with the same user ID or recipe ID. Logging scopes provide a way of associating the same data to every log message in such a group.
您经常会在应用程序中发现,有一组作都使用相同的数据,这对于附加到日志非常有用。例如,您可能有一系列数据库作,这些作都使用相同的事务 ID,或者您可能正在使用相同的用户 ID 或配方 ID 执行多个作。日志记录范围提供了一种将相同数据与此类组中的每个日志消息相关联的方法。

DEFINITION Logging scopes are used to group multiple operations by adding relevant data to multiple log message.
定义:日志记录范围用于通过将相关数据添加到多个日志消息来对多个作进行分组。

Logging scopes in ASP.NET Core are created by calling ILogger.BeginScope(T state) and providing the state data to be logged. You create scopes inside a using block; any log messages written inside the scope block will have the associated data, whereas those outside won’t.
ASP.NET Core 中的日志记录范围是通过调用 ILogger.BeginScope(T state) 并提供要记录的状态数据来创建的。您可以在 using 块中创建范围;写入 scope 块内的任何日志消息都将包含关联的数据,而 scope 块外的日志消息则没有。

Listing 26.8 Adding scope properties to log messages with BeginScope
示例 26.8 使用 BeginScope 添加 scope 属性以记录消息

_logger.LogInformation("No, I don't have scope");   #A
using(_logger.BeginScope("Scope value"))                #B
using(_logger.BeginScope(new Dictionary<string, object>     #C
    {{ "CustomValue1", 12345 } }))                          #C
{
    _logger.LogInformation("Yes, I have the scope!");    #D
}
_logger.LogInformation("No, I lost it again");      #A

❶ Log messages written outside the scope block don’t include the scope state.
在 scope 块之外写入的日志消息不包含 scope 状态。

❷ Calling BeginScope starts a scope block, with a scope state of “Scope value”.
调用 BeginScope 将启动一个范围块,其范围状态为“范围值”。

❸ You can pass anything as the state for a scope.
你可以将任何内容作为 scope 的状态传递。

❹ Log messages written inside the scope block include the scope state.
写入 scope 块内的日志消息包括 scope 状态。

The scope state can be any object at all: an int, a string, or a Dictionary, for example. It’s up to each logging provider implementation to decide how to handle the state you provide in the BeginScope call, but typically, it is serialized using ToString().
范围 state 可以是任何对象:例如 int、string 或 Dictionary。由每个日志记录提供程序实现决定如何处理您在 BeginScope 调用中提供的状态,但通常,它是使用 ToString() 序列化的。

TIP The most common use for scopes I’ve found is to attach additional key-value pairs to logs. To achieve this behavior in Seq, you need to pass Dictionary<string, object> as the state object. Nicholas Blumhardt, the creator of Serilog and Seq, has examples and the reasoning for this on his blog in the “The semantics of ILogger.BeginScope()” article: http://mng.bz/GxDD.
提示:我发现的 scope 最常见的用途是将额外的键值对附加到日志。要在 Seq 中实现此行为,您需要将 Dictionary<string, object> 作为状态对象传递。Serilog 和 Seq 的创建者 Nicholas Blumhardt 在他的博客“ILogger.BeginScope() 的语义”一文中提供了示例和原因:http://mng.bz/GxDD

When the log messages inside the scope block are written, the scope state is captured and written as part of the log, as shown in figure 26.13. The Dictionary<> of key-value pairs is added directly to the log message (CustomValue1), and the remaining state values are added to the Scope property. You will likely find the dictionary approach the more useful of the two, as the added properties are more easily filtered on, as you saw in figure 26.12.
当写入 scope 块内的日志消息时,将捕获 scope 状态并将其作为日志的一部分写入,如图 26.13 所示。键值对的 Dictionary<> 将直接添加到日志消息 (CustomValue1) 中,其余状态值将添加到 Scope 属性中。你可能会发现字典方法在两者中更有用,因为添加的属性更容易过滤,如图 26.12 所示。

alt text

Figure 26.13 Adding properties to logs using scopes. Any scope state that is added using the dictionary approach is added as structured logging properties, but other state is added to the Scope property. Adding properties makes it easier to associate related logs with one another.
图 26.13 使用范围向日志添加属性。使用字典方法添加的任何范围状态都将添加为结构化日志记录属性,但其他状态将添加到 Scope 属性中。添加属性可以更轻松地将相关日志彼此关联。

That brings us to the end of this chapter on logging. Whether you use the built-in logging providers or opt to use a third-party provider like Serilog or NLog, ASP.NET Core makes it easy to get detailed logs not only for your app code, but also for the libraries that make up your app’s infrastructure, like Kestrel and EF Core. Whichever you choose, I encourage you to add more logs than you think you’ll need; you’ll thank me when it comes time to track down a problem.
这将我们带到了本章关于日志记录的结尾。无论您是使用内置日志记录提供程序,还是选择使用 Serilog 或 NLog 等第三方提供程序,ASP.NET Core 都可以轻松获取应用程序代码的详细日志,还可以轻松获取构成应用程序基础结构的库(如 Kestrel 和 EF Core)的详细日志。无论您选择哪种方式,我都鼓励您添加比您认为需要的更多的日志;当需要追踪问题时,您会感谢我。

In the next chapter we’re going to be looking at your ASP.NET Core application from a different point of view. Instead of focusing on the code and logic behind your app, we’re going to look at how you prepare an app for production. You’ll see how to specify the URLs your application uses and how to publish an app so that it can be hosted in IIS. Finally, you’ll learn about the bundling and minification of client-side assets, why you should care, and how to use BundlerMinifier in ASP.NET Core.
在下一章中,我们将从不同的角度看待您的 ASP.NET Core 应用程序。我们不会关注应用程序背后的代码和逻辑,而是要了解如何为生产准备应用程序。您将了解如何指定应用程序使用的 URL,以及如何发布应用程序,以便它可以托管在 IIS 中。最后,您将了解客户端资产的捆绑和缩小、为什么应该关注,以及如何在 ASP.NET Core 中使用 BundlerMinifier。

26.6 Summary

26.6 总结

Logging is critical for quickly diagnosing errors in production apps. You should always configure logging for your application so that logs are written to a durable location such as a filesystem or other service, not just to the console, where they will be lost if the window closes or the server restarts.
日志记录对于快速诊断生产应用程序中的错误至关重要。您应该始终为应用程序配置日志记录,以便将日志写入持久位置(如文件系统或其他服务),而不仅仅是写入控制台,如果窗口关闭或服务器重新启动,日志将丢失。

You can add logging to your own services by injecting ILogger, where T is the name of the service. Alternatively, inject ILoggerFactory and call CreateLogger().
您可以通过注入 ILogger 将日志记录添加到您自己的服务中,其中 T 是服务的名称。或者,注入 ILoggerFactory 并调用 CreateLogger()。

The log level of a message indicates how important it is and ranges from Trace to Critical. Typically, you’ll create many low-importance log messages and a few high-importance log messages.
消息的日志级别表示它的重要性,范围从 Trace 到 Critical 不等。通常,您将创建许多低重要性的日志消息和一些高重要性的日志消息。

You specify the log level of a log by using the appropriate extension method of ILogger to create your log. To write an Information level log, use ILogger.LogInformation(message).
通过使用 ILogger 的相应扩展方法来指定日志的日志级别来创建日志。若要编写信息级别日志,请使用 ILogger.LogInformation(message)。

The log category indicates which component created the log. It is typically set to the fully qualified name of the class creating the log, but you can set it to any string if you wish. ILogger<T> will have a log category of T.
日志类别指示创建日志的组件。它通常设置为创建日志的类的完全限定名称,但您可以根据需要将其设置为任何字符串。ILogger<T> 的日志类别为 T。

You can format messages with placeholder values, similar to the string.Format method, but with meaningful names for the parameters. Calling logger.LogInfo("Loading Recipe with id {RecipeId}", 1234) would create a log reading "Loading Recipe with id 1234", but it would also capture the value RecipeId=1234. This structured logging makes analyzing log messages much easier.
您可以使用占位符值设置消息格式,类似于字符串。Format 方法,但参数具有有意义的名称。调用 logger。LogInfo(“Loading Recipe with id {RecipeId}”, 1234) 将创建一个日志,显示“Loading Recipe with id 1234”,但它也会捕获值 RecipeId=1234。这种结构化日志记录使分析日志消息变得更加容易。

ASP.NET Core includes many logging providers out of the box, including the console, debug, EventLog, and EventSource providers. Alternatively, you can add third-party logging providers.
ASP.NET Core 包含许多开箱即用的日志记录提供程序,包括 console、debug、EventLog 和 EventSource 提供程序。或者,您可以添加第三方日志记录提供商。

You can configure multiple ILoggerProvider instances in ASP.NET Core, which define where logs are output. WebApplicationBuilder adds the console and debug providers, and you can add providers using the Logging property.
您可以在 ASP.NET Core 中配置多个 ILoggerProvider 实例,这些实例定义日志的输出位置。WebApplicationBuilder 添加控制台和调试提供程序,您可以使用 Logging 属性添加提供程序。

You can control logging output verbosity using configuration. WebApplicationBuilder uses the "Logging" configuration section to control output verbosity. You typically filter out more logs in production than when developing your application.
您可以使用 configuration 控制日志记录输出的详细程度。WebApplicationBuilder 使用 “Logging” 配置部分来控制输出详细程度。与开发应用程序时相比,您在生产中筛选出的日志通常更多。

Only a single log filtering rule is selected for each logging provider when determining whether to output a log message. The most specific rule is selected based on the logging provider and the category of the log message.
在确定是否输出日志消息时,仅为每个日志记录提供程序选择一个日志筛选规则。根据日志记录提供程序和日志消息的类别选择最具体的规则。

Structured logging involves recording logs so that they can be easily queried and filtered, instead of the default unstructured format that’s output to the console. This makes analyzing logs, searching for problems, and identifying patterns easier.
结构化日志记录涉及记录日志,以便可以轻松查询和筛选日志,而不是输出到控制台的默认非结构化格式。这使得分析日志、搜索问题和识别模式变得更加容易。

You can add properties to a structured log by using scope blocks. A scope block is created by calling ILogger.BeginScope(state) in a using block. The state can be any object and is added to all log messages inside the scope block.
您可以使用范围块将属性添加到结构化日志中。通过在 using 块中调用 ILogger.BeginScope(state) 来创建范围块。state 可以是任何对象,并添加到 scope 块内的所有日志消息中。

ASP.NET Core in Action 25 Authentication and authorization for APIs

25 Authentication and authorization for APIs
25 API 的身份验证和授权

This chapter covers
本章涵盖

• Seeing how authentication works for APIs in ASP.NET Core
了解 ASP.NET Core中 API 的身份验证工作原理

• Using bearer tokens for authentication
使用不记名令牌进行身份验证

• Testing APIs locally with JSON Web Tokens
使用 JSON Web 令牌在本地测试 API

• Applying authorization policies to minimal APIs
将授权策略应用于最少的 API

In chapter 23 you learned how authentication works with traditional web apps, such as those you would build with Razor Pages or Model-View-Controller (MVC) controllers. Traditional web apps typically use encrypted cookies to store the identity of a user for a request, which the AuthenticationMiddleware then decodes. In this chapter you’ll learn how authentication works for API applications, how it differs from traditional web apps, and what options are available.
在第 23 章中,您了解了身份验证如何适用于传统 Web 应用程序,例如使用 Razor Pages 或模型-视图-控制器 (MVC) 控制器构建的应用程序。传统的 Web 应用程序通常使用加密的 cookie 来存储请求的用户身份,然后 AuthenticationMiddleware 对其进行解码。在本章中,您将了解身份验证如何用于 API 应用程序、它与传统 Web 应用程序有何不同,以及有哪些可用选项。

We start by taking a high-level look at how authentication works for APIs, both in isolation and when they’re part of a larger application or distributed system. You’ll learn about some of the protocols involved, such as OAuth 2.0 and OpenID Connect; patterns you can use to protect your APIs; and the tokens used to control access, typically JSON Web Tokens, called JWTs.
我们首先从高层次上了解身份验证如何用于 API,无论是在隔离状态下,还是在它们属于大型应用程序或分布式系统时。您将了解一些涉及的协议,例如 OAuth 2.0 和 OpenID Connect;可用于保护 API 的模式;以及用于控制访问的令牌,通常是 JSON Web 令牌,称为 JWT。

In section 25.3 you’ll learn how to put this knowledge into practice, adding authentication to a minimal API application using JWTs. In section 25.4 you’ll learn how to use the .NET command-line interface (CLI) to generate JWTs for testing your API locally.
在 Section 25.3 中,您将学习如何将这些知识付诸实践,使用 JWT 将身份验证添加到最小的 API 应用程序中。在第 25.4 节中,您将学习如何使用 .NET 命令行界面 (CLI) 生成 JWT 以在本地测试您的 API。

The .NET CLI works well for generating tokens, but you need a way to add this token to a request. Specifically, if you’re using OpenAPI definitions and Swagger UI as described in chapter 11, you need a way to tell Swagger about your authentication requirements. In section 25.5 you’ll learn about some of the authentication configuration options for your OpenAPI documents and how to use Swagger UI to send authenticated requests to your API.
.NET CLI 非常适合生成令牌,但您需要一种方法来将此令牌添加到请求中。具体来说,如果你正在使用 OpenAPI 定义和 Swagger UI,如第 11 章所述,你需要一种方法来告诉 Swagger 你的身份验证要求。在第 25.5 节中,您将了解 OpenAPI 文档的一些身份验证配置选项,以及如何使用 Swagger UI 将经过身份验证的请求发送到 API。

Finally, in section 25.6 I show how to apply authorization policies to minimal API endpoints to restrict which users can call your APIs. The authorization concepts you learned about in chapter 24 for Razor Pages are the same for APIs, so you’re still using claims, requirements, handlers, and polices.
最后,在第 25.6 节中,我将展示如何将授权策略应用于最小的 API 端点,以限制哪些用户可以调用您的 API。您在第 24 章中学到的 Razor Pages 授权概念与 API 相同,因此您仍在使用声明、要求、处理程序和策略。

We’ll start off by looking at how authentication works when you have an API application. Many of the authentication concepts are similar to traditional apps, but the requirement to support multiple types of users, traditional apps, client-side apps, and mobile apps has led to subtly different solutions.
首先,我们将了解当您拥有 API 应用程序时身份验证的工作原理。许多身份验证概念与传统应用程序类似,但需要支持多种类型的用户、传统应用程序、客户端应用程序和移动应用程序,这导致了略有不同的解决方案。

25.1 Authentication for APIs and distributed applications

25.1 API 和分布式应用程序的身份验证

In this section you’ll learn about the authentication process for API applications, why it typically differs from authentication for traditional web apps, and some of the common patterns and protocols that are involved.
在本节中,您将了解 API 应用程序的身份验证过程、为什么它通常不同于传统 Web 应用程序的身份验证,以及涉及的一些常见模式和协议。

25.1.1 Extending authentication to multiple apps

25.1.1 将身份验证扩展到多个应用程序

I outlined the authentication process for traditional web apps in chapter 23. When a user signs in to your application, you set an encrypted cookie. This cookie contains a serialized version of the ClaimsPrincipal of the user, including their ID and any associated claims. When you make a second request, the browser automatically sends this cookie. The AuthenticationMiddleware then decodes the cookie, deserializes the ClaimsPrincipal, and sets the current user for the request, as shown previously in figure 23.3 and reproduced in figure 25.1.
我在第 23 章中概述了传统 Web 应用程序的身份验证过程。当用户登录到您的应用程序时,您需要设置一个加密的 Cookie。此 Cookie 包含用户的 ClaimsPrincipal 的序列化版本,包括其 ID 和任何关联的声明。当您发出第二个请求时,浏览器会自动发送此 cookie。然后,AuthenticationMiddleware 对 cookie 进行解码,反序列化 ClaimsPrincipal,并为请求设置当前用户,如前面的图 23.3 所示,并在图 25.1 中重现。

alt text

Figure 25.1 When a user first signs in to an app, the app sets an encrypted cookie containing the ClaimsPrincipal. On subsequent requests, the cookie sent with the request contains the user principal, which is deserialized, validated, and used to authenticate the request.
图 25.1 当用户首次登录应用程序时,应用程序会设置一个包含 ClaimsPrincipal 的加密 Cookie。在后续请求中,与请求一起发送的 Cookie 包含用户主体,该主体经过反序列化、验证并用于对请求进行身份验证。

This flow works particularly well when you have a single traditional web app that’s doing all the work. The app is responsible for authenticating and managing users, as well as serving your app data and executing business logic, as shown in figure 25.2.
当您有一个执行所有工作的传统 Web 应用程序时,此流程特别有效。该应用程序负责验证和管理用户,以及提供应用程序数据和执行业务逻辑,如图 25.2 所示。

alt text

Figure 25.2 Traditional apps typically handle all the functionality of an app: the business logic, generating the UI, authentication, and user management.
图 25.2 传统应用程序通常处理应用程序的所有功能:业务逻辑、生成 UI、身份验证和用户管理。

In addition to traditional web apps, it’s common to use ASP.NET Core as an API to serve data for mobile and client-side single-page applications (SPAs). Similarly, even traditional web apps using Razor Pages often need to call API applications behind the scenes, as shown in figure 25.3.
除了传统的 Web 应用程序之外,通常将 ASP.NET Core 用作 API,为移动和客户端单页应用程序 (SPA) 提供数据。同样,即使是使用 Razor Pages 的传统 Web 应用程序也经常需要在后台调用 API 应用程序,如图 25.3 所示。

alt text

Figure 25.3 Modern applications typically need to expose web APIs for mobile and client-side apps, as well as potentially calling APIs on the backend. When all these services need to authenticate and manage users, this becomes logistically complicated.
图 25.3 现代应用程序通常需要为移动和客户端应用程序公开 Web API,并可能在后端调用 API。当所有这些服务都需要对用户进行身份验证和管理时,这在逻辑上变得复杂。

In this situation you have multiple apps and APIs, all of which need to understand that the same user is logically making a request across all the apps and APIs. If you keep the same approach as before, where each app manages its own users, things can quickly become unmanageable!
在这种情况下,您有多个应用程序和 API,所有这些应用程序和 API 都需要了解同一用户在逻辑上跨所有应用程序和 API 发出请求。如果您保持与以前相同的方法,即每个应用程序管理自己的用户,事情很快就会变得难以管理!

You’d need to duplicate all the sign-in logic between the apps and APIs, as well as have some central database holding the user details. Users would likely need to sign in multiple times to access different parts of the service. On top of that, using cookies becomes problematic for some mobile clients in particular or where you’re making requests to multiple domains (as cookies belong to only a single domain). So how can we improve this? By moving the authentication responsibilities to a separate service.
您需要在应用程序和 API 之间复制所有登录逻辑,并拥有一些包含用户详细信息的中央数据库。用户可能需要多次登录才能访问服务的不同部分。最重要的是,使用 cookie 对于某些移动客户端来说会成为问题,特别是当您向多个域发出请求时(因为 cookie 只属于一个域)。那么我们如何改进这一点呢?通过将身份验证责任转移到单独的服务。

25.1.2 Centralizing authentication in an identity provider

25.1.2 在身份提供程序中集中验证

Modern systems often have many moving parts, each of which requires some level of authentication and authorization to protect each app from unauthorized use. Instead of embedding authentication responsibilities in each application, a common approach is to extract the code that’s common to all the apps and APIs and then move it to an identity provider, as shown in figure 25.4.
现代系统通常有许多移动部件,每个部件都需要一定程度的身份验证和授权,以保护每个应用程序免受未经授权的使用。一种常见的方法是提取所有应用程序和 API 通用的代码,然后将其移动到身份提供者,而不是在每个应用程序中嵌入身份验证责任,如图 25.4 所示。

alt text

Figure 25.4 An alternative architecture involves using a central identity provider to handle all the authentication and user management for the system. Tokens are passed back and forth among the identity provider, apps, and APIs.
图 25.4 另一种架构涉及使用中央身份提供商来处理系统的所有身份验证和用户管理。令牌在身份提供商、应用程序和 API 之间来回传递。

Instead of signing in to an app directly, the app redirects to an identity provider. The user signs in to this identity provider, which passes bearer tokens back to the client (a browser or mobile app, for example) to indicate who the user is and what they’re allowed to access. The client can pass these tokens to the APIs to provide information about the logged-in user without needing to reauthenticate or manage users directly in the API.
应用程序不是直接登录到应用程序,而是重定向到身份提供商。用户登录到此身份提供商,该身份提供商将不记名令牌传递回客户端(例如浏览器或移动应用程序),以指示用户是谁以及允许他们访问的内容。客户端可以将这些令牌传递给 API,以提供有关已登录用户的信息,而无需直接在 API 中重新验证或管理用户。

DEFINITION Bearer tokens are strings that contain authentication details about a user or app. They may or may not be encrypted but are typically signed to avoid tampering. JWTs are the most common format. We’ll look more at JWTs in section 25.2.
定义:持有者令牌是包含有关用户或应用程序的身份验证详细信息的字符串。它们可能已加密,也可能未加密,但通常会进行签名以避免篡改。JWT 是最常见的格式。我们将在 25.2 节中更多地了解 JWT。

Using a separate identity provider is clearly more complicated on the face of it, as you’ve thrown a whole new service into the mix, but in the long run this has several advantages:
使用单独的身份提供商从表面上看显然更复杂,因为您已经将一个全新的服务投入其中,但从长远来看,这有几个好处:

• Users can share their identity among multiple services. As you’re logged in to the central identity provider, you’re essentially logged in to all apps that use that service. This gives you the single-sign-on experience, where you don’t have to keep logging in to multiple services.
用户可以在多个服务之间共享其身份。当您登录到中央身份提供商时,您实际上已经登录到使用该服务的所有应用程序。这为您提供了单点登录体验,您不必一直登录到多个服务。

• You don’t need to duplicate sign-in logic between multiple services. All the sign-in logic is encapsulated in the identity provider, so you don’t need to add sign-in screens to all your apps.
您无需在多个服务之间复制登录逻辑。所有登录逻辑都封装在身份提供商中,因此您无需向所有应用程序添加登录屏幕。

• The identity provider has a single responsibility. The identity provider is responsible only for authentication and managing users. In many cases, this is generic enough that you can (and should!) use a third-party identity service, such as Auth0 or Azure Active Directory, instead of building your own.
身份提供商有单一职责。身份提供商仅负责身份验证和管理用户。在许多情况下,这足够通用,您可以(并且应该)使用第三方身份服务,例如 Auth0 或 Azure Active Directory,而不是构建自己的身份服务。

• You can easily add new sign-in mechanisms. Whether you use the identity provider approach or the traditional approach, it’s possible to use external services to handle the authentication of users. You’ll have seen this in apps that allow you to “log in using Facebook” or “log in using Google,” for example. If you use a centralized identity provider, you can add support for more providers in one place instead of having to configure every app and API explicitly.
您可以轻松添加新的登录机制。无论您使用身份提供商方法还是传统方法,都可以使用外部服务来处理用户的身份验证。例如,您会在允许您 “使用 Facebook 登录” 或 “使用 Google 登录” 的应用程序中看到这一点。如果您使用集中式身份提供商,则可以在一个位置添加对更多提供商的支持,而不必显式配置每个应用程序和 API。

Out of the box, ASP.NET Core supports architectures like this and for consuming bearer tokens from identity providers, but it doesn’t include support for issuing those tokens in the core framework. That means you’ll need to use another library or service as the identity provider.
开箱即用的 ASP.NET Core 支持此类架构,并支持使用来自身份提供商的不记名令牌,但它不包括对在核心框架中颁发这些令牌的支持。这意味着您需要使用其他库或服务作为身份提供商。

As I mentioned in chapter 23, one excellent option is to use a third-party identity provider, such as Facebook, Google, Okta, Auth0, or Azure Active Directory. These providers take care of storing user passwords, authenticating using modern standards like WebAuthn (https://webauthn.guide), and looking for malicious attempts to impersonate users.
正如我在第 23 章中提到的,一个很好的选择是使用第三方身份提供商,比如 Facebook、Google、Okta、Auth0 或 Azure Active Directory。这些提供商负责存储用户密码、使用 WebAuthn (https://webauthn.guide) 等现代标准进行身份验证,并寻找冒充用户的恶意尝试。

By using an identity provider, you leave the tricky security details to the experts and can focus on the core purpose of your business, whichever domain that is. Not all providers are equal, though: For some providers (such as Auth0) you own the profiles, whereas for others (Facebook or Google) you don’t. Make sure to choose a provider that matches your requirements.
通过使用身份提供商,您可以将棘手的安全细节留给专家,并且可以专注于业务的核心目的,无论哪个域。但是,并非所有提供商都是平等的:对于某些提供商(例如 Auth0),您拥有配置文件,而对于其他提供商(Facebook 或 Google),您则不拥有。确保选择符合您要求的提供商。

Tip Wherever possible, I recommend using a third-party identity provider. Well-respected identity providers have many experts working solely on securing your customers’ details, proactively preventing attacks and ensuring that the data is safe. By leaving this tricky job to the experts, you’re free to focus on the core business of your app, whatever that may be.
提示:我建议尽可能使用第三方身份提供商。备受尊敬的身份提供商拥有许多专家,专门致力于保护客户的详细信息、主动防止攻击并确保数据安全。通过将这项棘手的工作留给专家,您可以自由地专注于应用程序的核心业务,无论它是什么。

Another common option is to build your own identity provider. This may sound like a lot of work (and it is!), but thanks to excellent libraries like OpenIddict (https://github.com/openiddict) and Duende’s IdentityServer (https://duendesoftware.com), it’s perfectly possible to write your own identity provider to serve bearer tokens that can be consumed by your apps and APIs.
另一个常见的选项是构建自己的身份提供商。这听起来像是很多工作(确实如此),但多亏了 OpenIddict (https://github.com/openiddict) 和 Duende 的 IdentityServer (https://duendesoftware.com) 等优秀库,您完全可以编写自己的身份提供商来提供可供您的应用程序和 API 使用的不记名令牌。

WARNING You should consider carefully whether the effort and risks associated with creating your own identity provider are worthwhile. Bugs are a fact of life, and a bug in your identity provider could easily result in a security vulnerability. Nevertheless, if you have specific identity requirements, creating your own identity provider may be a reasonable or necessary option.
警告:您应该仔细考虑与创建自己的身份提供商相关的努力和风险是否值得。错误是事实,身份提供商中的错误很容易导致安全漏洞。不过,如果您有特定的身份要求,创建自己的身份提供商可能是一个合理或必要的选择。

An aspect often overlooked by people getting started with OpenIddict and IdentityServer is that they aren’t prefabricated solutions. They consist of a set of services and middleware that you add to a standard ASP.NET Core app, providing an implementation of relevant identity standards, according to the specification. You, as a developer, still need to write the profile management code that knows how to create a new user (normally in a database), load a user’s details, validate their password, and manage their associated claims. On top of that, you need to provide all the UI code for the user to log in, manage their passwords, and configure two-factor authentication (2FA). It’s not for the faint of heart!
开始使用 OpenIddict 和 IdentityServer 的人经常忽略的一个方面是,它们不是预制的解决方案。它们由一组服务和中间件组成,您可以将其添加到标准 ASP.NET Core 应用程序中,根据规范提供相关身份标准的实现。作为开发人员,您仍然需要编写 Profile Management 代码,该代码知道如何创建新用户(通常在数据库中)、加载用户的详细信息、验证其密码以及管理其关联的声明。最重要的是,您需要为用户提供所有 UI 代码以登录、管理他们的密码和配置双因素身份验证 (2FA)。不适合胆小的人!

In many ways, you can think of an identity provider as a traditional web app that has only account management pages. If you want to take on building your own identity provider, ASP.NET Core Identity, described in chapter 23, provides a good basis for the user management side. Adding IdentityServer or OpenIddict gives you the ability to generate tokens for other services, using the OpenID Connect standard, for maximum interoperability with other services.
在许多方面,您可以将身份提供商视为只有账户管理页面的传统 Web 应用程序。如果您想构建自己的身份提供商,第 23 章中描述的 ASP.NET Core Identity 为用户管理方面提供了良好的基础。添加 IdentityServer 或 OpenIddict 后,您可以使用 OpenID Connect 标准为其他服务生成令牌,以实现与其他服务的最大互作性。

25.1.3 OpenID Connect and OAuth 2.0

25.1.3 OpenID Connect 和 OAuth 2.0

OpenID Connect (OIDC) (http://openid.net/connect) is an authentication protocol built on top of the OAuth 2.0 (https://oauth.net/2) specification. It’s designed to facilitate the kind of approaches described in section 25.1.2, where you want to leave the responsibility of storing user credentials to someone else (an identity provider). It provides an answer to the question “Which user sent this request?” without your having to manage the user yourself.
OpenID Connect (OIDC) (http://openid.net/connect) 是在 OAuth 2.0 (https://oauth.net/2) 规范之上构建的身份验证协议。它旨在促进第 25.1.2 节中描述的那种方法,您希望将存储用户凭据的责任留给其他人(身份提供商)。它为“哪个用户发送了此请求”问题提供答案,而无需您自己管理用户。

NOTE It isn’t strictly necessary to understand these protocols to add authentication to your APIs, but I think it’s best to have a basic understanding of them so that you understand where your APIs fit into the security landscape. If you want to learn more about OpenID Connect, OpenID Connect in Action, by Prabath Siriwardena (Manning, 2023), provides lots more details.
注意:为您的 API 添加身份验证并不是绝对必要的,但我认为最好对它们有一个基本的了解,以便您了解您的 API 在安全环境中的位置。如果您想了解有关 OpenID Connect 的更多信息,Prabath Siriwardena(曼宁,2023 年)的 OpenID Connect in Action 提供了更多详细信息。

Open ID Connect is built on top of the OAuth 2.0 protocol, so it helps to understand that protocol a little first. OAuth 2.0 is an authorization protocol. It allows a user to delegate access of a resource to a different service in a controlled manner without revealing any additional details, such as your identity or any other information.
Open ID Connect 构建在 OAuth 2.0 协议之上,因此首先了解该协议会有所帮助。OAuth 2.0 是一种授权协议。它允许用户以受控方式将资源的访问权限委托给其他服务,而无需透露任何其他详细信息,例如您的身份或任何其他信息。

That’s all a bit abstract, so let’s consider an example. You want to print some photos of your dog through a photo printing service, dogphotos.com. You sign up to the dogphotos.com service, and they give you two options for uploading your photos:
这一切都有点抽象,所以让我们考虑一个例子。您想通过照片打印服务打印一些您的狗的照片,dogphotos.com。您注册了 dogphotos.com 服务,他们为您提供了两种上传照片的选项:

• Upload from your computer.
从您的计算机上传。

• Download directly from Facebook using OAuth 2.0.
使用 OAuth 2.0 直接从 Facebook 下载。

As you’re using a new laptop, you haven’t downloaded all the photos of your dog to your computer, so you choose to use OAuth 2.0 instead, as shown in figure 25.5. This triggers the following sequence:
由于您使用的是新笔记本电脑,因此您尚未将狗的所有照片下载到计算机上,因此您选择使用 OAuth 2.0,如图 25.5 所示。这将触发以下序列:

  1. dogphotos.com redirects you to Facebook, where you must sign in (if you haven’t already).
    dogphotos.com 会将您重定向到 Facebook,您必须在此处登录(如果您尚未登录)。

  2. Once you’re authenticated, Facebook shows a consent screen, which describes the data dogphotos.com wants to access, which should be your photos only in this case.
    通过身份验证后,Facebook 会显示一个同意屏幕,其中描述了 dogphotos.com 想要访问的数据,在这种情况下,这些数据应该只是您的照片。

  3. When you choose OK, Facebook automatically redirects you to a URL on dogphotos.com and includes an authorization code in the URL.
    当您选择 OK (确定) 时,Facebook 会自动将您重定向到 dogphotos.com 上的 URL,并在 URL 中包含授权代码。

  4. dogphotos.com uses this code, in combination with a secret known only by Facebook and dogphotos.com, to retrieve an access token from Facebook.
    dogphotos.com 将此代码与只有 Facebook 和 dogphotos.com 知道的密钥结合使用,从 Facebook 检索访问令牌。

  5. Finally, dogphotos.com uses the token to call the Facebook API and retrieve your dog photos!
    最后,dogphotos.com 使用令牌调用 Facebook API 并检索您的狗照片!

alt text

Figure 25.5 Using OAuth 2.0 to authorize dogphotos.com to access your photos on Facebook
图 25.5 使用 OAuth 2.0 授权 dogphotos.com 访问您在 Facebook 上的照片

There’s a lot going on in this example, but it gives some nice benefits:
这个例子中有很多内容,但它提供了一些不错的好处:

• You didn’t have to give your Facebook credentials to dogphotos.com. You simply signed in to Facebook as normal.
您不必将您的 Facebook 凭据提供给 dogphotos.com。您只需照常登录 Facebook。

• You had control of which details dogphotos.com could access on your behalf via the Facebook photos API.
您可以控制 dogphotos.com 可以通过 Facebook 照片 API 代表您访问哪些详细信息。

• You didn’t have to give dogphotos.com any of your identity information (though in practice, this is often requested).
您不必向 dogphotos.com 提供任何身份信息(尽管在实践中,这经常被要求)。

Effectively, you delegated your access of the Facebook photos API to dogphotos.com. This approach is why OAuth 2.0 is described as an authorization protocol, not an authentication protocol. dogphotos.com doesn’t know your identity on Facebook; it is authorized only to access the photos API on behalf of someone.
实际上,您将 Facebook 照片 API 的访问权限委托给了 dogphotos.com。这种方法就是为什么 OAuth 2.0 被描述为授权协议,而不是身份验证协议的原因。dogphotos.com 不知道您在 Facebook 上的身份;它仅被授权代表某人访问 Photos API。

OAuth 2.0 authorization flows and grant types
OAuth 2.0 授权流程和授权类型

The OAuth 2.0 example shows in this section uses a common flow or grant type, as it’s called in OAuth 2.0, for obtaining a token from an identity provider. Oauth 2.0 defines several grant types and extensions, each designed for a different scenario:
本节中所示的 OAuth 2.0 示例使用通用流程或授权类型(在 OAuth 2.0 中称为)从身份提供商处获取令牌。Oauth 2.0 定义了多种授权类型和扩展,每种类型和扩展都针对不同的场景而设计:

• Authorization code—This is the flow I described in figure 25.5, in which an application uses the combination of an authorization code and a secret to retrieve a token.
授权码 - 这是我在图 25.5 中描述的流程,其中应用程序使用授权码和密钥的组合来检索令牌。

• Proof Key for Code Exchange (PKCE)—This is an extension to the authorization code that you should always favor, if possible, as it provides additional protections against certain attacks, as described in the RFC at https://www.rfc-editor.org/rfc/rfc7636.
代码交换证明密钥 (PKCE) - 这是授权码的扩展,如果可能,您应该始终使用该扩展,因为它提供了针对某些攻击的额外保护,如 RFC https://www.rfc-editor.org/rfc/rfc7636 中所述。

• Client credentials—This is used when no user is involved, such as when you have an API talking to another API.
客户端凭据 - 当不涉及用户时 (例如,当 API 与其他 API 通信时) 使用此凭据。

Many more grants are available (see https://oauth.net/2/grant-types), and each grant is suited to a different situation. The examples are the most common types, but if your scenario doesn’t match these, it’s worth exploring the other OAuth 2.0 grants available before thinking you need to invent your own! And with Oauth 2.1 coming soon (http://mng.bz/XNav), there may well be updated guidance to be aware of.
还有更多的资助金可供选择(见 https://oauth.net/2/grant-types),每种资助金都适用于不同的情况。这些示例是最常见的类型,但如果您的方案与这些不匹配,则值得先探索其他可用的 OAuth 2.0 授权,然后再考虑您需要创建自己的授权!随着 Oauth 2.1 的即将推出 (http://mng.bz/XNav),可能会有更新的指南需要注意

OAuth 2.0 is great for the scenario I’ve described so far, in which you want to delegate access to a resource (your photos) to someone else (dogphotos.com). But it’s also common for apps to want to know your identity in addition to accessing an API. For example, dogphotos.com may want to be able to contact you via Facebook if there’s a problem with your photos.
OAuth 2.0 非常适合我到目前为止描述的方案,在该方案中,您希望将对资源(您的照片)的访问权限委派给其他人 (dogphotos.com)。但是,除了访问 API 之外,应用程序还想知道您的身份也很常见。例如,如果您的照片有问题,dogphotos.com 可能希望能够通过 Facebook 与您联系。

This is where OpenID Connect comes in. OpenID Connect takes the same basic flows as OAuth 2.0 and adds some conventions, discoverability, and authentication. At a high level, OpenID Connect treats your identity (such as an ID or email address) as a resource that is protected in the same way as any other API. You still need to consent to give dogphotos.com access to your identity details, but once you do, it’s an extra API call for dogphotos.com to retrieve your identity details, as shown in figure 25.6.
这就是 OpenID Connect 的用武之地。OpenID Connect 采用与 OAuth 2.0 相同的基本流程,并添加了一些约定、可发现性和身份验证。概括地说,OpenID Connect 将您的身份(例如 ID 或电子邮件地址)视为一种资源,其保护方式与任何其他 API 相同。您仍然需要同意才能授予 dogphotos.com 访问您的身份详细信息的权限,但是一旦同意,dogphotos.com 将进行额外的 API 调用来检索您的身份详细信息,如图 25.6 所示。

alt text

Figure 25.6 Using OpenID Connect to authenticate with Facebook and retrieve identity information. The overall flow is the same as with Oauth 2.0, as shown in figure 25.5, but with an additional identity token describing the authentication event and API call to retrieve the identity details.
图 25.6 使用 OpenID Connect 向 Facebook 进行身份验证并检索身份信息。整个流程与 Oauth 2.0 相同,如图 25.5 所示,但使用一个额外的身份令牌来描述身份验证事件和 API 调用来检索身份详细信息。

OpenID Connect is a crucial authentication component in many systems, but if you’re building the API only (for example, the Facebook photos API from figures 25.5 and 25.6), all you really care about are the tokens in the requests; how that token was obtained is less important from a technical standpoint. In the next section we’ll look in detail at these tokens and how they work.
OpenID Connect 是许多系统中的关键身份验证组件,但如果您只构建 API(例如,图 25.5 和 25.6 中的 Facebook 照片 API),那么您真正关心的只是请求中的令牌;从技术角度来看,该代币是如何获得的并不重要。在下一节中,我们将详细介绍这些令牌及其工作原理。

25.2 Understanding bearer token authentication

25.2 了解持有者令牌身份验证

In this section you’ll learn about bearer tokens: what they are, how they can be used for security with APIs, and the common JWT format for tokens. You’ll learn about some of the limitations of the tokens, approaches to work around these, and some common concepts such as audiences and scopes.
在本节中,您将了解不记名令牌:它们是什么,如何通过 API 使用它们来确保安全性,以及令牌的常见 JWT 格式。您将了解令牌的一些限制、解决这些问题的方法,以及一些常见概念,例如受众和范围。

The name bearer token consists of two parts that describe its use:
名称持有者令牌由描述其用途的两个部分组成:

• Token—A security token is a string that provides access to a protected resource.
令牌 - 安全令牌是提供对受保护资源的访问权限的字符串。

• Bearer—A bearer token is one in which anyone who has the token (the bearer) can use it like anyone else. You don’t need to prove that you were the one who received the token originally or have access to any additional key. You can think of a bearer token as being a bit like money: if it’s in your possession, you can spend it!
Bearer - 在“不记名令牌”中,任何拥有该令牌 (“Bearer”) 的人都可以像其他任何人一样使用它。您无需证明您是最初接收令牌的人,也无需证明您有权访问任何其他密钥。您可以将不记名代币想象成有点像金钱:如果您拥有它,您就可以花掉它!

If the second point makes you a little uneasy, that’s good. You should think of bearer tokens as being a lot like passwords: you must protect them at all costs! You should avoid including bearer tokens in URL query strings, for example, as these may be automatically logged, exposing the token accidentally.
如果第二点让你有点不安,那很好。您应该将不记名令牌视为很像密码:您必须不惜一切代价保护它们!例如,您应该避免在 URL 查询字符串中包含不记名令牌,因为这些令牌可能会被自动记录,从而意外地暴露令牌。

Everything old is new again: Cookies for APIs
旧事物又是新的:API的 Cookie

Bearer token authentication is extremely common for APIs, but as with everything in tech, the landscape is constantly evolving. One area that has seen a lot of change is the process of securing SPAs like React, Angular, and Blazor WASM. The advice for some years was to use the Authorization code with PKCE grant (https://www.rfc-editor.org/rfc/rfc8252#section-6), but the big problem with this pattern is that the bearer tokens for calling the API are ultimately stored in the browser.
不记名令牌身份验证对于 API 来说极为常见,但与技术领域的一切一样,形势也在不断发展。一个发生很大变化的领域是保护 React、Angular 和 Blazor WASM 等 SPA 的过程。几年来,人们的建议是将授权码与 PKCE grant (https://www.rfc-editor.org/rfc/rfc8252#section-6) 一起使用,但这种模式的最大问题是,用于调用 API 的不记名令牌最终存储在浏览器中。

An alternative pattern has emerged recently: the Backend for Frontend (BFF) pattern. In this approach, you have a traditional ASP.NET Core application (the backend, which hosts the Blazor WASM or other SPA application (the frontend). The main job of the ASP.NET Core application is to handle OpenID Connect authentication, store the bearer tokens securely, and set an authentication cookie, exactly like a traditional web app.
最近出现了另一种模式:Backend for Frontend (BFF) 模式。在此方法中,你有一个传统的 ASP.NET Core 应用程序(后端,托管 Blazor WASM 或其他 SPA 应用程序(前端)。ASP.NET Core 应用程序的主要工作是处理 OpenID Connect 身份验证,安全地存储持有者令牌,并设置身份验证 Cookie,就像传统的 Web 应用程序一样。

The frontend app in the browser sends requests to the backend app, which automatically includes the cookie. The backend swaps out the authentication cookie for the appropriate bearer token and forwards the request to the real API.
浏览器中的前端应用程序将请求发送到后端应用程序,后端应用程序会自动包含 Cookie。后端将身份验证 Cookie 换成相应的不记名令牌,并将请求转发到实际 API。

The big advantages of this approach are that no bearer tokens are ever sent to the browser, and much of the frontend code is significantly simplified. The main down side is that you need to run the additional backend service to support the frontend app. Nevertheless, this is quickly becoming the recommended approach. You can read more about the pattern in Duende’s documentation at http://mng.bz/yQdB. Alternatively, you can find a project template for the BFF pattern from Damien Boden at http://mng.bz/MBlW.
这种方法的一大优点是不会向浏览器发送不记名令牌,并且大部分前端代码都得到了显著简化。主要缺点是您需要运行额外的后端服务来支持前端应用程序。尽管如此,这正迅速成为推荐的方法。您可以在 Duende 的文档 http://mng.bz/yQdB 中阅读有关该模式的更多信息。或者,您可以在 http://mng.bz/MBlW 上找到 Damien Boden 提供的 BFF 模式的项目模板。

Bearer tokens don’t have to have any particular value; they could be a completely random string, for example. However, the most common format and the format used by OpenID Connect is a JWT. JWTs (defined in https://www.rfc-editor.org/rfc/rfc7519.html) consist of three parts:
不记名令牌不必具有任何特定值;例如,它们可以是一个完全随机的字符串。但是,最常见的格式和 OpenID Connect 使用的格式是 JWT。JWT(在 https://www.rfc-editor.org/rfc/rfc7519.html 中定义)由三个部分组成:

• A JavaScript Object Notation (JSON) header describing the token
描述令牌的 JavaScript 对象表示法 (JSON) 标头

• A JSON payload containing the claims
包含声明的 JSON 有效负载

• A binary signature created from the header and the payload
从标头和有效负载创建的二进制签名

Each part is base64-encoded and concatenated with a '.' into a single string that can be safely passed in HTTP headers, for example, as shown in figure 25.7. The signature is created using key material that must be shared by the provider that created the token and any API that consumes it. This ensures that the JWT can’t be tampered with, such as to add extra claims to a token.
每个部分都经过 base64 编码,并用 '.' 连接成一个字符串,该字符串可以在 HTTP 标头中安全地传递,例如,如图 25.7 所示。签名是使用密钥材料创建的,该密钥材料必须由创建令牌的提供商和使用令牌的任何 API 共享。这可确保 JWT 无法被篡改,例如向令牌添加额外的声明。

WARNING Always validate the signature of any JWTs you consume, as described in the JWT Best Current Practices RFC (https://www.rfc-editor.org/rfc/rfc8725). ASP.NET Core does this by default.
警告:始终验证您使用的任何 JWT 的签名,如 JWT 当前最佳实践 RFC (https://www.rfc-editor.org/rfc/rfc8725) 中所述。默认情况下,ASP.NET Core 执行此作。

alt text

Figure 25.7 An example JWT, decoded using the website https://jwt.io. The JWT consists of three parts: the header, the payload, and the signature. You must always verify the signature of any JWTs you receive.
图 25.7 使用 website https://jwt.io 解码的 JWT 示例JWT 由三部分组成:标头、有效负载和签名。您必须始终验证您收到的任何 JWT 的签名。

Figure 25.7 shows the claims included in the JWT, some of which have cryptic names like iss and iat. These are standard claim names used in OpenID Connect (standing for “Issuer” and “Issued at,” respectively). You generally don’t need to worry about these, as they’re automatically handled by ASP.NET Core when it decodes the token. Nevertheless, it’s helpful to understand what some of these claims mean, as it will help when things go wrong:
图 25.7 显示了 JWT 中包含的声明,其中一些声明具有晦涩难懂的名称,如 iss 和 iat。这些是 OpenID Connect 中使用的标准声明名称(分别代表“颁发者”和“颁发者”)。您通常无需担心这些,因为它们由 ASP.NET Core 在解码令牌时自动处理。尽管如此,了解其中一些索赔的含义会有所帮助,因为当出现问题时它会有所帮助:

• sub—The subject of the token, the unique identifier of the subject it’s describing. This will often be a user, in which case it may be the identity provider’s unique ID for the user.
sub - 令牌的主题,即它所描述的主题的唯一标识符。这通常是一个用户,在这种情况下,它可能是身份提供商的用户唯一 ID。

• aud—The audience of the token, specifying the domains for which this token was created. When an API validates the token, the API should confirm that the JWT’s aud claim contains the domain of the API.
aud - 令牌的受众,指定为其创建此令牌的域。当 API 验证令牌时,API 应确认 JWT 的 aud 声明包含 API 的域。

• scope—The scopes granted in the token. Scopes define what the user/app consented to (and is allowed to do). Taking the example from section 25.1, dogphotos.com may have requested the photos.read and photos.edit scopes, but if the user consented only to the photos.read scope, the photos.edit scope would not be in the JWT it receives for use with the Facebook photos API. It’s up to the API itself to interpret what each scope means for the business logic of the request.
scope - 令牌中授予的范围。范围定义用户/应用程序同意 (和允许) 执行的作。以第 25.1 节为例,dogphotos.com 可能已经请求了photos.read和photos.edit范围,但是如果用户只同意photos.read范围,则photos.edit范围将不在它收到的用于 Facebook 照片 API 的 JWT 中。由 API 本身来解释每个范围对请求的业务逻辑的含义。

• exp—The expiration time of the token, after which it is no longer valid, expressed as the number of seconds since midnight on January 1, 1970 (known as the Unix timestamp).
exp - 令牌的过期时间 (超过此时间后) 不再有效,表示为自 1970 年 1 月 1 日午夜以来的秒数 (称为 Unix 时间戳)。

An important point to realize is that JWTs are not encrypted. That means anyone can read the contents of a JWT by default. Another standard, JSON Web Encryption (JWE), can be used to wrap a JWT in an encrypted envelope that can’t be read unless you have the key. Many identity providers include support for using JWEs with nested JWTs, and ASP.NET Core includes support for both out of the box, so it’s something to consider.
需要注意的一个重要点是 JWT 未加密。这意味着默认情况下,任何人都可以读取 JWT 的内容。另一个标准 JSON Web 加密 (JWE) 可用于将 JWT 包装在加密信封中,除非您拥有密钥,否则无法读取该信封。许多身份提供商都支持将 JWE 与嵌套的 JWT 一起使用,而 ASP.NET Core 也支持开箱即用,因此需要考虑这一点。

Bearer tokens, access tokens, reference tokens, oh my!
不记名令牌、访问令牌、引用令牌,天哪!

The concept of a bearer token described in this section is a generic idea that can be used in several ways and for different purposes. You’ve already read about access tokens and identity tokens used in OpenID Connect. These are both bearer tokens; their different names describe the purpose of the token.
本节中描述的 bearer token 的概念是一个通用概念,可以以多种方式用于不同的目的。您已经阅读了 OpenID Connect 中使用的访问令牌和身份令牌。这些都是不记名令牌;它们的不同名称描述了令牌的用途。

The following list describes some of the types of tokens you might read about or run into:
以下列表描述了您可能会阅读或遇到的一些令牌类型:

• Access token—Access tokens are used to authorize access to a resource. These are the tokens typically referred to when you talk about bearer authentication. They come in two flavors:
访问令牌 - 访问令牌用于授权访问资源。这些是您在谈论不记名身份验证时通常提到的令牌。它们有两种口味:

Self-contained—These are the most common tokens, with JWT as the most common format. They contain metadata, claims, and a signature. The strength of self-contained tokens—that they contain all the data and can be validated offline—is also their weakness, as they can’t be revoked. Due to this, they typically have a limited valid lifespan. They can also become large if they contain many claims, which increases request sizes.
自包含 — 这些是最常见的令牌,其中 JWT 是最常见的格式。它们包含元数据、声明和签名。自包含令牌的优势(它们包含所有数据并且可以离线验证)也是它们的弱点,因为它们无法撤销。因此,它们的有效寿命通常有限。如果它们包含许多声明,它们也会变得很大,这会增加请求大小。

Reference token—These don’t contain any data and are typically a random string. When a protected API receives a reference token, it must exchange the reference token with the identity provider for the claims (for example, a JWT). This approach ensures more privacy, as the claims are never exposed to the client, and the token can be revoked at the identity provider. However, it requires an extra HTTP round trip every time the API receives a request. This makes reference tokens a good option for high-security environments, where the performance effect is less critical.
引用令牌 – 这些不包含任何数据,通常是随机字符串。当受保护的 API 收到引用令牌时,它必须与身份提供商交换引用令牌以获取声明(例如 JWT)。此方法可确保更多隐私,因为声明永远不会向客户端公开,并且可以在身份提供商处撤销令牌。但是,每次 API 收到请求时,它都需要额外的 HTTP 往返。这使得 reference tokens 成为高安全性环境的不错选择,因为在这种环境中,性能影响不太重要。

• ID token—This token is used in OpenID Connect (http://mng.bz/a1M7) to describe an authentication event. It may contain additional claims about the authenticated user, but this is not required; if the claims aren’t provided in the ID token, they can be retrieved from the identity provider’s UserInfo endpoint. The ID token is always a JWT, but you should never send it to other APIs; it is not an access token. The ID token can also be used to log out the user at the identity provider.
ID 令牌 - 此令牌在 OpenID Connect (http://mng.bz/a1M7) 中用于描述身份验证事件。它可能包含有关经过身份验证的用户的其他声明,但这不是必需的;如果 ID 令牌中未提供声明,则可以从身份提供商的 UserInfo 终端节点检索它们。ID 令牌始终是 JWT,但您绝不应将其发送到其他 API;它不是访问令牌。ID 令牌还可用于在身份提供商处注销用户。

• Refresh token—For security reasons, access tokens typically have relatively short lifetimes, sometimes as low as 5 minutes. After this time, the access token is no longer valid, and you need to retrieve a new one. Making users log in to their identity provider every 5 minutes is clearly a bad experience, so as part of the OAuth or OpenID Connect flow you can also request a refresh token.
刷新令牌 - 出于安全原因,访问令牌的生命周期通常相对较短,有时低至 5 分钟。在此时间之后,访问令牌不再有效,您需要检索新的访问令牌。让用户每 5 分钟登录一次身份提供商显然是一种糟糕的体验,因此作为 OAuth 或 OpenID Connect 流程的一部分,您还可以请求刷新令牌。

When an access token expires, you can send the refresh token to an identity provider, and it returns a new access token without the user’s needing to log in again. The power to obtain valid access tokens means that it’s critical to protect refresh tokens; should an attacker obtain a refresh token, they effectively have the power to impersonate a user.
当访问令牌过期时,您可以将刷新令牌发送给身份提供商,它会返回新的访问令牌,而无需用户再次登录。获取有效访问令牌的能力意味着保护刷新令牌至关重要;如果攻击者获取了刷新令牌,他们实际上就有能力模拟用户。

In most of your work building and interacting with APIs, you’ll likely be using self-contained JWT access tokens. These are what I’m primarily referring to in this chapter whenever I mention bearer tokens or bearer authentication.
在构建 API 和与 API 交互的大部分工作中,您可能会使用自包含的 JWT 访问令牌。这些是我在本章中提到 bearer tokens 或 bearer authentication 时主要引用的内容。

Now you know what a token is, as well as how they’re issued by identity providers using the OpenID Connect and OAuth 2.0 protocols. Before we get to some code in section 25.3, we’ll see what a typical authentication flow looks like for an ASP.NET Core API app using JWT bearer tokens for authentication.
现在,您知道什么是令牌,以及身份提供商如何使用 OpenID Connect 和 OAuth 2.0 协议颁发令牌。在我们进入第 25.3 节中的一些代码之前,我们将了解使用 JWT 不记名令牌进行身份验证的 ASP.NET Core API 应用程序的典型身份验证流程是什么样的。

At a high level, authenticating using bearer tokens is identical to authenticating using cookies for a traditional app that has already authenticated, which you saw in figure 25.1. The request to the API contains the bearer token in a header. Any middleware before the authentication middleware sees the request as unauthenticated, exactly the same as for cookie authentication, as shown in figure 25.8.
在高级别上,使用 bearer tokens 进行身份验证与使用 cookie 对已经进行身份验证的传统应用程序相同,如图 25.1 所示。对 API 的请求在标头中包含不记名令牌。身份验证中间件之前的任何中间件都将请求视为未经身份验证,这与 cookie 身份验证完全相同,如图 25.8 所示。

alt text

Figure 25.8 When an API request contains a bearer token, the token is validated and deserialized by the authentication middleware. The middleware creates a ClaimsPrincipal from the token, optionally transforming it with additional claims, and sets the HttpContext.User property. Subsequent middleware sees the request as authenticated.
图 25.8 当 API 请求包含不记名令牌时,身份验证中间件会验证和反序列化该令牌。中间件从令牌创建 ClaimsPrincipal,可以选择使用其他声明对其进行转换,并设置 HttpContext.User 属性。后续中间件将请求视为已验证。

Things are a bit different in the AuthenticationMiddleware. Instead of deserializing a cookie containing the ClaimsPrincipal, the middleware decodes the JWT token in the Authorization header. It validates the signature using the signing keys from the identity provider, and verifies that the audience has the expected value and that the token has not expired.
AuthenticationMiddleware 中的情况略有不同。中间件不是反序列化包含 ClaimsPrincipal 的 Cookie,而是解码 Authorization 标头中的 JWT 令牌。它使用来自身份提供商的签名密钥验证签名,并验证受众是否具有预期值以及令牌是否未过期。

If the token is valid, the authentication middleware creates a ClaimsPrincipal representing the authenticated request and sets it on HttpContext.User. All middleware after the authentication middleware sees the request as authenticated.
如果令牌有效,则身份验证中间件将创建一个 ClaimsPrincipal,表示经过身份验证的请求,并在 HttpContext.User 上设置它。身份验证中间件之后的所有中间件都将请求视为已验证。

TIP If the claims in the token don’t match the key values you’re expecting, you can use claims transformation to remap claims. This applies to cookie authentication too, but it’s particularly common when you’re receiving tokens from third-party identity providers, where you don’t control the names of claims. You can also use this approach to add extra claims for a user, which weren’t in the original token. To learn more about claims transformation, see http://mng.bz/gBJV.
提示:如果令牌中的声明与预期的键值不匹配,则可以使用声明转换来重新映射声明。这也适用于 Cookie 身份验证,但当您从第三方身份提供商接收令牌时,这种情况尤其常见,因为您无法控制声明的名称。您还可以使用此方法为用户添加原始令牌中没有的额外声明。要了解有关声明转换的更多信息,请参阅 http://mng.bz/gBJV

We’ve covered a lot of theory about JWT tokens in this chapter, so you’ll be pleased to hear it’s time to look at some code!
在本章中,我们已经介绍了许多关于 JWT 令牌的理论,因此您会很高兴听到是时候查看一些代码了!

25.3 Adding JWT bearer authentication to minimal APIs

25.3 将 JWT 不记名身份验证添加到最小 API

In this section you’ll learn how to add JWT bearer token authentication to an ASP.NET Core app. I use the minimal API Recipe API application we started in chapter 12 in this chapter, but the process is identical if you’re building an API application using web API controllers.
在本节中,您将了解如何将 JWT 不记名令牌身份验证添加到 ASP.NET Core 应用程序。我使用我们在本章第 12 章中开始的最小 API Recipe API 应用程序,但如果您使用 Web API 控制器构建 API 应用程序,则过程是相同的。

.NET 7 significantly simplified the number of steps you need to get started with JWT authentication by adding some conventions, which we’ll discuss shortly. To add JWT to an existing API application, first install the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package using the .NET CLI
.NET 7 通过添加一些约定,大大简化了开始使用 JWT 身份验证所需的步骤数,我们稍后将对此进行讨论。要将 JWT 添加到现有 API 应用程序,请首先使用 .NET CLI 安装 Microsoft.AspNetCore.Authentication.JwtBearer NuGet 包

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

or by adding the to your project directly:
或者直接将 添加到您的项目中:

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer"
    Version="7.0.0" />

Next, add the required services to configure JWT authentication for your application, as shown in listing 25.1. As you may remember, the authentication and authorization middleware are automatically added to your middleware pipeline by WebApplication, but if you want to control the position of the middleware, you can override the location, as I do here.
接下来,添加所需的服务来为您的应用程序配置 JWT 身份验证,如清单 25.1 所示。您可能还记得,身份验证和授权中间件由 WebApplication 自动添加到您的中间件管道中,但如果您想控制中间件的位置,您可以覆盖该位置,就像我在这里所做的那样。

Listing 25.1 Adding JWT bearer authentication to a minimal API application
示例 25.1 向最小 API 应用程序添加 JWT 不记名身份验证

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication() ❶
.AddJwtBearer(); ❷
builder.Services.AddAuthorization(); ❸
builder.Services.AddScoped<RecipeService>();
WebApplication app = builder.Build();
app.UseAuthentication(); ❹
app.UseAuthorization(); ❺
app.MapGet("/recipe", async (RecipeService service) =>
{
return await service.GetRecipes();
}).RequireAuthorization(); ❻
app.Run();

❶ Adds the core authentication services
添加核心身份验证服务

❷ Adds and configures JWT authentication
添加和配置 JWT 身份验证

❸ Adds the core authorization services
添加核心授权服务

❹ Adds the authentication middleware
添加身份验证中间件

❺ Adds the authorization middleware
添加授权中间件

❻ Adds an authorization policy to the minimal API endpoint
将授权策略添加到最小 API 端点

As well as configuring the JWT authentication, listing 25.1 adds an authorization policy to the one minimal API endpoint shown in the app. The RequireAuthorization() function adds a simple “Is authenticated” authorization policy to the endpoint. This is exactly analgous to when you add an [Authorize] attribute to MVC or Web API controllers. Any requests for this endpoint must be authenticated; otherwise, the request is rejected by the authorization middleware with a 401 Unauthorized reponse, as shown in figure 25.9.
除了配置 JWT 身份验证之外,清单 25.1 还向应用程序中显示的一个最小 API 端点添加了一个授权策略。RequireAuthorization() 函数向终端节点添加一个简单的 “Is authenticated” 授权策略。这与向 MVC 或 Web API 控制器添加 [Authorize] 属性时完全相同。对此终端节点的任何请求都必须进行身份验证;否则,授权中间件会拒绝该请求,并给出 401 Unauthorized 响应,如图 25.9 所示。

alt text

Figure 25.9 If you send a request to an API protected with JWT bearer authentication and don’t include a token, you’ll receive a 401 Unauthorized challenge response.
图 25.9 如果您向受 JWT 不记名身份验证保护的 API 发送请求,并且不包含令牌,您将收到 401 Unauthorized 质询响应。

Authentication schemes: Choosing between cookies and bearer tokens
身份验证方案:在 Cookie 和 Bearer Tokens之间进行选择

One question you may have while reading about bearer authentication is how the authentication middleware knows whether to look for the cookie or a header. The answer is authentication schemes.
在阅读有关Bearer Authentication的文章时,您可能会遇到一个问题,即身份验证中间件如何知道是查找 Cookie 还是标头。答案是身份验证方案。

An authentication scheme in ASP.NET Core has an ID and an associated authentication handler that controls how the user is authenticated, as well as how authentication and authorization failures should be handled.
ASP.NET Core 中的身份验证方案具有一个 ID 和一个关联的身份验证处理程序,用于控制如何对用户进行身份验证,以及应如何处理身份验证和授权失败。

For example, in chapter 23 the cookie authentication scheme was used implicitly by ASP.NET Core Identity. The cookie authentication handler in this case authenticates users by looking for a cookie and redirects users to the login or “access denied” pages for authentication or authorization failures.
例如,在第 23 章中,ASP.NET Core Identity 隐式使用了 cookie 身份验证方案。在这种情况下,Cookie 身份验证处理程序通过查找 Cookie 来验证用户,并将用户重定向到登录页或“拒绝访问”页,以查找验证或授权失败。

In listing 25.1 you registered the JWT Bearer authentication scheme. The JWT bearer authentication handler reads tokens from the Authorization header and returns 401 and 403 responses for authentication or authorization failures.
在列表 25.1 中,您注册了 JWT Bearer 身份验证方案。JWT 不记名身份验证处理程序从 Authorization 标头中读取令牌,并返回 401 和 403 身份验证或授权失败的响应。

When you register only a single authentication scheme, such as in listing 25.1, ASP.NET Core automatically sets that as the default, but it’s possible to register multiple authentication schemes. This is particularly common if you are using OpenID Connect with a traditional web app, for example. In these cases you can choose which scheme is used for authentication events or authentication failures and how the schemes should interact.
当您仅注册单个身份验证方案时,例如清单 25.1 中,ASP.NET Core 会自动将其设置为默认值,但可以注册多个身份验证方案。例如,如果您将 OpenID Connect 与传统 Web 应用程序一起使用,这种情况尤其常见。在这些情况下,您可以选择将哪个方案用于身份验证事件或身份验证失败,以及这些方案应如何交互。

Using multiple authentication schemes can be confusing, so it’s important to follow the documentation closely when configuring authentication for your app. You can read more about authentication schemes at http://mng.bz/5w1a. If you need only a single scheme, you shouldn’t have any problems, but otherwise, here be dragons!
使用多个身份验证方案可能会造成混淆,因此在为应用程序配置身份验证时,请务必严格遵循文档。您可以在 http://mng.bz/5w1a 上阅读有关身份验证方案的更多信息。如果你只需要一个方案,你应该不会有任何问题,但除此之外,这里有龙!

Great! The 401 response in figure 25.9 verifies that the app is behaving correctly for unauthenticated requests. The obvious next step is to send a request to your API that includes a valid JWT bearer token. Unfortunately, this is where things traditionally get tricky. How do you generate a valid JWT? Luckily, in .NET 7, the .NET CLI comes with a tool to make creating test tokens easy.
伟大!图 25.9 中的 401 响应验证了应用程序是否对未经身份验证的请求行为正确。显而易见的下一步是向 API 发送包含有效 JWT 不记名令牌的请求。不幸的是,这是传统上事情变得棘手的地方。如何生成有效的 JWT?幸运的是,在 .NET 7 中,.NET CLI 附带了一个工具,可以轻松创建测试令牌。

25.4 Using the user-jwts tool for local JWT testing

25.4 使用 user-jwts 工具进行本地 JWT 测试

In section 25.3 you added JWT authentication to your application and protected your API with a basic authorization policy. The problem is that you can’t test your API unless you can generate JWT tokens. In production you’ll likely have an identity provider such as Auth0, Azure Active Directory, or IdentityServer to generate tokens for you using OpenID Connect. But that can make for cumbersome local testing. In this section you’ll learn how to use the .NET CLI to generate JWTs for local testing.
在第 25.3 节中,您向应用程序添加了 JWT 身份验证,并使用基本授权策略保护您的 API。问题是,除非可以生成 JWT 令牌,否则无法测试 API。在生产环境中,您可能会有一个身份提供商(如 Auth0、Azure Active Directory 或 IdentityServer)来使用 OpenID Connect 为您生成令牌。但这可能会导致本地测试变得繁琐。在本节中,您将学习如何使用 .NET CLI 生成用于本地测试的 JWT。

In .NET 7, the .NET CLI includes a tool called user-jwts that you can use to generate tokens. This tool acts as a mini identity provider, meaning that you can generate tokens with any claims you may need, and your API can verify them using signing key material generated by the tool.
在 .NET 7 中,.NET CLI 包括一个名为 user-jwts 的工具,您可以使用它来生成令牌。此工具充当微型身份提供商,这意味着您可以使用可能需要的任何声明生成令牌,并且您的 API 可以使用该工具生成的签名密钥材料对其进行验证。

TIP The user-jwts tool is built into the software development kit (SDK), so there’s nothing extra to install. You need to enable User Secrets for your project, but user-jwts will do this for you if you haven’t already. The user-jwts tool uses User Secrets to store the signing key material used to generate the JWTs, which your app uses to validate the JWT signatures.
提示: user-jwts 工具内置于软件开发工具包 (SDK) 中,因此无需安装任何额外内容。您需要为项目启用 User Secrets,但如果您尚未启用 user-jwts,则 user-jwts 将为您执行此作。user-jwts 工具使用用户密钥来存储用于生成 JWT 的签名密钥材料,您的应用程序可以使用该材料来验证 JWT 签名。

Let’s look at how to create a JWT with the user-jwts tool and use that to send a request to our application.
让我们看看如何使用 user-jwts 工具创建 JWT,并使用它向我们的应用程序发送请求。

25.4.1 Creating JWTs with the user-jwts tool

25.4.1 使用 user-jwts 工具创建 JWT

To create a JWT that you can use in requests to your API, run the following with the user-jwts tool from inside your project folder:
要创建可在 API 请求中使用的 JWT,请使用 user-jwts 工具从项目文件夹内运行以下命令:

dotnet user-jwts create

This command does several things:
此命令执行以下几项作:

• Enables User Secrets in the project if they’re not already configured, as though you had manually run dotnet user-secrets init.
如果尚未配置用户密钥,请在项目中启用用户密钥,就像您手动运行 dotnet user-secrets init 一样。

• Adds the signing key material to User Secrets, which you can view by running dotnet user-secrets list as described in chapter 10, which prints out the key material configuration, as in this example:
将签名密钥材料添加到用户密钥中,您可以通过运行 dotnet user-secrets list 来查看,如第 10 章所述,该列表将打印出密钥材料配置,如下例所示:

Authentication:Schemes:Bearer:SigningKeys:0:Value =
    rIhUzB3DIbtbUwiIxkgoKfFDkLpY+gIJOB4eaQzczq8=
Authentication:Schemes:Bearer:SigningKeys:0:Length = 32
Authentication:Schemes:Bearer:SigningKeys:0:Issuer = dotnet-user-jwts
Authentication:Schemes:Bearer:SigningKeys:0:Id = c99a872d

• Configures the JWT authentication services to support tokens generated by the user-jwts tool by adding configuration to appsettings.Development.json, as follows:
通过向 appsettings 添加配置,配置 JWT 身份验证服务以支持 user-jwts 工具生成的令牌。Development.json,如下所示:

{
  "Authentication": {
    "Schemes": {
      "Bearer": {
        "ValidAudiences": [
          "http://localhost:5073",
          "https://localhost:7112"
        ],
        "ValidIssuer": "dotnet-user-jwts"
      }
    }
  }
}

The user-jwts tool automatically configures the valid audiences based on the profiles in your launchSettings.json file. All the applicationUrls listed in launchSettings.json are listed as valid audiences, so it doesn’t matter which profile you use to run your app; the generated token should be valid. The JWT bearer authentication service automatically reads this configuration and configures itself to support user-jwts JWTs.
user-jwts 工具会根据 launchSettings.json 文件中的用户档案自动配置有效受众。launchSettings.json中列出的所有 applicationUrls 都列为有效受众,因此使用哪个配置文件来运行应用程序并不重要;生成的 Token 应该是有效的。JWT 不记名身份验证服务会自动读取此配置,并将自身配置为支持 user-jwts JWT。

• Creates a JWT. By default, the token is created with a sub and unique_claim set to your operating system’s username, with aud claims for each of the applicationUrls in your launchSettings.json and an issuer of dotnet-user-jwts. You’ll notice that these match the values added to your APIs configuration file.
After calling dotnet user-jwts create, the JWT token is printed to the console, along with the sub name used and the ID of the token. I’ve truncated the tokens throughout this chapter for brevity:
创建 JWT。默认情况下,令牌是使用 sub 创建的,unique_claim设置为作系统的用户名,launchSettings.json中的每个 applicationUrls 都有 aud 声明,并且颁发者是 dotnet-user-jwts。您会注意到,这些值与添加到 API 配置文件的值匹配。
调用 dotnet user-jwts create 后,JWT 令牌以及使用的子名称和令牌的 ID 将打印到控制台。为简洁起见,我在本章中截断了标记:

New JWT saved with ID 'f2080e51'.
Name: andrewlock

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFuZHJl…

TIP You can visualize exactly what’s in the token by copy and pasting it into https://jwt.io, as I showed in figure 25.7.
提示:您可以通过将令牌复制并粘贴到 https://jwt.io 中来准确可视化令牌中的内容,如图 25.7 所示。

Now that you have a token, it’s time to test it. To use the token, you need to add an Authorization header to requests using the following format (where <token> is the full token printed by user-jwts):
现在您有了一个令牌,是时候测试它了。要使用该令牌,您需要使用以下格式向请求添加 Authorization 标头(其中<token> 是 user-jwts 打印的完整令牌):

Authorization: Bearer <token>

If any part of this header is incorrect—if you misspell Authorization, misspell Bearer, don’t include a space between Bearer and your token, or mistype your token—you’ll get a 401 Unauthorized response.
如果此标头的任何部分不正确(如果拼写错误 Authorization、拼写错误 Bearer、在 Bearer 和令牌之间不包含空格或键入错误令牌),您将收到 401 Unauthorized 响应。

TIP If you get 401 Unauthorized responses even after adding an Authorization header to your requests, double-check your spelling, and make sure that the token is added correct with the "Bearer " prefix. Typos have a way of creeping in here! You can also increase the logging level in your API to see why failures are happening, as you’ll learn in chapter 26.
提示:如果在向请求添加 Authorization 标头后仍收到 401 Unauthorized 响应,请仔细检查您的拼写,并确保使用“Bearer”前缀正确添加令牌。错别字总有办法悄悄溜进来!您还可以提高 API 中的日志记录级别,以查看失败发生的原因,您将在第 26 章中学到。

Once you have added the token you can call your API, which should now return successfully, as shown in figure 25.10.
添加令牌后,您可以调用您的 API,现在应该成功返回,如图 25.10 所示。

alt text

Figure 25.10 Sending a request with an Authorization Bearer using Postman. The Authorization header must have the format Bearer <token>. You can also configure this in the Authorization tab of Postman.
图 25.10 使用 Postman 向授权持有者发送请求。Authorization 标头必须具有 Bearer <token>格式。您还可以在 Postman 的 Authorization (授权) 选项卡中配置此项。

The default token created by the JWT is sufficient to authenticate with your API, but depending on your requirements, you may want to customize the JWT to add or change claims. In the next section you’ll learn how.
JWT 创建的默认令牌足以使用 API 进行身份验证,但根据您的要求,您可能需要自定义 JWT 以添加或更改声明。在下一节中,您将了解如何作。

25.4.2 Customizing your JWTs

25.4.2 自定义 JWT

By default, the user-jwts tool creates a bare-bones JWT that you can use to call your app. If you need more customization, you can pass extra options to the dotnet user-jwts create command to control the JWT it generates. Some of the most useful options are
默认情况下,user-jwts 工具会创建一个基本 JWT,您可以使用它来调用您的应用程序。如果需要更多自定义,可以将额外的选项传递给 dotnet user-jwts create 命令,以控制它生成的 JWT。一些最有用的选项是

• --name sets the sub and unique_name claims for the JWT instead of using the operating system user as the name.
--name 设置 JWT 的 sub 和 unique_name 声明,而不是使用作系统用户作为名称。

• --claim <key>=<value> adds a claim called <key> with value <value> to the JWT. Use this option multiple times to add claims.
--claim <key>=<value> 将调用<key>的 value <value> 声明添加到 JWT。多次使用此选项可添加声明。

• --scope <value> adds a scope claim called <value> to the JWT. Use this option multiple times to add scopes.
--scope <value> 添加一个<value>调用 JWT 的 scope 声明。多次使用此选项可添加范围。

These aren’t the only options; you can control essentially everything about the generated JWT. Run dotnet user-jwts create --help to see all the options available. One option that may be useful in certain automated scripts or tests is the --output option. This controls how the JWT is printed to the console after creation. The default value, default, prints a summary of the JWT and the token itself, as you saw previously:
这些并不是唯一的选择;您基本上可以控制有关生成的 JWT 的所有内容。运行 dotnet user-jwts create --help 查看所有可用选项。在某些自动化脚本或测试中可能有用的一个选项是 --output 选项。这控制了 JWT 在创建后如何打印到控制台。默认值 default 打印 JWT 和令牌本身的摘要,如您之前所见:

New JWT saved with ID 'f2080e51'.
Name: andrewlock

Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFuZHJl…

This is handy if you’re creating tokens ad hoc at the command line, but the alternative output options may be more useful for scripts. For example, running
如果您在命令行中临时创建令牌,这很方便,但替代输出选项可能对脚本更有用。例如,运行

dotnet user-jwts create --output token

outputs the token only,
仅输出 token,

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6ImFuZHJl…

which is much more convenient if you’re trying to parse the output in a script, for example. Alternatively, you can pass --output json, which prints details about the JWT instead, as in this example:
例如,如果您尝试在脚本中解析输出,这会更方便。或者,您可以传递 --output json,它改为打印有关 JWT 的详细信息,如以下示例所示:

{
  "Id": "8bf9b2fd",
  "Scheme": "Bearer",
  "Name": "andrewlock",
  "Audience": " https://localhost:7236, http://localhost:5229",
  "NotBefore": "2022-10-22T17:50:26+00:00",
  "Expires": "2023-01-22T17:50:26+00:00",
  "Issued": "2022-10-22T17:50:26+00:00",
  "Token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6Im…",
  "Scopes": [],
  "Roles": [],
  "CustomClaims": {}
}

Note that this isn’t the payload of the token; it’s the configuration details used to create the JWT. The token itself is exposed in the Token field. Again, this may be useful if you’re generating JWTs using a script and need to parse the output.
请注意,这不是令牌的有效负载;它是用于创建 JWT 的配置详细信息。令牌本身在 Token 字段中公开。同样,如果您使用脚本生成 JWT 并需要解析输出,这可能很有用。

25.4.3 Managing your local JWTs

25.4.3 管理本地 JWT

When you’re generating a JWT, the user-jwts tool automatically saves the JWT configuration (the JSON shown in section 25.4.2) to your hard drive. This is stored next to the secrets.json file that contains the User Secrets, in a location that varies depending on your operating system and the <UserSecretsId> in your project file:
生成 JWT 时,user-jwts 工具会自动将 JWT 配置(第 25.4.2 节中显示的 JSON)保存到您的硬盘驱动器。它存储在包含 User Secrets 的 secrets.json 文件旁边,其<UserSecretsId>位置因作系统和项目文件而异:

• Windows—%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\user-jwts.json
• Linux and macOS—~/.microsoft/usersecrets/<UserSecretsId>/user-jwts.json

As for User Secrets, JWTs created by user-jwts aren’t encrypted, but they’re outside your project directory, so they are a better approach to managing secrets locally. The generated JWTs should be used only for local testing; you should be using a real identity provider for production systems to securely produce JWTs for a logged-in user. This is the reason why the user-jwts tool updates only appsettings.Development.json with the required configuration, not appsettings.json; it stops you from accidentally using user-jwts in production. You should add your production identity provider details in appsettings.json instead.
对于用户密钥,由 user-jwts 创建的 JWT 未加密,但它们位于项目目录之外,因此它们是在本地管理密钥的更好方法。生成的 JWT 应仅用于本地测试;您应该为生产系统使用真实身份提供程序,以便为登录用户安全地生成 JWT。这就是 user-jwts 工具仅更新 appsettings 的原因。Development.json 使用所需的配置,而不是 appsettings.json;它可以防止您在生产环境中意外使用 user-jwt。您应该改为在 appsettings.json 中添加生产身份提供商详细信息。

As well as editing the user-jwts.json file manually, you can use the user-jwts tool to manage the JWTs stored locally. In addition to using create, you can call dotnet user-jwts <command> from the project folder, where <command> is one of the following options:
除了手动编辑 user-jwts.json 文件外,您还可以使用 user-jwts 工具来管理本地存储的 JWT。除了使用 create 之外,还可以从项目文件夹调用 dotnet user-jwts<command> ,其中<command> 是以下选项之一:

• list—Lists a summary of all the tokens stored in user-jwts.json for the project.
list - 列出项目user-jwts.json中存储的所有标记的摘要。

• clear—Deletes all the tokens created for a project.
clear - 删除为项目创建的所有标记。

• remove—Deletes a single token for the project, using the token ID displayed by the list command.
remove - 使用 list 命令显示的令牌 ID 删除项目的单个令牌。

• print—Outputs the details of a single JWT, using the token ID, as key value pairs.
print - 使用令牌 ID 作为键值对输出单个 JWT 的详细信息。

• key—Can be used to view or reset the signing key material of tokens stored in the User Secrets Manager. Note that resetting the key material renders all previous JWTs generated by the tool invalid.
key - 可用于查看或重置存储在 User Secrets Manager 中的令牌的签名密钥材料。请注意,重置密钥材料会使该工具之前生成的所有 JWT 无效。

The user-jwts tool is handy for generating JWTs locally, but you must remember to add it to your local testing tool for all requests. If you’re using Postman for testing, you need to add the JWT to your request, as I showed in figure 25.10. However, if you’re using Swagger UI as I described in chapter 11, things aren’t quite that simple. In the next section you’ll learn how to describe your authorization requirements in your OpenAPI document.
user-jwts 工具对于在本地生成 JWT 非常方便,但您必须记住将其添加到所有请求的本地测试工具中。如果您使用 Postman 进行测试,则需要将 JWT 添加到您的请求中,如图 25.10 所示。但是,如果您使用的是我在第 11 章中描述的 Swagger UI,事情就没有那么简单了。在下一节中,您将学习如何在 OpenAPI 文档中描述您的授权要求。

25.5 Describing your authentication requirements to OpenAPI

25.5 向 OpenAPI 描述您的身份验证要求

In chapter 11 you learned how to add an OpenAPI document to your ASP.NET Core app that describes your API. This is used to power tooling such as automatic client generation, as well as Swagger UI. In this section you’ll learn how to add authentication requirements to your OpenAPI document so you can test your API using Swagger UI with tokens generated by the user-jwts tool.
在第 11 章中,您学习了如何将 OpenAPI 文档添加到您的 ASP.NET Core 应用程序中,以描述 API。这用于支持自动客户端生成以及 Swagger UI 等工具。在本节中,您将了解如何向 OpenAPI 文档添加身份验证要求,以便您可以使用 Swagger UI 和 user-jwts 工具生成的令牌来测试您的 API。

One of the slightly annoying things about adding authentication and authorization to your APIs is that it makes testing harder. You can’t just fire a web request from a browser; you must use a tool like Postman that you can add headers to. Even for command-line aficionados, curl commands can become unwieldy once you need to add authorization headers. And tokens expire and are typically harder to generate. The list goes on!
向 API 添加身份验证和授权的一个稍微令人讨厌的事情是,它使测试变得更加困难。您不能只从浏览器触发 Web 请求;您必须使用像 Postman 这样可以添加标头的工具。即使对于命令行爱好者来说,一旦您需要添加授权标头,curl 命令也会变得笨拙。令牌会过期,通常更难生成。名单还在继续!

I’ve seen these difficulties lead people to disable authentication requirements for local testing or to try to add them only late in a product’s life cycle. I strongly suggest you don’t do this! Trying to add real authentication late in a project is likely to cause headaches and bugs that you could easily have caught if you weren’t trying to work around the security complexity.
我看到这些困难导致人们禁用本地测试的身份验证要求,或者尝试仅在产品生命周期的后期才添加它们。我强烈建议你不要这样做!尝试在项目后期添加真正的身份验证可能会导致令人头疼和错误,如果您不尝试解决安全性复杂性,您很容易发现这些错误。

TIP Add real authentication and authorization to your APIs as soon as you understand the requirements, as you will likely catch more security-related bugs.
提示:了解要求后,立即向 API 添加真正的身份验证和授权,因为您可能会发现更多与安全相关的错误。

The user-jwts tool can help significantly with these challenges, as you can easily generate tokens in a format you need, optionally with a long expiration (so you don’t need to keep renewing them) without having to wrestle with an identity provider directly. Nevertheless, you need a way to add these tokens to whichever tool you use for testing, such as Swagger UI.
user-jwts 工具可以极大地帮助解决这些挑战,因为您可以轻松地以所需的格式生成令牌,并且可以选择具有较长的过期时间(因此您无需不断续订它们),而无需直接与身份提供商搏斗。不过,您需要一种方法将这些令牌添加到用于测试的任何工具中,例如 Swagger UI。

Swagger UI is based on the OpenAPI definition of your API, so the best (and easiest) way to add support for authentication to Swagger UI is to update the security requirements of your application in your OpenAPI document. This consists of two steps:
Swagger UI 基于 API 的 OpenAPI 定义,因此向 Swagger UI 添加身份验证支持的最佳(也是最简单的)方法是在 OpenAPI 文档中更新应用程序的安全要求。这包括两个步骤:

• Define the security scheme your API uses, such as OAuth 2.0, OpenID Connect, or simple Bearer authentication.
定义 API 使用的安全方案,例如 OAuth 2.0、OpenID Connect 或简单的 Bearer 身份验证。

• Declare which endpoints in your API use the security scheme.
声明 API 中的哪些端点使用安全方案。

The following listing shows how to configure an OpenAPI document using Swashbuckle for an API that uses JWT bearer authentication. The values defined on OpenApiSecurityScheme match the default settings configured by the user-jwts tool when you use AddJwtBearer(). AddSecurityDefinition() defines a security scheme for your API, and AddSecurityRequirement() declares that the whole API is protected using the security scheme.
下面的清单显示了如何使用 Swashbuckle 为使用 JWT 不记名身份验证的 API 配置 OpenAPI 文档。在 OpenApiSecurityScheme 上定义的值与使用 AddJwtBearer() 时 user-jwts 工具配置的默认设置匹配。AddSecurityDefinition() 为您的 API 定义一个安全方案,AddSecurityRequirement() 声明使用该安全方案保护整个 API。

Listing 25.2 Adding bearer authentication to an OpenAPI document using Swashbuckle
清单 25.2 使用 Swashbuckle 向 OpenAPI 文档添加不记名身份验证

WebApplicationBuilder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(x =>
{
    x.SwaggerDoc("v1", new OpenApiInfo {
        Title = "Recipe App", Version = "v1" });

    var security = new OpenApiSecurityScheme    #A
    {
        Name = HeaderNames.Authorization,    #B
        Type = SecuritySchemeType.ApiKey,    #C
        In = ParameterLocation.Header,    #D
        Description = "JWT Authorization header",  #E
        Reference = new OpenApiReference
        {
            Id = JwtBearerDefaults.AuthenticationScheme,  #F
            Type = ReferenceType.SecurityScheme   #G
        }
    };

    x.AddSecurityDefinition(security.Reference.Id, security);  #H
    x.AddSecurityRequirement(new OpenApiSecurityRequirement   #I
        {{security, Array.Empty<string>()}});  #I
});

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

app.MapGet("/", () => "Hello world!").RequireAuthorization();
app.Run();

❶ Defines the security used by your API
定义 API使用的安全性

❷ The name of the header to use (required)
要使用的标头的名称(必需)

❸ The type of security; may be OAuth2 or OpenIdConnect if using those (required)
安全性的类型;可能是 OAuth2 或 OpenIdConnect(如果需要)

❹ Where the token will be provided (required)
将提供令牌的位置(必需)

❺ A friendly description of the scheme, used in the UI
在UI中使用方案的友好描述

❻ A unique ID for the scheme. This uses the default JWT scheme name.
方案的唯一 ID。这将使用默认的 JWT 方案名称。

❼ The type of OpenID object (required)
OpenID 对象的类型(必需)

❽ Adds the security definition to the OpenAPI document
将安全定义添加到 OpenAPI 文档

❾ Marks the whole API as protected by the security definition
将整个 API 标记为受安全定义保护

When you run your application after adding the definition to your OpenAPI document, you should see an Authorize button in the top-right corner of Swagger UI, as shown in figure 25.11. Choosing this button opens a dialog box describing your authentication scheme, including a text box to enter your token. You must enter Bearer in this box with a space between them. Choose Authorize, which saves the value, and then Close. Now when you send a request to the API, Swagger UI attaches the token in the Authorization header, and the request succeeds.
在将定义添加到 OpenAPI 文档后运行应用程序时,您应该会在 Swagger UI 的右上角看到一个 Authorize 按钮,如图 25.11 所示。选择此按钮将打开一个描述您的身份验证方案的对话框,其中包括一个用于输入令牌的文本框。您必须在此框中输入 Bearer ,并在它们之间留出空格。选择 Authorize (授权) 以保存值,然后选择 Close (关闭)。现在,当您向 API 发送请求时,Swagger UI 会在 Authorization 标头中附加令牌,并且请求成功。

alt text

Figure 25.11 Adding an Authorization header using Swagger UI. When adding the token, ensure that you enter Bearer , including the Bearer prefix. Swagger UI then attaches the token to all subsequent requests, so you are authorized to call the API.
图 25.11 使用 Swagger UI 添加 Authorization 标头。添加令牌时,请确保输入 Bearer ,包括 Bearer 前缀。然后,Swagger UI 会将令牌附加到所有后续请求,以便您有权调用 API。

If you’re specifically using OpenID Connect or OAuth 2.0 to protect your APIs, you can configure these in the OpenApiSecurityScheme document instead of using bearer authentication. In that case, choosing Authorize in Swagger UI would redirect you to your identity provider to sign in and retrieve a token without your having to copy and paste anything. That’s extremely useful if you’re running an identity provider locally or exposing Swagger UI in production.
如果您专门使用 OpenID Connect 或 OAuth 2.0 来保护 API,则可以在 OpenApiSecurityScheme 文档中配置这些 API,而不是使用不记名身份验证。在这种情况下,在 Swagger UI 中选择 Authorize (授权) 会将您重定向到您的身份提供商以登录并检索令牌,而无需复制和粘贴任何内容。如果您在本地运行身份提供商或在生产环境中公开 Swagger UI,这将非常有用。

The example in listing 25.2 shows the configuration when your whole API is protected by an authorization requirement. That’s the most common situation in my experience, but you may want to expose certain endpoints to anonymous users without any authorization requirements. In that case, you can configure Swashbuckle to conditionally apply the requirement to only those endpoints with a requirement.
清单 25.2 中的示例显示了整个 API 受授权需求保护时的配置。根据我的经验,这是最常见的情况,但您可能希望将某些端点公开给匿名用户,而无需任何授权要求。在这种情况下,您可以将 Swashbuckle 配置为有条件地仅将要求应用于具有要求的终端节点。

TIP See the Swashbuckle documentation to learn how to configure this and many other features related to OpenAPI document generation: http://mng.bz/6D1A. Swashbuckle is highly extensible, but as always, it’s worth considering whether the added complexity you introduce to achieve perfect documentation of your API is worth the tradeoff. For publicly exposed OpenAPI documents, this may well be the case, but for local testing or internal APIs, the argument may be harder to make.
提示:请参阅 Swashbuckle 文档,了解如何配置此功能以及与 OpenAPI 文档生成相关的许多其他功能:http://mng.bz/6D1A。Swashbuckle 具有高度可扩展性,但与往常一样,值得考虑为实现 API 的完美文档而引入的额外复杂性是否值得权衡。对于公开公开的 OpenAPI 文档,情况很可能如此,但对于本地测试或内部 API,可能更难提出论点。

In this chapter we’ve looked in depth at using JWT bearer tokens for authentication and explored the parallels with cookie authentication for traditional apps. In the final section of this chapter we look at authorization and how you can apply different authorization policies to your minimal API endpoints.
在本章中,我们深入探讨了如何使用 JWT 不记名令牌进行身份验证,并探讨了与传统应用程序的 cookie 身份验证的相似之处。在本章的最后一节中,我们将介绍授权以及如何将不同的授权策略应用于您的最小 API 终端节点。

25.6 Applying authorization policies to minimal API endpoints

25.6 将授权策略应用于最小 API 端点

So far in this chapter we’ve focused on authentication: the process of validating the identity of the request initiator. For APIs, this typically requires decoding and validating a JWT bearer token in the authentication middleware and setting the ClaimsPrincipal for the request, as you saw in section 25.2. In this section we look at the next stage in protecting your APIs, authorization, and how you can apply different authorization requirements to your minimal API endpoints.
到目前为止,在本章中,我们重点介绍了身份验证:验证请求发起者身份的过程。对于 API,这通常需要在身份验证中间件中解码和验证 JWT 不记名令牌,并为请求设置 ClaimsPrincipal,如第 25.2 节所示。在本节中,我们将介绍保护 API 的下一阶段、授权,以及如何将不同的授权要求应用于最小 API 终端节点。

The good news is that authorization for minimal APIs is essentially identical to the authorization process you learned about in chapter 24 for Razor Pages and MVC controllers. The same concept of authorization policies, requirements, handlers, and claims-based authorization apply in the same way and use the exact same services. Figure 25.12 shows how this looks for a request to a minimal API endpoint protected with bearer authentication, which is remarkably similar to the Razor Pages equivalent in figure 24.2.
好消息是,最小 API 的授权与您在第 24 章中了解的 Razor Pages 和 MVC 控制器的授权过程基本相同。相同的授权策略、要求、处理程序和基于声明的授权概念以相同的方式应用,并使用完全相同的服务。图 25.12 显示了如何查找对受不记名身份验证保护的最小 API 端点的请求,这与图 24.2 中的 Razor Pages 等效项非常相似。

alt text

Figure 25.12 Authorizing a request to a minimal API endpoint. The routing middleware selects an endpoint that is protected by an authorization requirement. The authentication middleware decodes and verifies the bearer token, creating a ClaimsPrincipal, which the authorization middleware uses along with the endpoint metadata to determine whether the request is authorized.
图 25.12 授权对最小 API 端点的请求。路由中间件选择受授权要求保护的终端节点。身份验证中间件对持有者令牌进行解码和验证,创建一个 ClaimsPrincipal,授权中间件将其与终结点元数据一起使用,以确定请求是否获得授权。

You’ve already seen that you can apply a general authorization requirement by calling RequireAuthorization() on an endpoint or a route group. This is directly equivalent to adding the [Authorize] attribute to a Razor Page or MVC controller action. In fact, you can use the same [Authorize] attribute on an endpoint if you wish, so the following two endpoint definitions are equivalent:
您已经看到,您可以通过在终端节点或路由组上调用 RequireAuthorization() 来应用常规授权要求。这直接等效于将 [Authorize] 属性添加到 Razor Page 或 MVC 控制器作。事实上,如果需要,可以在终结点上使用相同的 [Authorize] 属性,因此以下两个终结点定义是等效的:

app.MapGet("/", () => "Hello world!").RequireAuthorization();
app.MapGet("/", [Authorize] () => "Hello world!");

If you want to require a specific policy (the "CanCreate" policy, for example), you can pass the policy names to the RequireAuthorization() method the same way you would for the [Authorize] attribute:
如果要要求特定策略(例如“CanCreate”策略),则可以将策略名称传递给 RequireAuthorization() 方法,就像对 [Authorize] 属性所做的那样:

app.MapGet("/", () => "Hello world!").RequireAuthorization("CanCreate");
app.MapGet("/", [Authorize("CanCreate")] () => "Hello world!");

Similarly, you can exclude endpoints from authentication requirements using the AllowAnonymous() function or [AllowAnonymous] attribute:
同样,您可以使用 AllowAnonymous() 函数或 [AllowAnonymous] 属性从身份验证要求中排除端点:

app.MapGet("/", () => "Hello world!").AllowAnonymous();
app.MapGet("/", [AllowAnonymous] () => "Hello world!");

This is a good start, but as you saw in chapter 24, you often need to perform resource-based authorization. For example, in the context of the recipe API, users should be allowed to edit or delete only recipes that they created; they can’t edit someone else’s recipe. That means you need to know details about the resource (the recipe) before determining whether a request is authorized.
这是一个好的开始,但正如您在第 24 章中看到的那样,您通常需要执行基于资源的授权。例如,在配方 API 的上下文中,应仅允许用户编辑或删除他们创建的配方;他们无法编辑其他人的配方。这意味着在确定请求是否获得授权之前,您需要了解有关资源(配方)的详细信息。

Resource-based authorization is essentially the same for minimal API endpoints as for Razor Pages or MVC controllers. You must follow several steps, most of which we covered in chapter 24:
最小 API 端点的基于资源的授权与 Razor Pages 或 MVC 控制器的授权基本相同。您必须遵循几个步骤,其中大部分我们在第 24 章中介绍:

  1. Create an AuthorizationHandler<TRequirement, TResource>, and register it in the DI container, as shown in chapter 24.
    创建一个AuthorizationHandler<TRequirement, TResource> ,并在 DI 容器中注册它,如第 24 章所示。

  2. Inject the IAuthorizationService into your endpoint handler.
    将 IAuthorizationService 注入到终端节点处理程序中。

  3. Call IAuthorizationService.AuthorizeAsync(user, resource, policy), passing in the ClaimsPrincipal for the request, the resource to authorize access to, and the policy to apply.
    调用 IAuthorizationService.AuthorizeAsync(user, resource, policy),传入请求的 ClaimsPrincipal、要授权访问的资源以及要应用的策略。

The first step is identical to the process shown in chapter 24, so you can reuse the same authorization handlers whether you’re using Razor Pages, minimal APIs, or both! You can access the IAuthorizationService from a minimal API endpoint using standard dependency injection (DI), which you learned about in chapters 8 and 9.
第一步与第 24 章中所示的过程相同,因此无论您使用的是 Razor Pages、最少的 API,还是同时使用这两者,您都可以重复使用相同的授权处理程序!您可以使用标准依赖关系注入 (DI) 从最小 API 终端节点访问 IAuthorizationService,您在第 8 章和第 9 章中对此进行了了解。

Listing 25.3 shows an example minimal API endpoint that uses resource-based authorization to protect the “delete” action for a recipe. The IAuthorizationService and HttpContext.User property are injected into the handler method along with the RecipeService. The endpoint then retrieves the recipe and calls AuthorizeAsync() to determine whether to continue with the delete or return a 403 Forbidden response.
清单 25.3 显示了一个示例最小 API 端点,它使用基于资源的授权来保护配方的 “delete”作。IAuthorizationService 和 HttpContext.User 属性与 RecipeService 一起注入处理程序方法中。然后,终端节点检索配方并调用 AuthorizeAsync() 以确定是继续删除还是返回 403 Forbidden 响应。

Listing 25.3 Using resource authorization to protect a minimal API endpoint
清单 25.3 使用资源授权保护最小 API 端点

app.MapDelete("recipe/{id}", async (
    int id, RecipeService service,
    IAuthorizationService authService,    #A
    ClaimsPrincipal user) =>    #B
{
    var recipe = await service.GetRecipe(id);    #C
    var result = await authService.AuthorizeAsync(    #D
        user, recipe, "CanManageRecipe");    #D

    if (!result.Succeeded)    #E
    {    #E
        return Results.Forbid();    #E
    }    #E

    await service.DeleteRecipe(id);  #F
    return Results.NoContent();  #F
});

❶ Injected to perform resource-based authorization
注入以执行基于资源的授权

❷ The HttpContext.User claims principal for the request
HttpContext.User 声明请求的主体

❸ Fetches the recipe to access
获取配方以访问

❹ Performs resource-based authorization, passing in the user, resource, and the policy name
执行基于资源的授权,传入用户、资源和策略名称

❺ If authorization failed, returns 403 Forbidden
如果授权失败,则返回 403 Forbidden

❻ If authorization succeeded, executes the endpoint as normal
如果授权成功,则照常执行端点

As is common when you start adding functionality, the logic at the heart of the endpoint has become a bit muddled as the endpoint has grown. There are several possible approaches you could take now:
当您开始添加功能时,通常情况下,随着终端节点的增长,终端节点核心的逻辑变得有点混乱。您现在可以采取几种可能的方法:

• Do nothing. The logic isn’t that confusing, and this is only one endpoint. This may be a good approach initially but can become problematic if the logic is duplicated across multiple endpoints.
什么都不做。逻辑并不那么令人困惑,这只是一个端点。这在最初可能是一种很好的方法,但如果 logic 在多个 endpoints 之间重复,则可能会产生问题。

• Pull the authorization out into a filter. As you saw in chapters 5 and 7, endpoint filters can be useful for extracting common cross-cutting concerns, such as validation and authorization. You may find that endpoint filters help reduce the duplication in your endpoint handlers, though this often comes at the expense of additional complexity in the filter itself, as well as a layer of indirection in your handlers. You can see this approach in the source code accompanying this chapter.
将授权拉出到过滤器中。正如您在第 5 章和第 7 章中看到的那样,端点过滤器可用于提取常见的横切关注点,例如验证和授权。您可能会发现终端节点筛选条件有助于减少终端节点处理程序中的重复,尽管这通常是以筛选条件本身的额外复杂性以及处理程序中的间接层为代价的。您可以在本章随附的源代码中看到这种方法。

• Push the authorization responsibilities down into the domain. Instead of performing the resource-based authorization in your endpoint handlers, you could run the checks inside the domain instead, in the RecipeService in this case. This has advantages, in that it often reduces duplication, keeps your endpoints simpler, and ensures that authorization checks are always applied regardless of how you call the domain methods.
将授权责任向下推送到域中。您可以在域内运行检查,而不是在终端节点处理程序中执行基于资源的授权,在本例中为 RecipeService。这样做的好处是,它通常可以减少重复,使您的端点更简单,并确保无论您如何调用域方法,都始终应用授权检查。

• The downside to this approach is that it may cause your domain/application model to depend directly on ASP.NET Core-specific constructs such as IAuthorizationService. You can work around this by creating a wrapper façade around the IAuthorizationService, but this may also add some complexity. Even if you take this approach, you typically want to apply declarative authorization policies to your endpoints as well to ensure that the endpoint executes only for users who could possibly be authorized.
此方法的缺点是,它可能会导致域/应用程序模型直接依赖于特定于 ASP.NET Core 的构造,例如 IAuthorizationService。您可以通过围绕 IAuthorizationService 创建包装器外观来解决此问题,但这也可能增加一些复杂性。即使您采用此方法,您通常也希望将声明式授权策略应用于终端节点,以确保终端节点仅对可能获得授权的用户执行。

There’s no single best answer on which approach to take; it will vary depending on what works best for your application. Authentication and authorization are inevitably tricky subjects, so it’s important to consider them early and design your application with security in mind.
关于采取哪种方法,没有单一的最佳答案;它会根据最适合您的应用程序的方法而有所不同。身份验证和授权不可避免地是棘手的主题,因此尽早考虑它们并在设计应用程序时考虑安全性非常重要。

Scope-based authorization policies
基于范围的授权策略

In section 15.2 I described the role of scopes in the authentication process. When you obtain a bearer token from an identity provider—whether you’re using OpenID Connect or OAuth 2.0—you define the scopes that you wish to retrieve. The user can then choose to grant or deny some or all of those requested scopes. Additionally, the identity provider might allow certain client applications access only to specific scopes. The final access token you receive from the identity provider, which is sent to the API, may have some or none of the requested scopes.
在 Section 15.2 中,我描述了范围在身份验证过程中的作用。当您从身份提供商处获取不记名令牌时(无论您使用的是 OpenID Connect 还是 OAuth 2.0),您都可以定义要检索的范围。然后,用户可以选择授予或拒绝部分或全部请求的范围。此外,身份提供商可能仅允许某些客户端应用程序访问特定范围。您从身份提供商处收到的最终访问令牌(发送到 API)可能具有部分或没有请求的范围。

It’s up to the API itself to decide what each scope means and how it should be used to enforce authorization policies. Scopes have no inherent functionality on their own, much like claims, but you can build functionality on top. For example, you can create authorization polices that require a token has the scope "recipe.edit" using
由 API 本身决定每个范围的含义以及如何使用它来实施授权策略。范围本身没有固有的功能,就像声明一样,但你可以在其上构建功能。例如,您可以使用

builder.Services.AddAuthorizationBuilder()
.AddPolicy("RecipeEditScope", policy =>
policy.RequireClaim("scope", " recipe.edit "));

This policy could then be applied to any endpoints that edit a recipe.
然后,此策略可应用于编辑配方的任何终端节点。

Another common pattern is to require a specific scope for you to be authorized to make any requests to a given ASP.NET Core app, such as a "receipeApi" scope. This approach can often replace audience validation in bearer token authorization and may be more flexible, as it doesn’t require your identity provider to know the domain at which your API app will be hosted.
另一种常见模式是要求特定范围,以便您有权向给定的 ASP.NET Core 应用程序发出任何请求,例如“receipeApi”范围。这种方法通常可以取代不记名令牌授权中的受众验证,并且可能更灵活,因为它不需要您的身份提供商知道将托管您的 API 应用程序的域。

Alternatively, you can use scopes to partition your APIs into groups that can only be accessed by certain types of clients. For example, you might have one set of APIs that can be accessed only by internal machine-to-machine clients, another set that can be accessed only by admin users, and another set that can be accessed only by nonadmin users.
或者,您可以使用范围将 API 划分为只能由某些类型的客户端访问的组。例如,您可能有一组只能由内部计算机到计算机客户端访问的 API,另一组只能由管理员用户访问,另一组只能由非管理员用户访问。

Duende has many practical examples of approaches to authorization and authentication using OpenID Connect at http://mng.bz/o1Jp. The examples are geared to IdentityServer users but show many best practices and patterns you can use with identity provider services as well.
Duende 在 http://mng.bz/o1Jp 上提供了许多使用 OpenID Connect 进行授权和身份验证的方法的实际示例。这些示例面向 IdentityServer 用户,但也展示了许多可用于身份提供商服务的最佳实践和模式。

That brings us to the end of this chapter on authentication and authorization. We’re not completely done with security, though; in chapter 27 we look at potential security threats and how to mitigate them. But first, in chapter 26 you’ll learn about the logging abstractions in ASP.NET Core and how you can use them to keep tabs on exactly what your app’s up to.
这将我们带到了本章关于身份验证和授权的结尾。不过,我们还没有完全完成安全性;在第 27 章中,我们将介绍潜在的安全威胁以及如何缓解这些威胁。但首先,在第 26 章中,您将了解 ASP.NET Core 中的日志记录抽象,以及如何使用它们来密切关注您的应用程序的确切动态。

25.7 Summary

25.7 总结

In large systems with multiple applications or APIs, you can use an identity provider to centralize authentication and user management. This often reduces the authentication responsibilities of apps, reducing duplication and making it easier to add new user management features.
在具有多个应用程序或 API 的大型系统中,您可以使用身份提供商来集中身份验证和用户管理。这通常可以减少应用程序的身份验证责任,减少重复,并更轻松地添加新的用户管理功能。

You should strongly consider using a third-party identity provider service instead of building your own. User management is rarely core to your business, and by delegating responsibility to a third-party you can leave protecting your most vulnerable assets to the experts.
您应该强烈考虑使用第三方身份提供商服务,而不是构建自己的服务。用户管理很少是您的业务核心,通过将责任委托给第三方,您可以将保护最脆弱的资产的工作留给专家。

If you do need to build your own identity provider, you can use the IdentityServer or OpenIddict library. These libraries implement the OpenID Connect protocol, adding token generation to a standard ASP.NET Core application. You must build the user management and UI components yourself.
如果您确实需要构建自己的身份提供商,则可以使用 IdentityServer 或 OpenIddict 库。这些库实现 OpenID Connect 协议,将令牌生成添加到标准 ASP.NET Core 应用程序中。您必须自己构建用户管理和 UI 组件。

OAuth 2.0 is an authorization protocol that allows a user to delegate authorization for accessing a resource to another application. This standard allows applications to interoperate without compromising on security.
OAuth 2.0 是一种授权协议,允许用户将访问资源的授权委托给另一个应用程序。此标准允许应用程序在不影响安全性的情况下进行互作。

OAuth 2.0 has multiple grant types representing common authorization flows. The authorization code flow with PKCE is the most common interactive grant type when a user initiates an interaction. For machine-only workflows, such as an API calling another API, you can use the client credentials grant type.
OAuth 2.0 具有多种授权类型,代表常见的授权流程。当用户发起交互时,使用 PKCE 的授权代码流是最常见的交互式授权类型。对于仅限计算机的工作流,例如调用其他 API 的 API,您可以使用客户端凭证授权类型。

OpenID Connect is built on top of OAuth 2.0. It adds conventions, discoverability, and authentication to OAuth 2.0, making it easier to interact with third-party providers and retrieve identity information about a user.
OpenID Connect 基于 OAuth 2.0 构建。它为 OAuth 2.0 添加了约定、可发现性和身份验证,从而可以更轻松地与第三方提供商交互并检索有关用户的身份信息。

JWTs are the most common bearer token format. They consist of a header, a payload, and a signature, and are base64-encoded. When receiving a JWT you must always verify the signature to ensure that it hasn’t been tampered with.
JWT 是最常见的不记名令牌格式。它们由标头、有效负载和签名组成,并且采用 base64 编码。收到 JWT 时,您必须始终验证签名以确保它未被篡改。

JWTs are not encrypted, so anyone can read them by default. JWE is a standard that wraps the JWT and encrypts it, protecting the contents. Many identity providers support generating JWEs, and ASP.NET Core supports decoding JWEs automatically.
JWT 未加密,因此默认情况下任何人都可以读取它们。JWE 是一种包装 JWT 并对其进行加密的标准,可保护其内容。许多身份提供商支持生成 JWE,而 ASP.NET Core 支持自动解码 JWE。

Bearer token authentication in ASP.NET Core is similar to cookie authentication with traditional web apps. The authentication middleware deserializes the token and validates it. If the token is valid, the middleware creates a ClaimsPrincipal and sets HttpContext.User.
ASP.NET Core 中的持有者令牌身份验证类似于传统 Web 应用程序的 Cookie 身份验证。身份验证中间件反序列化令牌并对其进行验证。如果令牌有效,中间件将创建一个 ClaimsPrincipal 并设置 HttpContext.User。

Configure JWT bearer authentication by adding the Microsoft.AspNetCore.Authentication.JwtBearer NuGet Package and calling AddAuthentication().AddJwtBearer() to add the required services to your app.
通过添加 Microsoft.AspNetCore.Authentication.JwtBearer NuGet 包并调用 AddAuthentication() 来配置 JWT 不记名身份验证。AddJwtBearer() 将所需的服务添加到您的应用程序中。

To generate a JWT for local testing, run dotnet user-jwts create. This configures your API to support JWTs created by the tool and prints a token to the terminal, which you can use for local testing of your API. Add the token to requests in the Authorization header, using the format "Bearer <token>".
要生成用于本地测试的 JWT,请运行 dotnet user-jwts create。这会将您的 API 配置为支持该工具创建的 JWT,并将令牌打印到终端,您可以使用该令牌对 API 进行本地测试。使用 “Bearer <token>” 格式将令牌添加到 Authorization 标头中的请求。

Pass additional options to the dotnet user-jwts create command to customize the generated JWT. Add extra claims to the generated JWT using the --claim option, change the sub claim name using --name, or add scope claims to the JWT using --scope.
将其他选项传递给 dotnet user-jwts create 命令以自定义生成的 JWT。使用 --claim 选项向生成的 JWT 添加额外的声明,使用 --name 更改子声明名称,或使用 --scope 向 JWT 添加范围声明。

To enable authorization in Swagger UI, you should add a security scheme to your OpenAPI document. Create an OpenApiSecurityScheme object, and register it with the OpenAPI document by calling AddSecurityDefinition(). Apply it to all the APIs in your app by calling AddSecurityRequirement(), passing in the scheme object.
要在 Swagger UI 中启用授权,您应该向 OpenAPI 文档添加安全方案。创建一个 OpenApiSecurityScheme 对象,并通过调用 AddSecurityDefinition() 将其注册到 OpenAPI 文档中。通过调用 AddSecurityRequirement() 并将其应用于应用中的所有 API,并传入 scheme 对象。

To add authorization to minimal API endpoints, call RequireAuthorization() or add the [Authorize] attribute to your endpoint handler. This optionally takes the name of an authorization policy to apply, n the same way as you would apply policies to Razor Pages and MVC controllers. You can call RequireAuthorization() on route groups to apply authorization to multiple APIs at the same time.
若要向最小 API 终结点添加授权,请调用 RequireAuthorization() 或将 [Authorize] 属性添加到终结点处理程序。这可以选择采用要应用的授权策略的名称,其方式与将策略应用于 Razor Pages 和 MVC 控制器的方式相同。您可以在路由组上调用 RequireAuthorization() 以同时将授权应用于多个 API。

Override an authorization requirement on an endpoint by calling AllowAnonymous() or by adding the [AllowAnonymous] attribute to an endpoint handler. This removes any authentication requirements from the endpoint, so users can call the endpoint without a bearer token in the request.
通过调用 AllowAnonymous() 或将 [AllowAnonymous] 属性添加到终结点处理程序来替代终结点上的授权要求。这将删除终端节点中的任何身份验证要求,因此用户可以在请求中没有持有者令牌的情况下调用终端节点。

ASP.NET Core in Action 24 Authorization: Securing your application

24 Authorization: Securing your application‌
24 Authorization: 保护您的应用程序

This chapter covers

本章涵盖

• Using authorization to control who can use your app
使用授权来控制谁可以使用你的应用
• Using claims-based authorization with policies
将基于声明的授权与策略结合使用
• Creating custom policies to handle complex requirements
创建自定义策略以处理复杂要求
• Authorizing a request depending upon the resource being accessed
根据正在访问的资源授权请求
• Hiding elements from a Razor template that the user is unauthorized to access
隐藏用户无权访问的 Razor 模板中的元素

In chapter 23 I showed you how to add users to an ASP.NET Core application by adding authentication. With authentication, users can register and log in to your app using an email address and password. Whenever you add authentication to an app, you inevitably find you want to be able to restrict what some users can do. The process of determining whether a user can perform a given action on your app is called authorization.
在第 23 章中,我向您展示了如何通过添加身份验证将用户添加到 ASP.NET Core 应用程序。通过身份验证,用户可以使用电子邮件地址和密码注册和登录您的应用。每当向应用程序添加身份验证时,您都不可避免地会发现您希望能够限制某些用户可以执行的作。确定用户是否可以对您的应用执行给定作的过程称为授权。

On an e-commerce site, for example, you may have admin users who are allowed to add new products and change prices, sales users who are allowed to view completed orders, and customer users who are allowed only to place orders and buy products.
例如,在电子商务网站上,您可能拥有允许添加新产品和更改价格的管理员用户、允许查看已完成订单的销售用户以及仅允许下订单和购买产品的客户用户。

In this chapter I show how to use authorization in an app to control what your users can do. In section 24.1 I introduce authorization and put it in the context of a real-life scenario you’ve probably experienced: an airport. I describe the sequence of events, from checking in, to passing through security, to entering an airport lounge, and you’ll see how these relate to the authorization concepts in this chapter.
在本章中,我将介绍如何在应用程序中使用授权来控制用户可以执行的作。在 Section 24.1 中,我介绍了 Authorization 并将其置于您可能经历过的真实场景的上下文中:机场。我将介绍事件的顺序,从办理登机手续到通过安检,再到进入机场休息室,您将了解这些事件与本章中的授权概念有何关系。

In section 24.2 I show how authorization fits into an ASP.NET Core web application and how it relates to the ClaimsPrincipal class you saw in the previous chapter. You’ll see how to enforce the simplest level of authorization in an ASP.NET Core app, ensuring that only authenticated users can execute a Razor Page or MVC action. This chapter focuses on authorization in Razor Pages and Model-View- Controller (MVC) controllers; in chapter 25 you’ll learn how the same principles apply to minimal API applications.
在第 24.2 节中,我将展示授权如何适应 ASP.NET Core Web 应用程序,以及它与您在上一章中看到的 ClaimsPrincipal 类的关系。你将了解如何在 ASP.NET Core 应用程序中强制实施最简单的授权级别,确保只有经过身份验证的用户才能执行 Razor Page 或 MVC作。本章重点介绍 Razor Pages 和模型视图控制器 (MVC) 控制器中的授权;在第 25 章中,您将了解相同的原则如何应用于最小的 API 应用程序。

We’ll extend that approach in section 24.3 by adding the concept of policies. These let you set specific requirements for a given authenticated user, requiring that they have specific pieces of information to execute an action or Razor Page.
我们将在 Section 24.3 中通过添加 policies 的概念来扩展该方法。这些允许您为给定的经过身份验证的用户设置特定要求,要求他们具有执行作或 Razor 页面的特定信息。

You’ll use policies extensively in the ASP.NET Core authorization system, so in section 24.4 we’ll explore how to handle more complex scenarios. You’ll learn about authorization requirements and handlers, and how you can combine them to create specific policies that you can apply to your Razor Pages and actions.
您将在 ASP.NET Core 授权系统中广泛使用策略,因此在第 24.4 节中,我们将探讨如何处理更复杂的情况。你将了解授权要求和处理程序,以及如何将它们组合起来以创建可应用于 Razor Pages 和作的特定策略。

Sometimes whether a user is authorized depends on which resource or document they’re attempting to access. A resource is anything that you’re trying to protect, so it could be a document or a post in a social media app. For example, you may allow users to create documents or to read documents from other users, but to edit only documents that they created themselves. This type of authorization, where you need the details of the document to determine if the user is authorized, is called resource-based authorization, and it’s the focus of section 24.5.
有时,用户是否获得授权取决于他们尝试访问的资源或文档。资源是您尝试保护的任何内容,因此它可以是社交媒体应用程序中的文档或帖子。例如,您可以允许用户创建文档或读取其他用户的文档,但只能编辑他们自己创建的文档。这种类型的授权,您需要文档的详细信息来确定用户是否获得授权,称为基于资源的授权,这是 Section 24.5 的重点。

In the final section of this chapter I show how you can extend the resource-based authorization approach to your Razor view templates. This lets you modify the UI to hide elements that users aren’t authorized to interact with. In particular, you’ll see how to hide the Edit button when a user isn’t authorized to edit the entity.
在本章的最后一节中,我将介绍如何将基于资源的授权方法扩展到 Razor 视图模板。这样,您就可以修改 UI 以隐藏用户无权与之交互的元素。具体而言,您将了解如何在用户无权编辑实体时隐藏 Edit (编辑) 按钮。

We’ll start by looking more closely at the concept of authorization, how it differs from authentication, and how it relates to real-life concepts you might see in an airport.
首先,我们将更仔细地研究授权的概念,它与身份验证有何不同,以及它与您可能在机场看到的现实生活中的概念有何关系。

24.1 Introduction to authorization‌

24.1 授权简介

In this section I provide an introduction to authorization and discuss how it compares with authentication. I use the real- life example of an airport as a case study to illustrate how claims-based authorization works.
在本节中,我将介绍授权并讨论它与身份验证的比较。我使用机场的真实示例作为案例研究来说明基于索赔的授权是如何运作的。

For people who are new to web apps and security, authentication and authorization can be a little daunting. It certainly doesn’t help that the words look so similar! The two concepts are often used together, but they’re definitely distinct:
对于刚接触 Web 应用程序和安全性的人来说,身份验证和授权可能有点令人生畏。这些词看起来如此相似当然无济于事!这两个概念经常一起使用,但它们绝对是不同的:

• Authentication—The process of determining who made a request
身份验证 - 确定请求发出者的过程
• Authorization—The process of determining whether the requested action is allowed
授权 - 确定是否允许请求的作的过程

Typically, authentication occurs first so that you know who is making a request to your app. For traditional web apps, your app authenticates a request by checking the encrypted cookie that was set when the user logged in (as you saw in chapter 23). API applications typically use a header instead of a cookie for authentication, but the overall process is the same, as you’ll see in chapter 25.
通常,首先进行身份验证,以便您知道谁在向您的应用发出请求。对于传统的 Web 应用程序,您的应用程序通过检查用户登录时设置的加密 cookie 来验证请求(如第 23 章所示)。API 应用程序通常使用 Headers 而不是 cookie 进行身份验证,但整个过程是相同的,您将在第 25 章中看到。

Once a request is authenticated and you know who is making the request, you can determine whether they’re allowed to execute an action on your server. This process is called authorization and is the focus of this chapter.
在请求经过身份验证并且您知道谁在发出请求后,您可以确定是否允许他们在您的服务器上执行作。此过程称为 authorization,是本章的重点。

Before we dive into code and start looking at authorization in ASP.NET Core, I’ll put these concepts into a real-life scenario that I hope you’re familiar with: checking in at an airport. To enter an airport and board a plane, you must pass through several steps: an initial step to prove who you are (authentication) and subsequent steps that check whether you’re allowed to proceed (authorization). In simplified form, these might look like this:
在我们深入研究代码并开始研究 ASP.NET Core 中的授权之前,我将把这些概念放入一个我希望您熟悉的真实场景中:在机场办理登机手续。要进入机场并登机,您必须通过几个步骤:证明您的身份的初始步骤(身份验证)和检查您是否被允许继续的后续步骤(授权)。在简化形式中,这些可能如下所示:

  1. Show your passport at the check-in desk. Receive a boarding pass.
    在值机柜台出示您的护照。收到登机牌。
  2. Show your boarding pass to enter security. Pass through security.
    出示登机牌进入安检。通过安检。
  3. Show your frequent-flyer card to enter the airline lounge. Enter the lounge.
    出示您的常旅客卡进入航空公司休息室。进入休息室。
  4. Show your boarding pass to board the flight. Enter the airplane.
    出示您的登机牌登机。进入飞机。

Obviously, these steps, also shown in figure 24.1, will vary somewhat in real life (I don’t have a frequent-flyer card!), but we’ll go with them for now. Let’s explore each step a little further.
显然,这些步骤(如图 24.1 所示)在现实生活中会有所不同(我没有常旅客卡!),但我们现在就来介绍一下。让我们进一步探讨每个步骤。

alt text
alt text

Figure 24.1 When boarding a plane at an airport, you pass through several authorization steps. At each authorization step, you must present a claim in the form of a boarding pass or a frequent-flyer card. If you’re not authorized, access is denied.
图 24.1 在机场登机时,您需要完成几个授权步骤。在每个授权步骤中,您必须以登机牌或常旅客卡的形式出示索赔。如果您未获得授权,则访问将被拒绝。

When you arrive at the airport, the first thing you do is go to the check-in counter. Here, you can purchase a plane ticket, but to do so, you need to prove who you are by providing a passport; you authenticate yourself. If you’ve forgotten your passport, you can’t authenticate, and you can’t go any further.
当您到达机场时,您做的第一件事是前往值机柜台。在这里,您可以购买机票,但要购买机票,您需要通过提供护照来证明您的身份;您验证自己。如果您忘记了护照,则无法进行身份验证,也无法继续。

Once you’ve purchased your ticket, you’re issued a boarding pass, which says which flight you’re on. We’ll assume that it also includes a BoardingPassNumber. You can think of this number as an additional claim associated with your identity.
购买机票后,您将收到一张登机牌,上面写着您乘坐的航班。我们假设它还包括 BoardingPassNumber。您可以将此号码视为与您的身份关联的附加声明。

DEFINITION A claim is a piece of information about a user that consists of a type and an optional value.
定义:声明是有关用户的一条信息,由类型和可选值组成。

The next step is security. The security guards ask you to present your boarding pass for inspection, which they use to check that you have a flight and so are allowed deeper into the airport. This is an authorization process: you must have the required claim (a BoardingPassNumber) to proceed.
下一步是安全性。保安要求您出示登机牌以供检查,他们用它来检查您是否有航班,因此可以进入机场更深处。这是一个授权过程:您必须拥有所需的声明 (BoardingPassNumber) 才能继续。

If you don’t have a valid BoardingPassNumber, there are two possibilities for what happens next:
如果您没有有效的 BoardingPassNumber,则接下来有两种可能的情况:

• If you haven’t yet purchased a ticket—You’ll be directed back to the check-in desk, where you can authenticate and purchase a ticket. At that point, you can try to enter security again.
如果您尚未购买机票 - 您将被引导回值机柜台,在那里您可以进行身份验证和购买机票。此时,您可以尝试再次进入安检。

• If you have an invalid ticket—You won’t be allowed through security, and there’s nothing else you can do. If, for example, you show up with a boarding pass a week late for your flight, they probably won’t let you through. (Ask me how I know!)
如果您的机票无效 - 您将不被允许通过安检,并且您无能为力。例如,如果您的航班登机牌晚了一周,他们可能不会让您通过。(问我怎么知道的!)

Once you’re through security, you need to wait for your flight to start boarding, but unfortunately, there aren’t any seats free. Typical! Luckily, you’re a regular flyer, and you’ve notched up enough miles to achieve Gold frequent-flyer status, so you can use the airline lounge.
通过安检后,您需要等待航班开始登机,但不幸的是,没有任何空位。典型!幸运的是,您是一名普通乘客,并且您已经积累了足够的里程来获得金卡常旅客身份,因此您可以使用航空公司休息室。

You head to the lounge, where you’re asked to present your Gold frequent-flyer card to the attendant, and they let you in. This is another example of authorization. You must have a FrequentFlyerClass claim with a value of Gold to proceed.
您前往休息室,在那里您被要求向服务员出示您的黄金常旅客卡,他们让您进入。这是授权的另一个示例。您必须有一个价值为 Gold 的 FrequentFlyerClass 索赔才能继续。

NOTE You’ve used authorization twice so far in this scenario. Each time, you presented a claim to proceed. In the first case, the presence of any BoardingPassNumber was sufficient, whereas for the FrequentFlyerClass claim, you needed the specific value of Gold.
注意:到目前为止,在此方案中,你已使用两次授权。每次,您都会提出要继续的索赔。在第一种情况下,任何 BoardingPassNumber 的存在就足够了,而对于 FrequentFlyerClass 声明,您需要 Gold 的特定值。

When you’re boarding the airplane, you have one final authorization step, in which you must present the BoardingPassNumber claim again. You presented this claim earlier, but boarding the aircraft is a distinct action from entering security, so you have to present it again.
当您登机时,您有一个最后的授权步骤,在该步骤中,您必须再次提供 BoardingPassNumber 声明。您之前提交了此声明,但登机与进入安检是不同的作,因此您必须再次提交。

This whole scenario has lots of parallels with requests to a web app:
整个场景与对 Web 应用程序的请求有很多相似之处:

• Both processes start with authentication.
两个过程都从身份验证开始。
• You must prove who you are to retrieve the claims you need for authorization.
您必须证明您是谁才能检索授权所需的索赔。
• You use authorization to protect sensitive actions like entering security and the airline lounge.
您使用授权来保护敏感作,例如进入安检和航空公司休息室。

I’ll reuse this airport scenario throughout the chapter to build a simple web application that simulates the steps you take in an airport. We’ve covered the concept of authorization in general, so in the next section we’ll look at how authorization works in ASP.NET Core. We’ll start with the most basic level of authorization, ensuring that only authenticated users can execute an action, and look at what happens when you try to execute such an action.
我将在本章中重用这个机场场景来构建一个简单的 Web 应用程序,用于模拟您在机场中采取的步骤。我们已经大致介绍了授权的概念,因此在下一节中,我们将了解如何在 ASP.NET Core 中工作。我们将从最基本的授权级别开始,确保只有经过身份验证的用户才能执行作,并查看当您尝试执行此类作时会发生什么。

24.2 Authorization in ASP.NET Core‌

24.2 ASP.NET Core 中的授权

In this section you’ll see how the authorization principles described in the previous section apply to an ASP.NET Core application. You’ll learn about the role of the [Authorize] attribute and AuthorizationMiddleware in authorizing requests to Razor Pages and MVC actions. Finally, you’ll learn about the process of preventing unauthenticated users from executing endpoints and what happens when users are unauthorized.‌
在本节中,您将了解上一节中描述的授权原则如何应用于 ASP.NET Core 应用程序。你将了解 [Authorize] 属性和 AuthorizationMiddleware 在授权对 Razor Pages 和 MVC作的请求中的作用。最后,您将了解防止未经身份验证的用户执行终端节点的过程,以及当用户未经授权时会发生什么。

The ASP.NET Core framework has authorization built in, so you can use it anywhere in your app, but it’s most common to apply authorization via the AuthorizationMiddleware. The AuthorizationMiddleware should be placed after both the routing middleware and the authentication middleware but before the endpoint middleware, as shown in figure 24.2.
ASP.NET Core 框架内置了授权,因此您可以在应用程序中的任何位置使用它,但最常见的是通过 AuthorizationMiddleware 应用授权。AuthorizationMiddleware 应该放在 routing 中间件和 authentication 中间件之后,但在 endpoint middleware 之前,如图 24.2 所示。

alt text

Figure 24.2 Authorization occurs after an endpoint has been selected and after the request is authenticated, but before the action method or Razor Page endpoint is executed.
图 24.2 在选择终结点之后和对请求进行身份验证之后,但在执行作方法或 Razor Page 终结点之前,进行授权。

NOTE Remember that in ASP.NET Core, an endpoint refers to the handler selected by the routing middleware, which generates a response when executed. It is typically a Razor Page, a web API controller action method, or a minimal API endpoint handler.
注意请记住,在 ASP.NET Core 中,终端节点是指路由中间件选择的处理程序,该处理程序在执行时生成响应。它通常是 Razor Page、Web API 控制器作方法或最小 API 端点处理程序。

With this configuration, the RoutingMiddleware selects an endpoint to execute based on the request’s URL, such as a Razor Page, as you saw in chapter 14. Metadata about the selected endpoint is available to all middleware that occurs after the routing middleware. This metadata includes details about any authorization requirements for the endpoint, and it’s typically attached by decorating an action or Razor Page with an [Authorize] attribute.
使用此配置,RoutingMiddleware 根据请求的 URL 选择要执行的终结点,例如 Razor Page,如第 14 章所示。有关所选终端节点的元数据可用于路由中间件之后出现的所有中间件。此元数据包括有关终结点的任何授权要求的详细信息,通常通过使用 [Authorize] 属性修饰作或 Razor 页面来附加。

The AuthenticationMiddleware deserializes the encrypted cookie (or bearer token for APIs) associated with the request to create a ClaimsPrincipal. This object is set as the HttpContext.User for the request, so all subsequent middleware can access this value. It contains all the Claims that were added to the cookie when the user authenticated.
AuthenticationMiddleware 反序列化与创建 ClaimsPrincipal 的请求关联的加密 Cookie(或 API 的持有者令牌)。此对象设置为请求的 HttpContext.User,因此所有后续中间件都可以访问此值。它包含在用户进行身份验证时添加到 Cookie 的所有声明。

NOTE Remember that the authentication middleware may be placed before the routing middleware when the authentication process is the same for all endpoints.
注意:请记住,当所有端点的身份验证过程都相同时,可以将身份验证中间件放在路由中间件之前。

Nevertheless, I prefer to place it as shown in figure 24.2, after the routing middleware, and always before the authorization middleware.
尽管如此,我更喜欢将它如图 24.2 所示,放在 routing middleware 之后,并且总是放在 authorization middleware 之前。

Now we come to the AuthorizationMiddleware. This middleware checks whether the selected endpoint has any authorization requirements, based on the metadata provided by the RoutingMiddleware. If the endpoint has authorization requirements, the AuthorizationMiddleware uses the HttpContext.User to determine whether the current request is authorized to execute the endpoint.
现在我们来看看 AuthorizationMiddleware。此中间件根据 RoutingMiddleware 提供的元数据检查所选端点是否具有任何授权要求。如果端点有授权要求,则 AuthorizationMiddleware 使用 HttpContext.User 来确定当前请求是否被授权执行端点。

If the request is authorized, the next middleware in the pipeline executes as normal. If the request is not authorized, the AuthorizationMiddleware short-circuits the middleware pipeline, and the endpoint middleware is never executed.
如果请求获得授权,则管道中的下一个中间件将正常执行。如果请求未获得授权,则 AuthorizationMiddleware 将使中间件管道短路,并且永远不会执行端点中间件。

NOTE The call to UseAuthorization() must always be placed after UseRouting() and UseAuthentication(), but before UseEndpoints(). WebApplication automatically adds all this middleware in the correct order, but if you override the position in the pipeline, such as by calling UseRouting(), you must make sure to maintain this overall order.
注意:对 UseAuthorization() 的调用必须始终放在 UseRouting() 和 UseAuthentication() 之后,但在 UseEndpoints() 之前。WebApplication 会自动以正确的顺序添加所有这些中间件,但是如果你覆盖管道中的位置,例如通过调用 UseRouting(),则必须确保保持这个整体顺序。

The AuthorizationMiddleware is responsible for applying authorization requirements and ensuring that only authorized users can execute protected endpoints. In section you’ll learn how to apply the simplest authorization requirement to an endpoint, and in section 24.2.2 you’ll see how the framework responds when a user is not authorized to execute an endpoint.
AuthorizationMiddleware 负责应用授权要求并确保只有授权用户才能执行受保护的端点。在部分您将学习如何将最简单的授权要求应用于 Endpoint,在 Section 24.2.2 中,您将看到当用户无权执行 Endpoint 时框架如何响应。

24.2.1 Preventing anonymous users from accessing your application‌

24.2.1 阻止匿名用户访问您的应用程序

When you think about authorization, you typically think about checking whether a particular user has permission to execute an endpoint. In ASP.NET Core you normally achieve this by checking whether a user has a given claim.
在考虑授权时,通常会考虑检查特定用户是否具有执行终端节点的权限。在 ASP.NET Core 中,通常通过检查用户是否具有给定的声明来实现此目的。

There’s an even more basic level of authorization we haven’t considered yet: allowing only authenticated users to execute an endpoint. This is even simpler than the claims scenario (which we’ll come to later), as there are only two possibilities:
还有一个更基本的授权级别我们还没有考虑:只允许经过身份验证的用户执行端点。这甚至比 claims 场景(我们稍后会介绍)还要简单,因为只有两种可能性:

• The user is authenticated—The action executes as normal.
用户已通过 AUTHENTICATED -作将正常执行。
• The user is unauthenticated—The user can’t execute the endpoint.
用户未经身份验证 - 用户无法执行端点。

You can achieve this basic level of authorization by using the [Authorize] attribute, which you saw in chapter 22 when we discussed authorization filters. You can apply this attribute to your actions and Razor Pages, as shown in the following listing, to restrict them to authenticated (logged-in) users only. If an unauthenticated user tries to execute an action or Razor Page protected with the [Authorize] attribute, they’ll be redirected to the login page.
您可以使用 [Authorize] 属性来实现此基本级别的授权,您在第 22 章讨论授权过滤器时看到了该属性。可以将此属性应用于作和 Razor Pages,如下面的清单所示,以将它们限制为仅经过身份验证(已登录)的用户。如果未经身份验证的用户尝试执行受 [Authorize] 属性保护的作或 Razor 页面,他们将被重定向到登录页面。

Listing 24.1 Applying [Authorize] to an action
清单 24.1 将 [Authorize] 应用于作

public class RecipeApiController : ControllerBase
{
    public IActionResult List() ❶
{
return Ok();
}
[Authorize] ❷
public IActionResult View() ❸
{
return Ok();
}
}

❶ This action can be executed by anyone, even when not logged in.
任何人都可以执行此作,即使未登录。
❷ Applies [Authorize] to individual actions, whole controllers, or Razor Pages
将 [授权] 应用于单个作、整个控制器或 Razor 页面
❸ This action can be executed only by authenticated users.
此作只能由经过身份验证的用户执行。

Applying the [Authorize] attribute to an endpoint attaches metadata to it, indicating that only authenticated users may access the endpoint. As you saw in figure 24.2, this metadata is made available to the AuthorizationMiddleware when an endpoint is selected by the RoutingMiddleware.
将 [Authorize] 属性应用于终端节点会将元数据附加到该终端节点,指示只有经过身份验证的用户才能访问该终端节点。如图 24.2 所示,当 RoutingMiddleware 选择端点时,此元数据可供 AuthorizationMiddleware 使用。

You can apply the [Authorize] attribute at the action scope, controller scope, Razor Page scope, or globally, as you saw in chapter 21. Any action or Razor Page that has the [Authorize] attribute applied in this way can be executed only by an authenticated user. Unauthenticated users will be redirected to the login page.
可以在作范围、控制器范围、Razor Page 范围或全局应用 [Authorize] 属性,如第 21 章所示。以这种方式应用了 [Authorize] 属性的任何作或 Razor 页面只能由经过身份验证的用户执行。未经身份验证的用户将被重定向到登录页面。

TIP There are several ways to apply the [Authorize] attribute globally. You can read about the options and when to choose which option on my blog: http://mng.bz/opQp.
提示:有几种方法可以全局应用 [Authorize] 属性。您可以在我的博客上阅读有关选项以及何时选择哪个选项的信息:http://mng.bz/opQp

Sometimes, especially when you apply the [Authorize] attribute globally, you might need to poke holes in this authorization requirement. If you apply the [Authorize] attribute globally, any unauthenticated requests are redirected to the login page for your app. But if the [Authorize] attribute is global, when the login page tries to load, you’ll be unauthenticated and redirected to the login page again. And now you’re stuck in an infinite redirect loop.
有时,尤其是在全局应用 [Authorize] 属性时,可能需要在此授权要求中戳漏洞。如果全局应用 [Authorize] 属性,则任何未经身份验证的请求都将重定向到应用的登录页面。但是,如果 [Authorize] 属性是全局属性,则当登录页尝试加载时,你将被取消身份验证并再次重定向到登录页。现在你陷入了一个无限的重定向循环中。

To get around this, you can direct specific endpoints to ignore the [Authorize] attribute by applying the [AllowAnonymous] attribute to an action or Razor Page, as shown in the next listing. This allows unauthenticated users to execute the action, so you can avoid the redirect loop that would otherwise result.
若要解决此问题,可以通过将 [AllowAnonymous] 属性应用于作或 Razor 页面,指示特定终结点忽略 [Authorize] 属性,如下一个列表所示。这允许未经身份验证的用户执行作,因此您可以避免否则会导致的重定向循环。

Listing 24.2 Applying [AllowAnonymous] to allow unauthenticated access
清单 24.2 应用 [AllowAnonymous] 以允许未经身份验证的访问

[Authorize] ❶
public class AccountController : ControllerBase
{
public IActionResult ManageAccount() ❷
{
return Ok();
}
[AllowAnonymous] ❸
public IActionResult Login() ❹
{
return Ok();
}
}

❶ Applied at the controller scope, so the user must be authenticated for all actions on the controller
在控制器范围内应用,因此用户必须对控制器上的所有作进行身份验证
❷ Only authenticated users may execute ManageAccount.
只有经过身份验证的用户才能执行 ManageAccount。
❸ [AllowAnonymous] overrides [Authorize] to allow unauthenticated users.
[AllowAnonymous] 覆盖 [Authorize] 以允许未经身份验证的用户。
❹ Login can be executed by anonymous users.
匿名用户可以执行登录。

WARNING If you apply the [Authorize] attribute globally, be sure to add the [AllowAnonymous] attribute to your login actions, error actions, password reset actions, and any other actions that you need unauthenticated users to execute. If you’re using the default Identity UI described in chapter 23, this is already configured for you.
警告:如果全局应用 [Authorize] 属性,请确保将 [AllowAnonymous] 属性添加到登录作、错误作、密码重置作以及需要未经身份验证的用户执行的任何其他作。如果您使用的是第 23 章中描述的默认身份 UI,则已为您配置了此 UI。

If an unauthenticated user attempts to execute an action protected by the [Authorize] attribute, traditional web apps redirect them to the login page. But what about APIs that don’t have a user interface? And what about more complex scenarios, where a user is logged in but doesn’t have the necessary claims to execute an action? In section we’ll look at how the ASP.NET Core authentication services handle all this for you.
如果未经身份验证的用户尝试执行受 [Authorize] 属性保护的作,则传统 Web 应用程序会将其重定向到登录页面。但是没有用户界面的 API 呢?对于更复杂的情况,即用户已登录但没有执行作所需的声明,该怎么办?在部分我们将了解 ASP.NET Core 身份验证服务如何为您处理所有这些。

24.2.2 Handling unauthorized requests‌

24.2.2 处理未经授权的请求

In the previous section you saw how to apply the [Authorize] attribute to an action to ensure that only authenticated users can execute it. In section 24.3 we’ll look at more complex examples that require you to also have a specific claim. In both cases, you must meet one or more authorization requirements (for example, you must be authenticated) to execute the action.‌
在上一节中,您了解了如何将 [Authorize] 属性应用于作,以确保只有经过身份验证的用户才能执行该作。在 Section 24.3 中,我们将查看更复杂的示例,这些示例要求您也有一个特定的声明。在这两种情况下,您都必须满足一个或多个授权要求(例如,您必须经过身份验证)才能执行作。

If the user meets the authorization requirements, the request passes unimpeded through the AuthorizationMiddleware, and the endpoint is executed in the EndpointMiddleware. If they don’t meet the requirements for the selected endpoint, the AuthorizationMiddleware will short-circuit the request. Depending on why the request failed authorization, the AuthorizationMiddleware generates one of two different types of responses, as shown in figure 24.3:
如果用户满足授权要求,则请求通过 AuthorizationMiddleware 畅通无阻,端点在 EndpointMiddleware 中执行。如果它们不满足所选终端节点的要求,则 AuthorizationMiddleware 将使请求短路。根据请求授权失败的原因, AuthorizationMiddleware 生成两种不同类型的响应之一,如图 24.3 所示:

• Challenge—This response indicates that the user was not authorized to execute the action because they weren’t yet logged in.
• 质询 - 此响应表示用户由于尚未登录而无权执行作。

• Forbid—This response indicates that the user was logged in but didn’t meet the requirements to execute the action. They didn’t have a required claim, for example.
• 禁止 - 此响应表示用户已登录,但不符合执行作的要求。例如,他们没有必需的索赔。

alt text

Figure 24.3 The three types of response to an authorization attempt. In the left example, the request contains an authentication cookie, so the user is authenticated in the AuthenticationMiddleware. The AuthorizationMiddleware confirms that the authenticated user can access the selected endpoint, so the endpoint is executed. In the center example, the request is not authenticated, so the Authorization- Middleware generates a challenge response. In the right example, the request is authenticated, but the user does not have permission to execute the endpoint, so a forbid response is generated.
图 24.3 对授权尝试的三种响应类型。在左侧示例中,请求包含一个身份验证 cookie,因此在 AuthenticationMiddleware 中对用户进行身份验证。AuthorizationMiddleware 确认经过身份验证的用户可以访问选定的终端节点,因此将执行终端节点。在中间的示例中,请求未经过身份验证,因此 Authorization- Middleware 生成质询响应。在正确的示例中,请求已经过身份验证,但用户没有执行终端节点的权限,因此会生成 forbid 响应。

NOTE If you apply the [Authorize] attribute in basic form, as you did in section 24.2.1, you will generate only challenge responses. In this case, a challenge response will be generated for unauthenticated users, but authenticated users will always be authorized.
注意:如果以基本形式应用 [Authorize] 属性,就像在第 24.2.1 节中所做的那样,您将仅生成质询响应。在这种情况下,将为未经身份验证的用户生成质询响应,但经过身份验证的用户将始终获得授权。

The exact HTTP response generated by a challenge or forbid response typically depends on the type of application you’re building and so the type of authentication your application uses: a traditional web application with Razor Pages, or an API application.
质询或禁止响应生成的确切 HTTP 响应通常取决于要构建的应用程序类型,因此应用程序使用的身份验证类型:具有 Razor Pages 的传统 Web 应用程序或 API 应用程序。

For traditional web apps using cookie authentication, such as when you use ASP.NET Core Identity, as in chapter 23, the challenge and forbid responses generate an HTTP redirect to a page in your application. A challenge response indicates the user isn’t yet authenticated, so they’re redirected to the login page for the app. After logging in, they can attempt to execute the protected resource again. A forbid response means the request was from a user that already logged in, but they’re still not allowed to execute the action.
对于使用 Cookie 身份验证的传统 Web 应用程序,例如当您使用 ASP.NET Core Identity 时(如第 23 章所示),challenge 和 forbid 响应会生成指向应用程序中页面的 HTTP 重定向。质询响应指示用户尚未通过身份验证,因此他们将被重定向到应用程序的登录页面。登录后,他们可以尝试再次执行受保护的资源。禁止响应表示请求来自已登录的用户,但仍不允许他们执行该作。

Consequently, the user is redirected to a “forbidden” or “access denied” web page, as shown in figure 24.4, which informs them they can’t execute the action or Razor Page.
因此,用户将被重定向到 “forbidden” 或 “access denied” 网页,如图 24.4 所示,该网页通知他们无法执行作或 Razor Page。

alt text

Figure 24.4 A forbid response in traditional web apps using cookie authentication. If you don’t have permission to execute a Razor Page and you’re already logged in, you’ll be redirected to an “access denied” page.
图 24.4 传统 Web 应用程序中使用 cookie 身份验证的 forbid 响应。如果您没有执行 Razor 页面的权限,并且您已经登录,您将被重定向到“拒绝访问”页面。

The preceding behavior is standard for traditional web apps, but API apps typically use a different approach to authentication, as you’ll see in chapter 25. Instead of logging in and using the API directly, you’d typically log in to a third- party application that provides a token to the client-sidesingle-page application (SPA) or mobile app. The client-side app sends this token when it makes a request to your API.
上述行为是传统 Web 应用程序的标准行为,但 API 应用程序通常使用不同的身份验证方法,如第 25 章所示。您通常不会直接登录并使用 API,而是登录到向客户端提供令牌的第三方应用程序单页应用程序 (SPA) 或移动应用程序。客户端应用程序在向 API 发出请求时发送此令牌。

Authenticating a request for an API app is essentially identical to a traditional web app that uses cookies, as you’ll see in chapter 25; AuthenticationMiddleware deserializes the credentials to create the ClaimsPrincipal. The difference is in how an API handles authorization failures.
对 API 应用程序的请求进行身份验证与使用 cookie 的传统 Web 应用程序基本相同,如第 25 章所示;AuthenticationMiddleware 反序列化凭据以创建 ClaimsPrincipal。区别在于 API 处理授权失败的方式。

When an API app generates a challenge response, it returns a 401 Unauthorized error response to the caller. Similarly, when the app generates a forbid response, it returns a 403 Forbidden response. The traditional web app essentially handled these errors by automatically redirecting unauthorized users to the login or “access denied” page, but the API app doesn’t do this. It’s up to the client-side SPA or mobile app to detect these errors and handle them as appropriate.
当 API 应用生成质询响应时,它会向调用方返回 401 Unauthorized 错误响应。同样,当应用程序生成 forbid 响应时,它会返回 403 Forbidden 响应。传统的 Web 应用程序基本上是通过自动将未经授权的用户重定向到登录或 “access denied” 页面来处理这些错误,但 API 应用程序不会这样做。客户端 SPA 或移动应用程序负责检测这些错误并根据需要处理它们。

TIP This difference in authorization behavior is one of the reasons I generally recommend creating separate apps for your APIs and Razor pages apps; it’s possible to have both in the same app, but the configuration is often more complex.
提示:授权行为的这种差异是我通常建议为您的 API 和 Razor 页面应用程序创建单独应用程序的原因之一;可以在同一个应用程序中同时拥有两者,但配置通常更复杂。

The different behavior between traditional web apps and SPAs can be confusing initially, but you generally don’t need to worry about that too much in practice. Whether you’re building an API app or a traditional MVC web app, the authorization code in your app looks the same in both cases.
传统 Web 应用程序和 SPA 之间的不同行为最初可能会令人困惑,但在实践中,您通常不需要太担心这一点。无论您是构建 API 应用程序还是传统的 MVC Web 应用程序,应用程序中的授权代码在这两种情况下看起来都相同。

Apply [Authorize] attributes to your endpoints, and let the framework take care of the differences for you.
将 [Authorize] 属性应用于您的终端节点,让框架为您处理差异。

NOTE In chapter 23 you saw how to configure ASP.NET Core Identity in a Razor Pages app. This chapter assumes that you’re building a Razor Pages app too, but the chapter is equally applicable if you’re building an API, as you’ll see in chapter 25. Authorization policies are applied in the same way, whichever style of app you’re building. Only the final response of unauthorized requests differs.
注意:在第 23 章中,你了解了如何在 Razor Pages 应用中配置 ASP.NET 核心标识。本章假定你也在构建 Razor Pages 应用,但如果你正在构建 API,则本章同样适用,如第 25 章所示。无论您正在构建哪种风格的应用程序,授权策略都以相同的方式应用。只有未授权请求的最终响应不同。

You’ve seen how to apply the most basic authorization requirement—restricting an endpoint to authenticated users —but most apps need something more subtle than this all-or- nothing approach. Consider the airport scenario from section 24.1. Being authenticated (having a passport) isn’t enough to get you through security. Instead, you also need a specific claim: BoardingPassNumber. In the next section we’ll look at how you can implement a similar requirement in ASP.NET Core.‌
您已经了解了如何应用最基本的授权要求,即将终端节点限制为经过身份验证的用户,但大多数应用程序需要比这种全有或全无方法更微妙的东西。考虑 24.1 节中的机场场景。通过身份验证(拥有护照)不足以让您通过安检。相反,您还需要一个特定的声明:BoardingPassNumber。在下一节中,我们将了解如何在 ASP.NET Core 中实现类似的要求。

24.3 Using policies for claims- based authorization‌

24.3 使用策略进行基于声明的授权

In the previous section, you saw how to require that users be logged in to access an endpoint. In this section you’ll see how to apply additional requirements. You’ll learn to use authorization policies to perform claims-based authorization to require that a logged-in user have the required claims to execute a given endpoint.
在上一节中,您了解了如何要求用户登录才能访问终端节点。在本节中,您将了解如何应用其他要求。您将学习如何使用授权策略来执行基于声明的授权,以要求登录用户具有执行给定终端节点所需的声明。

In chapter 23 you saw that authentication in ASP.NET Core centers on a ClaimsPrincipal object, which represents the user. This object has a collection of claims that contain pieces of information about the user, such as their name, email, and date of birth.
在第 23 章中,您看到 ASP.NET Core 中的身份验证以表示用户的 ClaimsPrincipal 对象为中心。此对象具有一组声明,其中包含有关用户的信息片段,例如其姓名、电子邮件和出生日期。

You can use this information to customize the app for each user, by displaying a welcome message addressing the user by name, for example, but you can also use claims for authorization. For example, you might authorize a user only if they have a specific claim (such as BoardingPassNumber) or if a claim has a specific value (FrequentFlyerClass claim with the value Gold).
您可以使用此信息为每个用户自定义应用程序,例如,通过显示按名称称呼用户的欢迎消息,但您也可以使用声明进行授权。例如,仅当用户具有特定声明(如 BoardingPassNumber)或声明具有特定值(值为 Gold 的 FrequentFlyerClass 声明)时,你才可以授权用户。

In ASP.NET Core the rules that define whether a user is authorized are encapsulated in a policy.
在 ASP.NET Core 中,定义用户是否获得授权的规则封装在策略中。

DEFINITION A policy defines the requirements you must meet for a request to be authorized.
定义:策略定义要授权请求必须满足的要求。

Policies can be applied to an endpoint using the [Authorize] attribute, similar to the way you saw in section 24.2.1. This listing shows a Razor Page PageModel that represents the first authorization step in the airport scenario. The AirportSecurity.cshtml Razor Page is protected by an [Authorize] attribute, but you’ve also provided a policy name, "CanEnterSecurity", as shown in the following listing.
可以使用 [Authorize] 属性将策略应用于终端节点,类似于您在第 24.2.1 节中看到的方式。此列表显示了一个 Razor Page PageModel,它表示 airport 场景中的第一个授权步骤。AirportSecurity.cshtml Razor 页面受 [Authorize] 属性保护,但你还提供了策略名称“CanEnterSecurity”,如以下列表所示。

Listing 24.3 Applying an authorization policy to a Razor Page
清单 24.3 将授权策略应用于 Razor 页面

[Authorize("CanEnterSecurity")] ❶
public class AirportSecurityModel : PageModel
{
public void OnGet() ❷
{
}
}

❶ Applying the “CanEnterSecurity” policy using [Authorize]
使用 [Authorize]应用“CanEnterSecurity”策略

❷ Only users that satisfy the “CanEnterSecurity” policy can execute the Razor Page.
只有满足“CanEnterSecurity”策略的用户才能执行 Razor 页面。

If a user attempts to execute the AirportSecurity.cshtml Razor Page, the authorization middleware verifies whether the user satisfies the policy’s requirements (we’ll look at the policy itself shortly). This gives one of three possible outcomes:
如果用户尝试执行 AirportSecurity.cshtml Razor 页面,授权中间件会验证用户是否满足策略的要求(我们稍后将介绍策略本身)。这将给出以下三种可能的结果之一:

• The user satisfies the policy—The middleware pipeline continues, and the EndpointMiddleware executes the Razor Page as normal.
用户满足策略 - 中间件管道继续,并且 EndpointMiddleware 照常执行 Razor Page。

• The user is unauthenticated—The user is redirected to the login page.
用户未通过 IF - 用户被重定向到登录页面。

• The user is authenticated but doesn’t satisfy the policy—The user is redirected to a “forbidden” or “access denied” page.
用户已通过身份验证,但不满足策略 - 用户被重定向到“禁止”或“拒绝访问”页面。

These three outcomes correlate with real-life outcomes you might expect when trying to pass through security at the airport:
这三种结果与您在尝试通过机场安检时可能预期的现实结果相关:

• You have a valid boarding pass—You can enter security as normal.
您持有有效的登机牌 - 您可以正常进入安检。

• You don’t have a boarding pass—You’re redirected to purchase a ticket.
您没有登机牌 - 您将被重定向到购买机票。

• Your boarding pass is invalid (you turned up a day late, for example)—You’re blocked from entering.
您的登机牌无效(例如,您迟到一天)- 您被阻止进入。

Listing 24.3 shows how you can apply a policy to a Razor Page using the [Authorize] attribute, but you still need to define the CanEnterSecurity policy.
清单 24.3 显示了如何使用 [Authorize] 属性将策略应用于 Razor 页面,但您仍然需要定义 CanEnterSecurity 策略。

You add policies to an ASP.NET Core application in Program.cs, as shown in listing 24.4. First, you add the authorization services and return an AuthorizationBuilder object using AddAuthorizationBuilder(). You can then add policies to the builder by calling AddPolicy(). You define the policy itself by calling methods in a lambda method on a AuthorizationPolicyBuilder (called policyBuilder here).
您可以在 Program.cs 中将策略添加到 ASP.NET Core 应用程序,如清单 24.4 所示。首先,添加授权服务并使用 AddAuthorizationBuilder() 返回 AuthorizationBuilder 对象。然后,您可以通过调用 AddPolicy() 将策略添加到生成器中。您可以通过在 AuthorizationPolicyBuilder(此处称为 policyBuilder)上调用 lambda 方法中的方法来定义策略本身。

Listing 24.4 Adding an authorization policy using AuthorizationPolicyBuilder
清单 24.4 使用 AuthorizationPolicyBuilder 添加授权策略

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorizationBuilder() ❶
.AddPolicy( ❷
"CanEnterSecurity", ❸
policyBuilder => policyBuilder ❹
.RequireClaim("BoardingPassNumber")); ❹
});
// Additional configuration

❶ Calls AddAuthorizationBuilder to add the required authorization services
调用 AddAuthorizationBuilder 以添加所需的授权服务
❷ Adds a new policy
添加新策略
❸ Provides a name for the policy
为策略提供名称
❹ Defines the policy requirements using AuthorizationPolicyBuilder
使用 AuthorizationPolicyBuilder 定义策略要求

When you call AddPolicy you provide a name for the policy, which should match the value you use in your [Authorize] attributes, and you define the requirements of the policy. In this example, you have a single simple requirement: the user must have a claim of type BoardingPassNumber. If a user has this claim, whatever its value, the policy is satisfied, and the user will be authorized.
调用 AddPolicy 时,您需要为策略提供一个名称,该名称应与您在 [Authorize] 属性中使用的值匹配,并定义策略的要求。在此示例中,您有一个简单的要求:用户必须具有 BoardingPassNumber 类型的声明。如果用户具有此声明,则无论其值如何,都满足策略,并且用户将获得授权。

NOTE A claim is information about the user, as a key-value pair. A policy defines the requirements for successful authorization. A policy may require that a user have a given claim, or it may specify more complex requirements, as you’ll see shortly.
注意:声明是有关用户的信息,以键值对的形式。策略定义成功授权的要求。策略可能要求用户具有给定的声明,或者它可能指定更复杂的要求,您很快就会看到。

AuthorizationPolicyBuilder contains several methods for creating simple policies like this, as shown in table 24.1. For example, an overload of the RequireClaim() method lets you specify a specific value that a claim must have. The following would let you create a policy where the "BoardingPassNumber" claim must have a value of "A1234":
AuthorizationPolicyBuilder包含几种用于创建此类简单策略的方法,如表 24.1 所示。例如,RequireClaim() 方法的重载允许您指定声明必须具有的特定值。下面将允许你创建一个策略,其中 “BoardingPassNumber” 声明的值必须为“A1234”:

policyBuilder => policyBuilder.RequireClaim("BoardingPassNumber", "A1234");

Table 24.1 Simple policy builder methods on AuthorizationPolicyBuilder
表 24.1 AuthorizationPolicyBuilder 上的简单策略生成器方法

Method Policy behavior
RequireAuthenticatedUser() The required user must be authenticated. Creates a policy similar to the default [Authorize] attribute, where you don’t set a policy.
RequireClaim(claim, values) The user must have the specified claim. If provided, the claim must be one of the specified values.
RequireUsername(username) The user must have the specified username.
RequireAssertion(function) Executes the provided lambda function, which returns a bool, indicating whether the policy was satisfied.

Role-based authorization vs. claims-based authorization
基于角色的授权与基于声明的授权

If you look at all of the methods available on the AuthorizationPolicyBuilder type using IntelliSense, you might notice that there’s a method I didn’t mention in table 24.1: RequireRole(). This is a remnant of the role-based approach to authorization used in previous versions of ASP.NET, and I don’t recommend using it.
如果您使用 IntelliSense 查看 AuthorizationPolicyBuilder 类型上的所有可用方法,您可能会注意到我在表 24.1 中没有提到的方法:RequireRole()。这是 ASP.NET 早期版本中使用的基于角色的授权方法的残余部分,我不建议使用它。

Before Microsoft adopted the claims-based authorization used by ASP.NET, role-based authorization was the norm. Users were assigned to one or more roles, such as Administrator or Manager, and authorization involved checking whether the current user was in the required role.‌
在 Microsoft 采用 ASP.NET 使用的基于声明的授权之前,基于角色的授权是常态。将用户分配给一个或多个角色,例如 Administrator 或 Manager,授权涉及检查当前用户是否位于所需的角色中。

This role-based approach to authorization is possible in ASP.NET Core, but it’s used primarily for legacy compatibility reasons. Claims-based authorization is the suggested approach. Unless you’re porting a legacy app that uses roles, I suggest that you embrace claims-based authorization and leave those roles behind.
这种基于角色的授权方法在 ASP.NET Core 中是可能的,但它主要用于旧版兼容性原因。基于声明的授权是建议的方法。除非您要移植使用角色的旧应用程序,否则我建议您采用基于声明的授权,并将这些角色抛在脑后。

Note that the fact that you’re using claims-based permissions doesn’t mean you need to get rid of roles entirely, but you should use roles as a basis for assigning claims to a user rather than authorize that a user belongs to one or more roles.
请注意,使用基于声明的权限这一事实并不意味着需要完全删除角色,但应使用角色作为将声明分配给用户的基础,而不是授权用户属于一个或多个角色。

You can use these methods to build simple policies that can handle basic situations, but often you’ll need something more complicated. What if you want to create a policy that enforces that only users over the age of 18 can execute an endpoint?
您可以使用这些方法来构建可以处理基本情况的简单策略,但通常需要更复杂的策略。如果要创建一个策略,强制要求只有 18 岁以上的用户才能执行终端节点,该怎么办?

The DateOfBirth claim provides the information you need, but there’s no single correct value, so you couldn’t use the RequireClaim() method. You could use the RequireAssertion() method and provide a function that calculates the age from the DateOfBirth claim, but that could get messy pretty quickly.
DateOfBirth 声明提供所需的信息,但没有单个正确的值,因此无法使用 RequireClaim() 方法。您可以使用 RequireAssertion() 方法并提供一个函数,该函数根据 DateOfBirth 声明计算年龄,但这很快就会变得混乱。

For more complex policies that can’t be easily defined using the RequireClaim() method, I recommend that you take a different approach and create a custom policy, as you’ll see in the following section.‌
对于无法使用 RequireClaim() 方法轻松定义的更复杂的策略,我建议您采用不同的方法并创建自定义策略,如下一节所示。

27.4 Creating custom policies for authorization‌

27.4 创建自定义授权策略

You’ve already seen how to create a policy by requiring a specific claim or requiring a specific claim with a specific value, but often the requirements will be more complex than that. In this section you’ll learn how to create custom authorization requirements and handlers. You’ll also see how to configure authorization requirements where there are multiple ways to satisfy a policy, any of which are valid.
您已经了解了如何通过要求特定声明或要求具有特定值的特定声明来创建策略,但要求通常比这更复杂。在本节中,您将了解如何创建自定义授权要求和处理程序。您还将了解如何配置授权要求,其中有多种方法可以满足策略,其中任何一种都是有效的。

Let’s return to the airport example. You’ve already configured the policy for passing through security, and now you’re going to configure the policy that controls whether you’re authorized to enter the airline lounge.
让我们回到机场的例子。您已经配置了通过安检的策略,现在您将配置控制您是否有权进入航空公司休息室的策略。

As you saw in figure 24.1, you’re allowed to enter the lounge if you have a FrequentFlyerClass claim with a value of Gold. If this was the only requirement, you could use AuthorizationPolicyBuilder to create a policy like this:
如图 24.1 所示,如果您有价值为 Gold 的 FrequentFlyerClass 索赔,则可以进入休息室。如果这是唯一的要求,则可以使用 AuthorizationPolicyBuilder 创建如下所示的策略:

options.AddPolicy("CanAccessLounge", policyBuilder => policyBuilder.RequireClaim("FrequentFlyerClass", "Gold"));

But what if the requirements are more complicated? For example, suppose you can enter the lounge if you’re at least 18 years old (as calculated from the DateOfBirth claim) and you’re one of the following:
但是,如果要求更复杂呢?例如,假设您至少年满 18 岁(根据 DateOfBirth 声明计算)并且您是以下之一,则可以进入休息室:

• You’re a Gold-class frequent flyer (have a FrequentFlyerClass claim with value "Gold")
您是金卡级常旅客(有价值为“金卡”的 FrequentFlyerClass 索赔)
• You’re an employee of the airline (have an EmployeeNumber claim).
您是航空公司的员工(有 EmployeeNumber 索赔)。

If you’ve ever been banned from the lounge (you have an IsBannedFromLounge claim), you won’t be allowed in, even if you satisfy the other requirements.
如果您曾经被禁止进入休息室(您有 IsBannedFromLounge 索赔),即使您满足其他要求,也不会被允许进入。

There’s no way of achieving this complex set of requirements with the basic use of AuthorizationPolicyBuilder you’ve seen so far. Luckily, these methods are a wrapper around a set of building blocks that you can combine to achieve the desired policy.
到目前为止,您所看到的 AuthorizationPolicyBuilder 的基本用法无法实现这组复杂的要求。幸运的是,这些方法是一组构建块的包装器,您可以组合这些构建块来实现所需的策略。

24.4.1 Requirements and handlers: The building blocks of a policy‌

24.4.1 要求和处理程序:策略的构建块

Every policy in ASP.NET Core consists of one or more requirements, and every requirement can have one or more handlers. For the airport lounge example, you have a single policy ("CanAccessLounge"), two requirements (MinimumAgeRequirement and AllowedInLoungeRequirement), and several handlers, as shown in figure 24.5.
ASP.NET Core 中的每个策略都包含一个或多个要求,每个要求可以有一个或多个处理程序。对于机场休息室示例,您有一个策略(“CanAccessLounge”)、两个要求(MinimumAgeRequirement 和 AllowedInLoungeRequirement)和多个处理程序,如图 24.5 所示。

alt text

Figure 24.5 A policy can have many requirements, and every requirement can have many handlers. By combining multiple requirements in a policy and providing multiple handler implementations, you can create complex authorization policies that meet any of your business requirements.
图 24.5 一个策略可以有很多需求,每个需求可以有很多处理程序。通过在策略中组合多个要求并提供多个处理程序实施,您可以创建满足任何业务需求的复杂授权策略。

For a policy to be satisfied, a user must fulfill all the requirements. If the user fails any of the requirements, the authorize middleware won’t allow the protected endpoint to be executed. In this example, a user must be allowed to access the lounge and must be over 18 years old.
要满足策略,用户必须满足所有要求。如果用户不符合任何要求,则 authorize 中间件将不允许执行受保护的终端节点。在此示例中,必须允许用户访问休息室,并且必须年满 18 岁。

Each requirement can have one or more handlers, which will confirm that the requirement has been satisfied. For example, as shown in figure 24.5, AllowedInLoungeRequirement has two handlers that can satisfy the requirement:
每个要求都可以有一个或多个处理程序,这些处理程序将确认已满足要求。例如,如图 24.5 所示,AllowedInLoungeRequirement 有两个可以满足要求的处理程序:

• FrequentFlyerHandler
• IsAirlineEmployeeHandler

If the user satisfies either of these handlers, AllowedInLoungeRequirement is satisfied. You don’t need all handlers for a requirement to be satisfied; you need only one.
如果用户满足其中任一处理程序,则满足 AllowedInLoungeRequirement。您不需要满足所有处理程序即可满足需求;你只需要一个。

NOTE Figure 24.5 shows a third handler,BannedFromLoungeHandler, which I’ll cover in section 24.4.2. It’s slightly different in that it can fail a requirement but not satisfy it.
注意:图 24.5 显示了第三个处理程序 BannedFromLoungeHandler,我将在 24.4.2 节中介绍。它略有不同,因为它可能不符合要求,但不能满足它。

You can use requirements and handlers to achieve most any combination of behavior you need for a policy. By combining handlers for a requirement, you can validate conditions using a logical OR: if any of the handlers is satisfied, the requirement is satisfied. By combining requirements, you create a logical AND: all the requirements must be satisfied for the policy to be satisfied, as shown in figure 24.6.‌
您可以使用要求和处理程序来实现策略所需的大多数行为组合。通过组合需求的处理程序,您可以使用逻辑 OR 验证条件:如果满足任何处理程序,则满足需求。通过组合需求,您可以创建一个逻辑 AND:必须满足所有要求才能满足策略,如图 24.6 所示。

alt text

Figure 24.6 For a policy to be satisfied, every requirement must be satisfied. A requirement is satisfied if any of the handlers is satisfied.
图 24.6 要满足策略,必须满足所有要求。如果满足任何处理程序,则满足要求。

TIP You can add multiple policies to a Razor Page or action method by applying the [Authorize] attribute multiple times, as in [Authorize ("Policy1"), Authorize("Policy2")]. All policies must be satisfied for the request to be authorized.
提示:可以通过多次应用 [Authorize] 属性,将多个策略添加到 Razor 页面或作方法,如 [Authorize (“Policy1”), Authorize(“Policy2”)] 中所示。必须满足所有策略,请求才能获得授权。

I’ve highlighted requirements and handlers that will make up your "CanAccessLounge" policy, so in the next section you’ll build each of the components and apply them to the airport sample app.
我已经重点介绍了构成“CanAccessLounge”策略的要求和处理程序,因此在下一节中,您将构建每个组件并将它们应用于 airport 示例应用程序。

24.4.2 Creating a policy with a custom requirement and handler‌

24.4.2 创建具有自定义要求和处理程序的策略

You’ve seen all the pieces that make up a custom authorization policy, so in this section we’ll explore the implementation of the "CanAccessLounge" policy.
您已经了解了构成自定义授权策略的所有部分,因此在本节中,我们将探讨 “CanAccessLounge” 策略的实现。

CREATING AN IAUTHORIZATIONREQUIREMENT TO REPRESENT A REQUIREMENT
创建 IAUTHORIZATIONREQUIREMENT 以表示要求

As you’ve seen, a custom policy can have multiple requirements, but what is a requirement in code terms? Authorization requirements in ASP.NET Core are any class that implements the IAuthorizationRequirement interface. This is a blank marker interface, which you can apply to any class to indicate that it represents a requirement.
如您所见,自定义策略可以有多个要求,但代码术语中的要求是什么?ASP.NET Core 中的授权要求是实现 IAuthorizationRequirement 接口的任何类。这是一个空白标记接口,您可以将其应用于任何类,以指示它表示要求。

If the interface doesn’t have any members, you might be wondering what the requirement class needs to look like. Typically, they’re simple plain old CLR object (POCO) classes. The following listing shows AllowedInLoungeRequirement, which is about as simple as a requirement can get. It has no properties or methods; it implements the required IAuthorizationRequirement interface.
如果接口没有任何成员,您可能想知道 requirement 类需要是什么样子。通常,它们是简单的普通旧 CLR 对象 (POCO) 类。下面的清单显示了 AllowedInLoungeRequirement,这与要求所能获得的最低要求差不多。它没有属性或方法;它实现所需的 IAuthorizationRequirement 接口。

Listing 24.5 AllowedInLoungeRequirement

public class AllowedInLoungeRequirement
    : IAuthorizationRequirement { } ❶

❶ The interface identifies the class as an authorization requirement.
接口将类标识为授权要求。

This is the simplest form of requirement, but it’s also common to have one or two properties that make the requirement more generalized. For example, instead of creating the highly specific MustBe18YearsOldRequirement, you could create a parameterized MinimumAgeRequirement, as shown in the following listing. By providing the minimum age as a parameter to the requirement, you can reuse the requirement for other policies with different minimum-age requirements.
这是最简单的要求形式,但通常具有一两个使要求更通用的属性。例如,您可以创建参数化的 MinimumAgeRequirement,而不是创建高度具体的 MustBe18YearsOldRequirement,如下面的清单所示。通过提供最低年龄作为要求的参数,您可以将该要求重新用于具有不同最低年龄要求的其他保单。

Listing 24.6 The parameterized MinimumAgeRequirement
清单 24.6 参数化的 MinimumAgeRequirement

public class MinimumAgeRequirement : IAuthorizationRequirement ❶
{
public MinimumAgeRequirement(int minimumAge) ❷
{
MinimumAge = minimumAge;
}
public int MinimumAge { get; } ❸
}

❶ The interface identifies the class as an authorization requirement.
接口将类标识为授权要求。

❷ The minimum age is provided when the requirement is created.
创建要求时提供最低年龄。

❸ Handlers can use the exposed minimum age to determine whether the requirement is satisfied.
处理程序可以使用公开的最小年龄来确定是否满足要求。

The requirements are the easy part. They represent each of the components of the policy that must be satisfied for the policy to be satisfied overall. Note that requirements are meant to be lightweight objects that can be created “manually.” So while you can have constructor parameters, as shown in listing 24.6, you can’t use dependency injection (DI) here. That’s not as limiting as it sounds, because your handlers can use DI.
要求是最简单的部分。它们表示策略的每个组成部分,必须满足这些组成部分才能使策略总体上得到满足。请注意,需求是可以 “手动” 创建的轻量级对象。因此,虽然你可以有构造函数参数,如清单 24.6 所示,但你不能在这里使用依赖注入 (DI)。这并不像听起来那么限制,因为你的处理程序可以使用 DI。

CREATING A POLICY WITH MULTIPLE REQUIREMENTS
创建具有多个要求的策略

You’ve created the two requirements, so now you can configure the "CanAccessLounge" policy to use them. You configure your policies as you did before, in Program.cs.
您已经创建了这两个要求,因此现在可以配置 “CanAccessLounge” 策略来使用它们。您可以像以前一样在 Program.cs 中配置策略。

Listing 24.7 shows how to do this by creating an instance of each requirement and passing them to AuthorizationPolicyBuilder. The authorization handlers use these requirement objects when attempting to authorize the policy.
清单 24.7 展示了如何通过创建每个需求的实例并将它们传递给AuthorizationPolicyBuilder来做到这一点。授权处理程序在尝试授权策略时使用这些要求对象。

Listing 24.7 Creating an authorization policy with multiple requirements
清单 24.7 创建具有多个需求的授权策略

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.services.AddAuthorization(options =>
{ ❶
options.AddPolicy( ❶
"CanEnterSecurity", ❶
policyBuilder => policyBuilder ❶
.RequireClaim(Claims.BoardingPassNumber)); ❶
options.AddPolicy( ❷
"CanAccessLounge", ❷
policyBuilder => policyBuilder.AddRequirements( ❸
new MinimumAgeRequirement(18), ❸
new AllowedInLoungeRequirement() ❸
));
});
// Additional configuration

❶ Adds the previous simple policy for passing through security
新增之前简单的通过安检策略

❷ Adds a new policy for the airport lounge, called CanAccessLounge
为机场贵宾室添加一项名为 CanAccessLounge 的新政策
❸ Adds an instance of each IAuthorizationRequirement object
添加每个 IAuthorizationRequirement 对象的实例

You now have a policy called "CanAccessLounge" with two requirements, so you can apply it to a Razor Page or action method using the [Authorize] attribute, in exactly the same way you did for the "CanEnterSecurity" policy:
现在,您有一个名为“CanAccessLounge”的策略,其中包含两个要求,因此您可以使用 [Authorize] 属性将其应用于 Razor 页面或作方法,其方式与对“CanEnterSecurity”策略执行的作完全相同:

[Authorize("CanAccessLounge")]
public class AirportLoungeModel : PageModel
{
public void OnGet() { }
}

When a request is routed to the AirportLounge.cshtml Razor Page, the authorize middleware executes the authorization policy and each of the requirements is inspected. But you saw earlier that the requirements are purely data; they indicate what needs to be fulfilled, but they don’t describe how that has to happen. For that, you need to write some handlers.
将请求路由到 AirportLounge.cshtml Razor 页面时,authorize 中间件将执行授权策略并检查每个要求。但您之前看到,这些要求纯粹是数据;它们指出了需要满足什么,但没有描述必须如何实现。为此,您需要编写一些处理程序。

CREATING AUTHORIZATION HANDLERS TO SATISFY YOUR REQUIREMENTS
创建授权处理程序以满足您的要求

Authorization handlers contain the logic of how a specific IAuthorizationRequirement can be satisfied. When executed, a handler can do one of three things:
授权处理程序包含如何满足特定 IAuthorizationRequirement 的逻辑。执行时,处理程序可以执行以下三项作之一:

• Mark the requirement handling as a success.
将需求处理标记为成功。

• Do nothing.
什么都不做。

• Explicitly fail the requirement.
明确不符合要求。

Handlers should implement AuthorizationHandler, where T is the type of requirement they handle. For example, the following listing shows a handler for AllowedInLoungeRequirement that checks whether the user has a claim called FrequentFlyerClass with a value of Gold.
处理程序应实现 AuthorizationHandler,其中 T 是它们处理的需求类型。例如,下面的清单显示了 AllowedInLoungeRequirement 的处理程序,该处理程序检查用户是否具有名为 FrequentFlyerClass 且值为 Gold 的声明。

Listing 24.8 FrequentFlyerHandler for AllowedInLoungeRequirement
清单 24.8 AllowedInLoungeRequirement 的 FrequentFlyerHandler

public class FrequentFlyerHandler :
AuthorizationHandler<AllowedInLoungeRequirement> ❶
{
protected override Task HandleRequirementAsync( ❷
AuthorizationHandlerContext context, ❸
AllowedInLoungeRequirement requirement) ❹
{
if(context.User.HasClaim("FrequentFlyerClass", "Gold")) ❺
{
context.Succeed(requirement); ❻
}
return Task.CompletedTask; ❼
}
}

❶ The handler implements AuthorizationHandler<T>.
处理程序实现 AuthorizationHandler<T>

❷ You must override the abstract HandleRequirementAsync method.
您必须重写抽象 HandleRequirementAsync 方法。

❸ The context contains details such as the ClaimsPrincipal user object.
上下文包含诸如 ClaimsPrincipal 用户对象之类的详细信息。

❹ The requirement instance to handle
要处理的要求实例

❺ Checks whether the user has the Frequent-FlyerClass claim with the Gold value
检查用户是否具有值为 Gold 的Frequent-FlyerClass 声明

❻ If the user had the necessary claim, marks the requirement as satisfied by calling Succeed
如果用户具有必要的声明,则通过调用 Succeed将要求标记为满足

❼ If the requirement wasn’t satisfied, does nothing
如果未满足要求,则不执行任何作

This handler is functionally equivalent to the simple RequireClaim() handler you saw at the start of section 24.4, but using the requirement and handler approach instead.
这个处理程序在功能上等同于你在 24.4 节开头看到的简单RequireClaim()处理程序,但使用的是需求和处理程序方法。

When a request is routed to the AirportLounge.cshtml Razor Page, the authorization middleware sees the [Authorize] attribute on the endpoint with the "CanAccessLounge" policy. It loops through all the requirements in the policy and all the handlers for each requirement, calling the HandleRequirementAsync method for each.‌
当请求路由到 AirportLounge.cshtml Razor 页面时,授权中间件会在具有“CanAccessLounge”策略的终结点上看到 [Authorize] 属性。它循环访问策略中的所有要求和每个要求的所有处理程序,并为每个要求调用 HandleRequirementAsync 方法。

The authorization middleware passes the current AuthorizationHandlerContext and the requirement to be checked to each handler. The current ClaimsPrincipal being authorized is exposed on the context as the User property. In listing 24.8, FrequentFlyerHandler uses the context to check for a claim called FrequentFlyerClass with the Gold value, and if it exists, indicates that the user is allowed to enter the airline lounge by calling Succeed().
授权中间件将当前 AuthorizationHandlerContext 和要检查的要求传递给每个处理程序。当前被授权的 ClaimsPrincipal 在上下文中作为 User 属性公开。在列表 24.8 中,FrequentFlyerHandler 使用上下文来检查名为 FrequentFlyerClass 且值为 Gold 的声明,如果存在,则表示允许用户通过调用 Succeed() 进入航空公司休息室。

NOTE Handlers mark a requirement as being satisfied by calling context .Succeed() and passing the requirement as an argument.
注意处理程序通过调用 context 将需求标记为满足。Succeed() 并将需求作为参数传递。

It’s important to note the behavior when the user doesn’t have the claim. FrequentFlyerHandler doesn’t do anything in this case; it returns a completed Task to satisfy the method signature.
请务必注意用户没有声明时的行为。在这种情况下,FrequentFlyerHandler 不执行任何作;它返回一个已完成的 Task 以满足方法签名。

NOTE Remember that if any of the handlers associated with a requirement passes, the requirement is a success. Only one of the handlers must succeed for the requirement to be satisfied.
注意:请记住,如果与要求关联的任何处理程序通过,则要求成功。只有一个处理程序必须成功才能满足要求。

This behavior, whereby you either call context.Succeed() or do nothing, is typical for authorization handlers. The following listing shows the implementation of IsAirlineEmployeeHandler, which uses a similar claim check to determine whether the requirement is satisfied.
此行为,即调用 context.Succeed() 或不执行任何作,是授权处理程序的典型特征。下面的清单显示了 IsAirlineEmployeeHandler 的实现,它使用类似的声明检查来确定是否满足要求。

Listing 24.9 IsAirlineEmployeeHandler

public class IsAirlineEmployeeHandler :
AuthorizationHandler<AllowedInLoungeRequirement> ❶
{
protected override Task HandleRequirementAsync( ❷
AuthorizationHandlerContext context, ❷
AllowedInLoungeRequirement requirement) ❷
{
if(context.User.HasClaim(c => c.Type == "EmployeeNumber")) ❸
{
context.Succeed(requirement); ❹
}
return Task.CompletedTask; ❺
}
}

❶ The handler implements AuthorizationHandler<T>.
处理程序实现 AuthorizationHandler<T>

❷ You must override the abstract HandleRequirementAsync method.
您必须覆盖抽象的 HandleRequirementAsync 方法。

❸ Checks whether the user has the EmployeeNumber claim
检查用户是否具有 EmployeeNumber 声明

❹ If the user has the necessary claim, marks the requirement as satisfied by calling Succeed
如果用户具有必要的声明,则通过调用 Succeed 将需求标记为满足

❺ If the requirement wasn’t satisfied, does nothing
如果不满足要求,则不执行任何作

I’ve left the implementation of MinimumAgeHandler for MinimumAgeRequirement as an exercise for the reader, as it’s similar to the handlers you have already seen. You can find an example implementation in the code samples for the chapter.
我将 MinimumAgeRequirement 的 MinimumAgeHandler 的实现留给读者作为练习,因为它类似于您已经看到的处理程序。您可以在本章的代码示例中找到示例实现。

TIP It’s possible to write generic handlers that can be used with multiple requirements, but I suggest sticking to handling a single requirement. If you need to extract some common functionality, move it to an external service, and call that from both handlers.
提示:可以编写可用于多个需求的通用处理程序,但我建议坚持处理单个需求。如果需要提取一些常用功能,请将其移动到外部服务,然后从两个处理程序中调用它。

This pattern of authorization handler is common, but in some cases, instead of checking for a success condition, you might want to check for a failure condition. In the airport example, you don’t want to authorize someone who was previously banned from the lounge, even if they would otherwise be allowed to enter.
这种授权处理程序模式很常见,但在某些情况下,您可能希望检查失败条件,而不是检查成功条件。在 airport 示例中,您不希望授权之前被禁止进入贵宾室的人,即使他们本来可以进入。

You can handle this scenario by using the context.Fail() method exposed on the context, as shown in the following listing. Calling Fail() in a handler always causes the requirement, and hence the whole policy, to fail. You should use it only when you want to guarantee failure, even if other handlers indicate success.
您可以使用上下文处理此方案。Fail() 方法,如下面的清单所示。在处理程序中调用 Fail() 总是会导致需求失败,从而导致整个策略失败。仅当您希望保证失败时,才应使用它,即使其他处理程序指示成功。

Listing 24.10 Calling context.Fail() in a handler to fail the requirement
清单 24.10 调用context.Fail()使需求失败

public class BannedFromLoungeHandler :
AuthorizationHandler<AllowedInLoungeRequirement> ❶
{
protected override Task HandleRequirementAsync( ❷
AuthorizationHandlerContext context, ❷
AllowedInLoungeRequirement requirement) ❷
{
if(context.User.HasClaim(c => c.Type == "IsBannedFromLounge")) ❸
{
context.Fail(); ❹
}
return Task.CompletedTask; ❺
}
}

❶ The handler implements AuthorizationHandler<T>.
处理程序实现 AuthorizationHandler<T>

❷ You must override the abstract HandleRequirementAsync method.
您必须重写抽象的 HandleRequirementAsync 方法。

❸ Checks whether the user has the IsBannedFromLounge claim
检查用户是否具有 IsBannedFromLounge 声明

❹ If the user has the claim, fails the requirement by calling Fail. The whole policy fails.
如果用户具有声明,则通过调用 Fail 来满足要求。整个策略失败了。

❺ If the claim wasn’t found, does nothing
如果未找到索赔,则不执行任何作

In most cases, your handlers will either call Succeed() or will do nothing, but the Fail() method is useful when you need a kill switch to guarantee that a requirement won’t be satisfied.
在大多数情况下,处理程序将调用 Succeed() 或不执行任何作,但当你需要一个终止开关来保证不会满足要求时,Fail() 方法非常有用。

NOTE Whether a handler calls Succeed(), Fail(), or neither, the authorization system always executes all the handlers for a requirement and all the requirements for a policy, so you can be sure your handlers will always be called.
注意:无论处理程序是调用 Succeed()、Fail(),还是两者都不调用,授权系统始终执行要求的所有处理程序和策略的所有要求,因此您可以确保始终调用处理程序。

The final step to complete your authorization implementation for the app is to register the authorization handlers with the DI container, as shown in the following listing.
完成应用程序的授权实现的最后一步是向 DI 容器注册授权处理程序,如下面的清单所示。

Listing 24.11 Registering the authorization handlers with the DI container
Listing 24.11 向 DI 容器注册授权处理程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(options =>
options.AddPolicy(
"CanEnterSecurity",
policyBuilder => policyBuilder
.RequireClaim(Claims.BoardingPassNumber));
options.AddPolicy(
"CanAccessLounge",
policyBuilder => policyBuilder.AddRequirements(
new MinimumAgeRequirement(18),
new AllowedInLoungeRequirement()
));
});
services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
services.AddSingleton<IAuthorizationHandler, FrequentFlyerHandler>();
services.AddSingleton<IAuthorizationHandler, BannedFromLoungeHandler>();
services.AddSingleton<IAuthorizationHandler, IsAirlineEmployeeHandler>();
// Additional configuration

For this app, the handlers don’t have any constructor dependencies, so I’ve registered them as singletons with the container. If your handlers have scoped or transient dependencies (the EF Core DbContext, for example), you might want to register them as scoped instead, as appropriate.
对于此应用程序,处理程序没有任何构造函数依赖项,因此我已将它们注册为容器中的单例。如果处理程序具有范围或暂时性依赖项(例如 EF Core DbContext),则可能需要根据需要将它们注册为范围依赖项。

NOTE Services are registered with a lifetime of transient, scoped, or singleton, as discussed in chapter 9.
注意:服务注册的生命周期为 transient、scoped 或 singleton,如第 9 章所述。

You can combine the concepts of policies, requirements, and handlers in many ways to achieve your goals for authorization in your application. The example in this section, although contrived, demonstrates the components you need to apply authorization declaratively at the action method or Razor Page level by creating policies and applying the [Authorize] attribute as appropriate.
您可以通过多种方式组合策略、要求和处理程序的概念,以实现应用程序中的授权目标。本部分中的示例虽然是人为的,但演示了通过创建策略并根据需要应用 [Authorize] 属性,在作方法或 Razor 页面级别以声明方式应用授权所需的组件。

As well as applying the [Authorize] attribute explicitly to actions and Razor Pages, you can configure it globally, so that a policy is applied to every endpoint in your application. Additionally, for Razor Pages you can apply different authorization policies to different folders. You can read more about applying authorization policies using conventions in Microsoft’s “Razor Pages authorization conventions in ASP.NET Core” documentation: http://mng.bz/nMm2.
除了将 [Authorize] 属性显式应用于作和 Razor Pages 之外,还可以全局配置它,以便将策略应用于应用程序中的每个终结点。此外,对于 Razor Pages,可以将不同的授权策略应用于不同的文件夹。您可以在 Microsoft 的“ASP.NET Core 中的 Razor Pages 授权约定”文档中阅读有关使用约定应用授权策略的更多信息:http://mng.bz/nMm2

There’s one area, however, where the [Authorize] attribute falls short: resource-based authorization. The [Authorize] attribute attaches metadata to an endpoint, so the authorization middleware can authorize the user before an endpoint is executed. But what if you need to authorize the action from within the endpoint?
但是,[Authorize] 属性在一个方面存在不足:基于资源的授权。[Authorize] 属性将元数据附加到终结点,以便授权中间件可以在执行终结点之前对用户进行授权。但是,如果您需要从终端节点内部授权作,该怎么办?

This is common when you’re applying authorization at the document or resource level. If users are allowed to edit only documents they created, you need to load the document before you can tell whether they’re allowed to edit it! This isn’t easy with the declarative [Authorize] attribute approach, so you must often use an alternative, imperative approach. In the next section you’ll see how to apply this resource-based authorization in a Razor Page handler.‌
当您在文档或资源级别应用授权时,这种情况很常见。如果仅允许用户编辑他们创建的文档,则需要先加载文档,然后才能判断是否允许他们编辑该文档!使用声明性 [Authorize] 属性方法,这并不容易,因此您必须经常使用替代的命令式方法。在下一部分中,你将了解如何在 Razor Page 处理程序中应用此基于资源的授权。

24.5 Controlling access with resource-based authorization‌

24.5 使用基于资源的授权控制访问

In this section you’ll learn about resource-based authorization. This is used when you need to know details about the resource being protected to determine whether a user is authorized. You’ll learn how to apply authorization policies manually using the IAuthorizationService and how to create resource-based AuthorizationHandlers.
在本节中,您将了解基于资源的授权。当您需要了解有关受保护资源的详细信息以确定用户是否获得授权时,可以使用此方法。您将了解如何使用 IAuthorizationService 手动应用授权策略,以及如何创建基于资源的 AuthorizationHandlers。

Resource-based authorization is a common problem for applications, especially when you have users who can create or edit some sort of document. Consider the recipe application you worked on in chapter 23. This app lets users create, view, and edit recipes.
基于资源的授权是应用程序的常见问题,尤其是当您拥有可以创建或编辑某种文档的用户时。考虑您在第 23 章中处理的配方应用程序。此应用程序允许用户创建、查看和编辑配方。

Up to this point, everyone can create new recipes, and anyone can edit any recipe, even if they haven’t logged in. Now you want to add some additional behavior:
到目前为止,每个人都可以创建新配方,任何人都可以编辑任何配方,即使他们尚未登录。现在,您需要添加一些其他行为:

• Only authenticated users should be able to create new recipes.
只有经过身份验证的用户才能创建新配方。

• You can edit only the recipes you created.
您只能编辑您创建的配方。

You’ve already seen how to achieve the first of these requirements: decorate the Create .cshtml Razor Page with an [Authorize] attribute and don’t specify a policy, as shown in the following listing. This will force the user to authenticate before they can create a new recipe.
你已了解如何实现这些要求中的第一个:使用 [Authorize] 属性装饰 Create .cshtml Razor 页面,并且不指定策略,如下面的列表所示。这将强制用户在创建新配方之前进行身份验证。

Listing 24.12 Adding AuthorizeAttribute to the Create.cshtml Razor Page
列表 24.12 将 AuthorizeAttribute 添加到 Create.cshtml Razor 页面

[Authorize] ❶
public class CreateModel : PageModel
{[BindProperty]
public CreateRecipeCommand Input { get; set; }
public void OnGet() ❷
{ ❷
Input = new CreateRecipeCommand(); ❷
} ❷
public async Task<IActionResult> OnPost() ❷
{ ❷
// Method body not shown for brevity ❷
} ❷
}

❶ Users must be authenticated to execute the Create.cshtml Razor Page.
用户必须经过身份验证才能执行 Create.cshtml Razor 页面。
❷ All page handlers are protected. You can apply [Authorize] only to the PageModel, not handlers.
所有页面处理程序都受到保护。只能将 [Authorize] 应用于 PageModel,而不能应用于处理程序。

TIP As with all filters, you can apply the [Authorize] attribute only to the Razor Page, not to individual page handlers. The attribute applies to all page handlers in the Razor Page.
提示:与所有筛选器一样,只能将 [Authorize] 属性应用于 Razor 页面,而不能应用于单个页面处理程序。该属性适用于 Razor 页面中的所有页面处理程序。

Adding the [Authorize] attribute fulfills your first requirement, but unfortunately, with the techniques you’ve seen so far, you have no way to fulfill the second. You could apply a policy that either permits or denies a user the ability to edit all recipes, but there’s currently no easy way to restrict this so that a user can only edit their own recipes.
添加 [Authorize] 属性可以满足第一个要求,但遗憾的是,使用你目前看到的技术,无法满足第二个要求。您可以应用一个策略来允许或拒绝用户编辑所有配方,但目前没有简单的方法来限制这一点,以便用户只能编辑自己的配方。

To find out who created the Recipe, you must first load it from the database. Only then can you attempt to authorize the user, taking the specific recipe (resource) into account. The following listing shows a partially implemented page handler for how this might look, where authorization occurs partway through the method, after the Recipe object has been loaded.
要找出 Recipe 的创建者,您必须先从数据库中加载它。只有这样,您才能尝试授权用户,同时考虑特定的配方(资源)。下面的清单显示了一个部分实现的页面处理程序,其中授权发生在方法的中途,在 Recipe 对象加载之后。

Listing 24.13 The Edit.cshtml page must load the Recipe
清单 24.13 Edit.cshtml 页面必须加载 Recipe

public IActionResult OnGet(int id) ❶
{
var recipe = _service.GetRecipe(id); ❷
var createdById = recipe.CreatedById; ❷
// Authorize user based on createdById ❸
if(isAuthorized) ❹
{ ❹
return View(recipe); ❹
} ❹
}

❶ The id of the recipe to edit is provided by model binding.
要编辑的配方的 id 由模型绑定提供。
❷ You must load the Recipe from the database before you know who created it.
您必须先从数据库中加载 Recipe,然后才能知道谁创建了它。
❸ You must authorize the current user to verify that they’re allowed to edit this specific Recipe.
您必须授权当前用户验证是否允许他们编辑此特定配方。
❹ The action method can continue only if the user was authorized.
只有在用户获得授权的情况下,作方法才能继续。

You need access to the resource (in this case, the Recipe entity) to perform the authorization, so the declarative [Authorize] attribute can’t help you. In section 24.5.1 you’ll see the approach you need to take to handle these situations and to apply authorization inside your endpoints.
您需要访问资源(在本例中为 Recipe 实体)才能执行授权,因此声明性 [Authorize] 属性无法为您提供帮助。在 Section 24.5.1 中,您将看到处理这些情况并在 endpoints 内应用授权所需采用的方法。

WARNING Be careful when exposing the integer ID of your entities in the URL, as in listing 24.13. Users will be able to edit every entity by modifying the ID in the URL to access a different entity. Be sure to apply authorization checks, or you could expose a security vulnerability called insecure direct object reference (IDOR). You can read more about IDOR at http://mng.bz/QPnG.
警告:在 URL 中公开实体的整数 ID 时要小心,如清单 24.13 所示。用户将能够通过修改 URL 中的 ID 来编辑每个实体,以访问不同的实体。请务必应用授权检查,否则可能会暴露称为不安全直接对象引用 (IDOR) 的安全漏洞。您可以在 http://mng.bz/QPnG 上阅读有关 IDOR 的更多信息。

24.5.1 Manually authorizing requests with IAuthorizationService‌

24.5.1 使用 IAuthorizationService 手动授权请求

All of the approaches to authorization so far have been declarative. You apply the [Authorize] attribute, with or without a policy name, and you let the framework take care of performing the authorization itself.
到目前为止,所有授权方法都是声明性的。您可以应用 [Authorize] 属性(无论是否具有策略名称),并让框架自行执行授权。

For this recipe-editing example, you need to use imperative authorization, so you can authorize the user after you’ve loaded the Recipe from the database. Instead of applying a marker saying “Authorize this method,” you need to write some of the authorization code yourself.
对于此配方编辑示例,您需要使用命令式授权,以便您可以在从数据库加载配方后授权用户。您需要自己编写一些授权代码,而不是应用“Authorize this method”标记。

DEFINITION Declarative and imperative are two different styles of programming. Declarative programming describes what you’re trying to achieve and lets the framework figure out how to achieve it. Imperative programming describes how to achieve something by providing each of the steps needed.‌
定义:声明式和命令式是两种不同的编程风格。声明式编程描述了您要实现的目标,并让框架弄清楚如何实现它。命令式编程描述了如何通过提供所需的每个步骤来实现某些目标。

ASP.NET Core exposes IAuthorizationService, which you can inject into any of your services or endpoints for imperative authorization. The following listing shows how you could update the Edit.cshtml Razor Page (shown partially in listing 24.13) to use the IAuthorizationService to verify whether the action is allowed to continue execution.
ASP.NET Core 公开了 IAuthorizationService,您可以将其注入到任何服务或终端节点中,以实现命令式授权。以下列表显示了如何更新 Edit.cshtml Razor 页面(部分显示在列表 24.13 中),以使用 IAuthorizationService 来验证是否允许作继续执行。

Listing 24.14 Using IAuthorizationService for resource-based authorization
清单 24.14 使用 IAuthorizationService 进行基于资源的授权

[Authorize] ❶
public class EditModel : PageModel
{
[BindProperty]
public Recipe Recipe { get; set; }
private readonly RecipeService _service;
private readonly IAuthorizationService _authService; ❷
public EditModel(
RecipeService service,
IAuthorizationService authService) ❷
{
_service = service;
_authService = authService; ❷
}
public async Task<IActionResult> OnGet(int id)
{
Recipe = _service.GetRecipe(id); ❸
AuthorizationResult authResult = await _authService ❹
.AuthorizeAsync(User, Recipe, "CanManageRecipe"); ❹
if (!authResult.Succeeded) ❺
{ ❺
return new ForbidResult(); ❺
} ❺
return Page(); ❻
}
}

❶ Only authenticated users should be allowed to edit recipes.
只允许经过身份验证的用户编辑配方。
❷ IAuthorizationService is injected into the class constructor using DI.
使用 DI 将 IAuthorizationService 注入到类构造函数中。
❸ Loads the Recipe from the database
从数据库加载配方
❹ Calls IAuthorizationService, providing ClaimsPrinicipal, resource, and the policy name
调用 IAuthorizationService,提供 ClaimsPrinicipal、资源和策略名称
❺ If authorization failed, returns a Forbidden result
如果授权失败,则返回 Forbidden 结果
❻ If authorization was successful, continues displaying the Razor Page
如果授权成功,则继续显示 Razor 页面

IAuthorizationService exposes an AuthorizeAsync method, which requires three things to authorize the request:
IAuthorizationService 公开了一个 AuthorizeAsync 方法,该方法需要三项内容来授权请求:

• The ClaimsPrincipal user object, exposed on the PageModel as User
ClaimsPrincipal 用户对象,在 PageModel 上作为 User 公开

• The resource being authorized: Recipe
正在授权的资源:Recipe

• The policy to evaluate: "CanManageRecipe"
要评估的策略:“CanManageRecipe”

The authorization attempt returns an AuthorizationResult object, which indicates whether the attempt was successful via the Succeeded property. If the attempt wasn’t successful, you should return a new ForbidResult, which is converted to an HTTP 403 Forbidden response or redirects the user to the “access denied” page, depending on whether you’re building a traditional web app or an API app.‌‌‌
授权尝试返回一个 AuthorizationResult 对象,该对象通过 Succeeded 属性指示尝试是否成功。如果尝试不成功,您应该返回一个新的 ForbidResult,该结果将转换为 HTTP 403 Forbidden 响应或将用户重定向到“access denied”页面,具体取决于您是构建传统的 Web 应用程序还是 API 应用程序。

NOTE As mentioned in section 24.2.2, which type of response is generated depends on which authentication services are configured. The default Identity configuration, used by Razor Pages, generates redirects. API apps typically generate HTTP 401 and 403 responses instead.
注意:如第 24.2.2 节所述,生成的响应类型取决于配置的身份验证服务。Razor Pages 使用的默认标识配置会生成重定向。API 应用程序通常会生成 HTTP 401 和 403 响应。

You’ve configured the imperative authorization in the Edit.cshtml Razor Page itself, but you still need to define the "CanManageRecipe" policy that you use to authorize the user. This is the same process as for declarative authorization, so you have to do the following:
你已在 Edit.cshtml Razor 页面本身中配置了命令性授权,但仍需要定义用于授权用户的“CanManageRecipe”策略。此过程与声明式授权的过程相同,因此您必须执行以下作:

• Create a policy in Program.cs by calling AddAuthorization().
通过调用 AddAuthorization() 在 Program.cs 中创建策略。
• Define one or more requirements for the policy.
定义策略的一个或多个要求。
• Define one or more handlers for each requirement.
为每个要求定义一个或多个处理程序。
• Register the handlers in the DI container.
在 DI 容器中注册处理程序。

With the exception of the handler, these steps are identical to the declarative authorization approach with the [Authorize] attribute, so I run through them only briefly here.
除了处理程序之外,这些步骤与具有 [Authorize] 属性的声明性授权方法相同,因此我在这里只简要介绍一下它们。

First, you can create a simple IAuthorizationRequirement. As with many requirements, this contains no data and simply implements the marker interface:
首先,您可以创建一个简单的 IAuthorizationRequirement。与许多要求一样,这不包含任何数据,只实现 marker 接口:

public class IsRecipeOwnerRequirement : IAuthorizationRequirement { }

Defining the policy in Program.cs is similarly simple, as you have only a single requirement. Note that there’s nothing resource-specific in any of this code so far:
在 Program.cs 中定义策略同样简单,因为您只有一个需求。请注意,到目前为止,此代码中没有任何特定于资源的内容:

builder.Services.AddAuthorization(options => { options.AddPolicy("CanManageRecipe", policyBuilder =>
policyBuilder.AddRequirements(new IsRecipeOwnerRequirement()));
});

You’re halfway there. All you need to do now is create an authorization handler for IsRecipeOwnerRequirement and register it with the DI container.
你已经成功了一半。现在,您需要做的就是为 IsRecipeOwnerRequirement 创建一个授权处理程序,并将其注册到 DI 容器中。

24.5.2 Creating a resource-based AuthorizationHandler‌

24.5.2 创建基于资源的 AuthorizationHandler

Resource-based authorization handlers are essentially the same as the authorization handler implementations you saw in section 24.4.2. The only difference is that the handler also has access to the resource being authorized.
基于资源的授权处理程序本质上与您在 Section 24.4.2 中看到的授权处理程序实现相同。唯一的区别是处理程序还可以访问被授权的资源。

To create a resource-based handler, you should derive from the AuthorizationHandler<TRequirement, TResource> base class, where TRequirement is the type of requirement to handle and TResource is the type of resource that you provide when calling IAuthorizationService. Compare this with the AuthorizationHandler<T> class you implemented previously, where you specified only the requirement.
若要创建基于资源的处理程序,应从AuthorizationHandler<TRequirement, TResource> 基类派生,其中 TRequirement 是要处理的要求类型,TResource 是调用 IAuthorizationService 时提供的资源类型。将此类与您之前实现的类进行比较,在AuthorizationHandler<T> 该类中,您只指定了要求。

The next listing shows the handler implementation for your recipe application. You can see that you’ve specified the requirement as IsRecipeOwnerRequirement and the resource as Recipe, and you have implemented the HandleRequirementAsync method.
下一个清单显示了配方应用程序的处理程序实现。您可以看到,您已将要求指定为 IsRecipeOwnerRequirement,将资源指定为 Recipe,并且已实现 HandleRequirementAsync 方法。

Listing 24.15 IsRecipeOwnerHandler for resource-based authorization
列表 24.15 IsRecipeOwnerHandler 用于基于资源的授权

public class IsRecipeOwnerHandler :
AuthorizationHandler<IsRecipeOwnerRequirement, Recipe> ❶
{
private readonly UserManager<ApplicationUser> _userManager; ❷
public IsRecipeOwnerHandler( ❷
UserManager<ApplicationUser> userManager) ❷
{ ❷
_userManager = userManager; ❷
} ❷
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
IsRecipeOwnerRequirement requirement,
Recipe resource) ❸
{
var appUser = await _userManager.GetUserAsync(context.User);
if(appUser == null) ❹
{
return;
}
if(resource.CreatedById == appUser.Id) ❺
{
context.Succeed(requirement); ❻
}
}
}

❶ Implements the necessary base class, specifying the requirement and resource type
实现必要的基类,指定需求和资源类型
❷ Injects an instance of the UserManager<T> class using DI
使用 DI 注入 UserManager<T> 类的实例
❸ As well as the context and requirement, you’re provided the resource instance.
除了上下文和需求外,还为您提供资源实例。
❹ If you aren’t authenticated, appUser will be null.
如果您未通过身份验证,appUser 将为 null。
❺ Checks whether the current user created the Recipe by checking the CreatedById property
通过检查CreatedById 属性来检查当前用户是否创建了配方
❻ If the user created the document, Succeeds the requirement; otherwise, does nothing
如果用户创建了文档,则 Succeeds the requirement;否则,不执行任何作

This handler is slightly more complicated than the examples you’ve seen previously, primarily because you’re using an additional service, UserManager<>, to load the ApplicationUser entity based on ClaimsPrincipal from the request.
此处理程序比您之前看到的示例稍微复杂一些,主要是因为您正在使用附加服务UserManager<> 来根据请求中的 ClaimsPrincipal 加载 ApplicationUser 实体。

NOTE In practice, the ClaimsPrincipal will likely already have the Id added as a claim, making the extra step unnecessary in this case. This example shows the general pattern if you need to use dependency-injected services.
注意:实际上,ClaimsPrincipal 可能已将 Id 添加为声明,因此在这种情况下不需要额外的步骤。此示例显示了需要使用 dependency-injected 服务时的一般模式。

The other significant difference is that the HandleRequirementAsync method has provided the Recipe resource as a method argument. This is the same object you provided when calling AuthorizeAsync on IAuthorizationService. You can use this resource to verify whether the current user created it. If so, you Succeed() the requirement; otherwise, you do nothing.
另一个显著区别是 HandleRequirementAsync 方法已将 Recipe 资源作为方法参数提供。这与您在 IAuthorizationService 上调用 AuthorizeAsync 时提供的对象相同。您可以使用此资源来验证它是否为当前用户创建。如果是这样,则 Succeed() 要求;否则,您什么都不做。

The final task is adding IsRecipeOwnerHandler to the DI container. Your handler uses an additional dependency, UserManager<>, that uses EF Core, so you should register the handler as a scoped service:
最后一项任务是将 IsRecipeOwnerHandler 添加到 DI 容器中。处理程序使用使用 EF Core 的附加依赖项 UserManager<>,因此应将处理程序注册为范围服务:

services.AddScoped<IAuthorizationHandler, IsRecipeOwnerHandler>();

TIP If you’re wondering how to know whether you register a handler as scoped or a singleton, think back to chapter 9.
提示:如果您想知道如何知道将处理程序注册为 scoped 还是 singleton,请回想第 9 章。

Essentially, if you have scoped dependencies, you must register the handler as scoped; otherwise, singleton is fine.
本质上,如果你有 scoped 依赖项,则必须将处理程序注册为 scoped;否则,Singleton 就可以了。

With everything hooked up, you can take the application for a spin. If you try to edit a recipe you didn’t create by clicking the Edit button on the recipe, you’ll either be redirected to the login page (if you hadn’t yet authenticated) or see an “access denied” page, as shown in figure 24.7.
连接好所有内容后,您可以试用该应用程序。如果您尝试通过单击配方上的 Edit 按钮来编辑不是创建的配方,您将被重定向到登录页面(如果您尚未进行身份验证)或看到 “access denied” 页面,如图 24.7 所示。

alt text

Figure 24.7 If you’re logged in but not authorized to edit a recipe, you’ll be redirected to an “Access Denied” page. If you’re not logged in, you’ll be redirected to the Login page.
图 24.7 如果您已登录但无权编辑配方,您将被重定向到“Access Denied”页面。如果您尚未登录,您将被重定向到 Login (登录页面)。

By using resource-based authorization, you’re able to enact more fine-grained authorization requirements that you can apply at the level of an individual document or resource.
通过使用基于资源的授权,您可以制定更精细的授权要求,这些要求可以应用于单个文档或资源级别。

Instead of being able to authorize only that a user can edit any recipe, you can authorize whether a user can edit this recipe.
您可以授权用户是否可以编辑此配方,而不是仅授权用户可以编辑任何配方。

All the authorization techniques you’ve seen so far have focused on server-side checks. Both the [Authorize] attribute and resource-based authorization approaches focus on stopping users from executing a protected endpoint on the server. This is important from a security point of view, but there’s another aspect you should consider: the user experience when they don’t have permission.
到目前为止,您看到的所有授权技术都集中在服务器端检查上。[Authorize] 属性和基于资源的授权方法都侧重于阻止用户在服务器上执行受保护的终结点。从安全角度来看,这很重要,但您应该考虑另一个方面:用户没有权限时的体验。

You’ve protected the code executing on the server, but arguably the Edit button should never have been visible to the user if they weren’t going to be allowed to edit the recipe! In the next section we’ll look at how you can conditionally hide the Edit button by using resource-based authorization in your view models.‌
您已经保护了在服务器上执行的代码,但可以说,如果不允许用户编辑配方,则 Edit (编辑) 按钮永远不应该对用户可见!在下一节中,我们将了解如何在视图模型中使用基于资源的授权来有条件地隐藏 Edit 按钮。

Resource-based authorization versus business-logic checks
基于资源的授权与业务逻辑检查

The value proposition of using the ASP.NET Core framework’s resource- based authorization approach isn’t always clear compared with using simple, manual, business-logic based checks (as in listing 24.13). Using IAuthorizationService and the authorization infrastructure adds an explicit dependency on the ASP.NET Core framework that you may not want to use if you’re performing authorization checks in your domain model services.
与使用简单的、手动的、基于业务逻辑的检查(如清单 24.13 所示)相比,使用 ASP.NET Core 框架基于资源的授权方法的价值主张并不总是很清楚。使用 IAuthorizationService 和授权基础结构会添加对 ASP.NET Core 框架的显式依赖项,如果您在域模型服务中执行授权检查,则可能不想使用该框架。

This is a valid concern without an easy answer. I tend to favor simple business-logic checks inside the domain, without relying on the framework’s authorization infrastructure, to make my domain easier to test and framework-independent. But doing so loses some of the benefits of such a framework:
这是一个合理的担忧,没有一个简单的答案。我倾向于在域内进行简单的业务逻辑检查,而不依赖框架的授权基础设施,以使我的域更易于测试且独立于框架。但这样做会失去这种框架的一些好处:

• The IAuthorizationService uses declarative policies, even though you are calling the authorization framework imperatively.
IAuthorizationService 使用声明性策略,即使您以命令方式调用授权框架也是如此。

• You can decouple the need to authorize an action from the actual requirements.
您可以将授权作的需要与实际要求分离。

• You can easily rely on peripheral services and properties of the request, which may be harder (or undesirable) with business logic checks.
您可以轻松依赖请求的外围服务和属性,这对于业务逻辑检查可能更难(或不可取)。

You can achieve these benefits in business-logic checks, but that typically requires creating a lot of infrastructure too, so you lose a lot of the benefits of keeping things simple. Which approach is best will depend on the specifics of your application design, and there may well be cases for using both.
你可以在业务逻辑检查中实现这些好处,但这通常需要创建大量的基础设施,所以你会失去很多保持简单的好处。哪种方法最好,将取决于应用程序设计的具体情况,并且很可能会出现同时使用这两种方法的情况。

For example, one possible approach is to use the basic [Authorize] attribute as described in section 24.2.1 to prevent anonymous access to your APIs, potentially with simple, coarse policies applied to your APIs. You would then rely on “manual” business-logic checks against the ClaimsPrincipal in your domain as required. This may reduce a lot of the complexity and indirection associated with the ASP.NET Core authorization system.
例如,一种可能的方法是使用第 24.2.1 节中所述的基本 [Authorize] 属性来防止匿名访问您的 API,可能会使用简单、粗略的策略应用于您的 API。然后,您可以根据需要对域中的 ClaimsPrincipal 进行“手动”业务逻辑检查。这可能会降低与 ASP.NET Core 授权系统相关的许多复杂性和间接性。

24.6 Hiding HTML elements from unauthorized users‌

24.6 对未经授权的用户隐藏 HTML 元素

All the authorization code you’ve seen so far has revolved around protecting endpoints on the server side, rather than modifying the UI for users. This is important and should be the starting point whenever you add authorization to an app.
到目前为止,您看到的所有授权代码都围绕着保护服务器端的端点,而不是为用户修改 UI。这很重要,每当您向应用程序添加授权时,都应该从此作为起点。

WARNING Malicious users can easily circumvent your UI, so it’s important to always authorize your endpoints on the server, never on the client alone.
警告:恶意用户可以轻松绕过您的 UI,因此请务必始终在服务器上授权您的终端节点,而不是仅在客户端上授权。

From a user-experience point of view, however, it’s not friendly to have buttons or links that look like they’re available but present an “access denied” page when they’re clicked. A better experience would be for the links to be disabled or not visible at all.
然而,从用户体验的角度来看,让按钮或链接看起来可用但在点击时显示 “拒绝访问” 页面并不友好。更好的体验是禁用链接或根本不显示链接。

You can achieve this in several ways in your own Razor templates. In this section I’m going to show you how to add an additional property to the PageModel, called CanEditRecipe, which the Razor view template will use to change the rendered HTML.
您可以在自己的 Razor 模板中通过多种方式实现此目的。在本节中,我将向您展示如何向 PageModel 添加一个名为 CanEditRecipe 的附加属性,Razor 视图模板将使用该属性来更改呈现的 HTML。

TIP An alternative approach would be to inject IAuthorizationService directly into the view template using the @inject directive, as you saw in chapter 9, but you should generally prefer to keep logic like this in the page handler.
提示:另一种方法是使用 @inject 指令将 IAuthorizationService 直接注入视图模板中,如第 9 章所示,但您通常更愿意在页面处理程序中保留这样的逻辑。

When you’re finished, the rendered HTML looks unchanged for recipes you created, but the Edit button will be hidden when viewing a recipe someone else created, as shown in figure 24.8.
完成后,您创建的配方的渲染 HTML 看起来没有变化,但是在查看其他人创建的配方时,Edit (编辑) 按钮将被隐藏,如图 24.8 所示。

alt text

Figure 24.8 Although the HTML will appear unchanged for recipes you created, the Edit button is hidden when you view recipes created by a different user.
图 24.8 虽然您创建的配方的 HTML 看起来不变,但当您查看其他用户创建的配方时,Edit(编辑)按钮会隐藏。

Listing 24.16 shows the PageModel for the View.cshtml Razor Page, which is used to render the recipe page shown in figure 24.8. As you’ve already seen for resource-based authorization, you can use the IAuthorizationService to determine whether the current user has permission to edit the Recipe by calling AuthorizeAsync.
列表 24.16 显示了 View.cshtml Razor 页面的 PageModel,它用于呈现图 24.8 中所示的配方页面。正如您已经看到的基于资源的授权,您可以使用 IAuthorizationService 通过调用 AuthorizeAsync 来确定当前用户是否有权编辑配方。

You can then set this value as an additional property on the PageModel, called CanEditRecipe.
然后,您可以将此值设置为 PageModel 上的附加属性,称为 CanEditRecipe。

Listing 24.16 Setting the CanEditRecipe property in the View.cshtml Razor Page
列表 24.16 在 View.cshtml Razor 页面中设置 CanEditRecipe 属性

public class ViewModel : PageModel
{
public Recipe Recipe { get; set; }
public bool CanEditRecipe { get; set; } ❶
private readonly RecipeService _service;
private readonly IAuthorizationService _authService;
public ViewModel(
RecipeService service,
IAuthorizationService authService)
{
_service = service;
_authService = authService;
}
public async Task<IActionResult> OnGetAsync(int id)
{
Recipe = _service.GetRecipe(id); ❷
AuthorizationResult isAuthorised = await _authService ❸
.AuthorizeAsync(User, recipe, "CanManageRecipe"); ❸
CanEditRecipe = isAuthorised.Succeeded; ❹
return Page();
}
}

❶ The CanEditRecipe property will be used to control whether the Edit button is rendered.
CanEditRecipe 属性将用于控制是否呈现 Edit 按钮。

❷ Loads the Recipe resource for use with IAuthorizationService
加载 Recipe 资源以用于 IAuthorizationService

❸ Verifies whether the user is authorized to edit the Recipe
验证用户是否有权编辑Recipe

❹ Sets the CanEditRecipe property on the PageModel as appropriate
根据需要在 PageModel 上设置 CanEditRecipe 属性

Instead of blocking execution of the Razor Page (as you did previously in the Edit.cshtml page handler), use the result of the call to AuthorizeAsync to set the CanEditRecipe value on the PageModel. You can then make a simple change to the View.chstml Razor template, adding an if clause around the rendering of the Edit link:
不要阻止 Razor 页面的执行(就像之前在 Edit.cshtml 页面处理程序中所做的那样),而是使用对 AuthorizeAsync 的调用结果在 PageModel 上设置 CanEditRecipe 值。然后,您可以对 View.chstml Razor 模板进行简单的更改,在 Edit 链接的呈现周围添加 if 子句:

@if(Model.CanEditRecipe)
{
<a asp-page="Edit" asp-route-id="@Model.Id" class="btn btn-primary">Edit</a>
}

This ensures that only users who will be able to execute the Edit.cshtml Razor Page can see the link to that page.
这可确保只有能够执行 Edit.cshtml Razor 页面的用户才能看到指向该页面的链接。

WARNING The if clause means that the Edit link will not be displayed unless the current user created the recipe, but you should never rely on client-side security alone. It’s important to keep the server-side authorization check in your Edit.cshtml page handler to protect against any direct access attempts. Even if a malicious user circumvents your UI, the server-side authorization ensures that your application is secure.
警告:if 子句表示除非当前用户创建了配方,否则不会显示 Edit 链接,但您绝不应仅依赖客户端安全性。请务必在 Edit.cshtml 页面处理程序中保留服务器端授权检查,以防止任何直接访问尝试。即使恶意用户绕过了您的 UI,服务器端授权也可以确保您的应用程序是安全的。

With that final change, you’ve finished adding authorization to the recipe application. Anonymous users can browse the recipes created by others, but they must log in to create new recipes. Additionally, authenticated users can edit only the recipes that they created, and they won’t see an Edit link for other people’s recipes.
完成最后的更改后,您已完成向配方应用程序添加授权。匿名用户可以浏览其他人创建的配方,但他们必须登录才能创建新配方。此外,经过身份验证的用户只能编辑他们创建的配方,并且不会看到其他人的配方的 Edit (编辑) 链接。

Authorization is a key aspect of most apps, so it’s important to bear it in mind from an early point. Although it’s possible to add authorization later, as you did with the recipe app, it’s normally preferable to consider authorization sooner rather than later in the app’s development.
授权是大多数应用程序的关键方面,因此尽早牢记这一点非常重要。尽管可以稍后添加授权,就像您对配方应用程序所做的那样,但通常最好在应用程序开发中尽早考虑授权。

In chapters 23 and 24 we focused on authentication and authorization for traditional web applications using Razor. In chapter 25 we’ll look at API applications, how authentication works with tokens, and how to add authorization policies to minimal APIs.
在第 23 章和第 24 章中,我们重点介绍了使用 Razor 对传统 Web 应用程序的身份验证和授权。在第 25 章中,我们将介绍 API 应用程序、身份验证如何与令牌配合使用,以及如何将授权策略添加到最小的 API。

27.7 Summary

27.7 总结

Authentication is the process of determining who a user is. It’s distinct from authorization, the process of determining what a user can do. Authentication typically occurs before authorization.
身份验证是确定用户身份的过程。它与 authorization 不同,授权是确定用户可以做什么的过程。身份验证通常在授权之前进行。

You can use the authorization services in any part of your application, but it’s typically applied using the AuthorizationMiddleware by calling UseAuthorization(). This should be placed after the calls to UseRouting() and UseAuthentication(), and before the call to UseEndpoints() for correct operation.
您可以在应用程序的任何部分使用授权服务,但通常通过调用 UseAuthorization() 使用 AuthorizationMiddleware 来应用授权服务。这应该放在调用 UseRouting() 和 UseAuthentication() 之后,以及调用 UseEndpoints() 之前,以便正确作。

You can protect Razor Pages and MVC actions by applying the [Authorize] attribute. The routing middleware records the presence of the attribute as metadata with the selected endpoint. The authorization middleware uses this metadata to determine how to authorize the request.
可以通过应用 [Authorize] 属性来保护 Razor Pages 和 MVC作。路由中间件将属性的存在记录为所选终端节点的元数据。授权中间件使用此元数据来确定如何授权请求。

The simplest form of authorization requires that a user be authenticated before executing an action. You can achieve this by applying the [Authorize] attribute to a Razor Page, action, controller, or globally. You can also apply attributes conventionally to a subset of Razor Pages.
最简单的授权形式要求在执行作之前对用户进行身份验证。可以通过将 [Authorize] 属性应用于 Razor 页面、作、控制器或全局来实现此目的。您还可以按惯例将属性应用于 Razor Pages 的子集。

Claims-based authorization uses the current user’s claims to determine whether they’re authorized to execute an action. You define the claims needed to execute an action in a policy.
基于声明的授权使用当前用户的声明来确定他们是否有权执行作。您可以定义在策略中执行作所需的声明。

Policies have a name and are configured in Program.cs as part of the call to AddAuthorization() in ConfigureServices. You define the policy using AddPolicy(), passing in a name and a lambda that defines the claims needed.
策略有一个名称,并在 Program.cs 中作为对 ConfigureServices 中 AddAuthorization() 的调用的一部分进行配置。您可以使用 AddPolicy() 定义策略,传入定义所需声明的名称和 lambda。

You can apply a policy to an action or Razor Page by specifying the policy in the authorize attribute; for example, [Authorize("CanAccessLounge")]. This policy will be used by the AuthorizationMiddleware to determine whether the user is allowed to execute the selected endpoint.
您可以通过在 authorize 属性中指定策略,将策略应用于作或 Razor 页面;例如,[Authorize(“CanAccessLounge”)]。AuthorizationMiddleware 将使用此策略来确定是否允许用户执行所选端点。

In a Razor Pages app, if an unauthenticated user attempts to execute a protected action, they’ll be redirected to the login page for your app. If they’re already authenticated but don’t have the required claims, they’ll be shown an “access denied” page instead.
在 Razor Pages 应用中,如果未经身份验证的用户尝试执行受保护的作,他们将被重定向到应用的登录页面。如果他们已经通过身份验证但没有所需的声明,则会显示“access denied”页面。

For complex authorization policies, you can build a custom policy. A custom policy consists of one or more requirements, and a requirement can have one or more handlers. You can combine requirements and handlers to create policies of arbitrary complexity.
对于复杂的授权策略,您可以构建自定义策略。自定义策略由一个或多个要求组成,一个要求可以有一个或多个处理程序。您可以组合需求和处理程序来创建任意复杂度的策略。

For a policy to be authorized, every requirement must be satisfied. For a requirement to be satisfied, one or more of the associated handlers must indicate success, and none must indicate explicit failure.
要授权策略,必须满足所有要求。要满足要求,一个或多个关联的处理程序必须指示成功,并且没有处理程序必须指示显式失败。

AuthorizationHandler<T> contains the logic that determines whether a requirement is satisfied. For example, if a requirement requires that users be over 18, the handler could look for a DateOfBirth claim and calculate the user’s age.
AuthorizationHandler<T> 包含确定是否满足要求的逻辑。例如,如果要求要求用户年满 18 岁,则处理程序可以查找 DateOfBirth 声明并计算用户的年龄。

Handlers can mark a requirement as satisfied by calling context.Succeed (requirement). If a handler can’t satisfy the requirement, it shouldn’t call anything on the context, as a different handler could call Succeed() and satisfy the requirement.
处理程序可以通过调用 context 将需求标记为满足。成功 (要求)。如果处理程序无法满足要求,则它不应在上下文中调用任何内容,因为其他处理程序可以调用 Succeed() 并满足要求。

If a handler calls context.Fail(), the requirement fails, even if a different handler marked it as a success using Succeed(). Use this method only if you want to override any calls to Succeed() from other handlers to ensure that the authorization policy will fail authorization.
如果处理程序调用 context.Fail(),则要求失败,即使其他处理程序使用 Succeed() 将其标记为成功也是如此。仅当您想要覆盖其他处理程序对 Succeed() 的任何调用以确保授权策略授权失败时,才使用此方法。

Resource-based authorization uses details of the resource being protected to determine whether the current user is authorized. For example, if a user is allowed to edit only their own documents, you need to know the author of the document before you can determine whether they’re authorized.
基于资源的授权使用受保护资源的详细信息来确定当前用户是否获得授权。例如,如果只允许用户编辑自己的文档,则需要先知道文档的作者,然后才能确定他们是否获得授权。

Resource-based authorization uses the same policy, requirements, and handler system as before. Instead of applying authorization with the [Authorize] attribute, you must manually call IAuthorizationService and provide the resource you’re protecting.
基于资源的授权使用与以前相同的策略、要求和处理程序系统。您必须手动调用 IAuthorizationService 并提供您正在保护的资源,而不是使用 [Authorize] 属性应用授权。

You can modify the user interface to account for user authorization by adding additional properties to your PageModel. If a user isn’t authorized to execute an action, you can remove or disable the link to that action method in the UI. You should always authorize on the server, even if you’ve removed links from the UI.
您可以通过向 PageModel 添加其他属性来修改用户界面以考虑用户授权。如果用户无权执行作,您可以在 UI 中删除或禁用指向该作方法的链接。您应该始终在服务器上授权,即使您已从 UI 中删除了链接。

ASP.NET Core in Action 23 Authentication: Adding users to your application with Identity

Part 4 Securing and deploying your applications
第 4 部分:保护和部署应用程序

So far in the book you’ve learned how to use minimal APIs,Razor Pages, and Model-View-Controller (MVC) controllers to build both server-rendered applications and APIs. You know how to dynamically generate JavaScript Object Notation (JSON) and HTML code based on incoming requests, and how to use configuration and dependency injection to customize your app’s behavior at runtime. In part 4 you’ll learn how to add users and profiles to your app and how to publish and secure your apps.
到目前为止,在本书中,您已经学习了如何使用最少的 API、Razor Pages 和模型-视图-控制器 (MVC) 控制器来构建服务器渲染的应用程序和 API。您知道如何根据传入请求动态生成 JavaScript 对象表示法 (JSON) 和 HTML 代码,以及如何使用配置和依赖项注入来自定义应用程序在运行时的行为。在第 4 部分中,您将学习如何将用户和配置文件添加到您的应用程序,以及如何发布和保护您的应用程序。

In chapters 23 through 25 you’ll learn how to protect your applications with authentication and authorization. In chapter 23 you’ll see how you can add ASP.NET Core Identity to your apps so that users can log in and enjoy a customized experience. You’ll learn how to protect your Razor Pages apps using authorization in chapter 24 so that only some users can access certain pages in your app. In chapter 25 you’ll learn how to apply the same protections to your minimal API and web API applications.
在第 23 章到第 25 章中,您将学习如何使用身份验证和授权保护您的应用程序。在第 23 章中,您将了解如何将 ASP.NET Core Identity 添加到您的应用程序中,以便用户可以登录并享受自定义体验。您将在第 24 章中了解如何使用授权保护 Razor Pages 应用程序,以便只有部分用户可以访问应用程序中的某些页面。在第 25 章中,您将学习如何将相同的保护应用于您的最小 API 和 Web API 应用程序。

Adding logging to your application is one of those activities that’s often left until after you discover a problem in production. Adding sensible logging from the get-go will help you quickly diagnose and fix errors as they arise. Chapter 26 introduces the logging framework built into ASP.NET Core. You’ll see how you can use it to write log messages to a wide variety of locations, whether it’s the console, a file, or a third-party remote-logging service.
向应用程序添加日志记录是通常要等到您在生产中发现问题后才进行的活动之一。从一开始就添加合理的日志记录将帮助您快速诊断和修复出现的错误。第 26 章介绍了 ASP.NET Core 中内置的日志记录框架。您将了解如何使用它将日志消息写入各种位置,无论是控制台、文件还是第三方远程日志记录服务。

By this point you’ll have all the fundamentals to build a production application with ASP.NET Core. In chapter 27 I cover the steps required to make your app live, including how to publish an app to Internet Information Services (IIS) and how to configure the URLs your app listens on.
此时,您将拥有使用 ASP.NET Core 构建生产应用程序的所有基础知识。在第 27 章中,我将介绍使您的应用程序上线所需的步骤,包括如何将应用程序发布到 Internet Information Services (IIS) 以及如何配置您的应用程序侦听的 URL。

Before you expose your application to the world, an important part of web development is securing your app correctly. Even if you don’t feel you have any sensitive data in your application, you must make sure to protect your users from attacks by adhering to security best practices. You’ll learn how to configure HTTPS for your application in chapter 28 and why this is a vital step for modern web development. Similarly, in chapter 29 I describe some common security vulnerabilities, how attackers can exploit them, and what you can do to protect your applications.
在向全世界公开您的应用程序之前,Web 开发的一个重要部分是正确保护您的应用程序。即使您认为应用程序中没有任何敏感数据,也必须确保通过遵守安全最佳实践来保护您的用户免受攻击。您将在第 28 章中学习如何为您的应用程序配置 HTTPS,以及为什么这是现代 Web 开发的关键步骤。同样,在第 29 章中,我描述了一些常见的安全漏洞,攻击者如何利用它们,以及您可以采取哪些措施来保护您的应用程序。

23 Authentication: Adding users to your application with Identity
23 身份验证:使用 Identity 将用户添加到您的应用程序

This chapter covers
本章介绍

• Seeing how authentication works in web apps in ASP.NET Core
了解身份验证在 ASP.NET Core中的 Web 应用程序中的工作原理
• Creating a project using the ASP.NET Core Identity system
使用 ASP.NET Core Identity 系统创建项目
• Adding user functionality to an existing web app
向现有 Web 应用程序添加用户功能
• Customizing the default ASP.NET Core Identity UI
自定义默认 ASP.NET Core Identity UI

One of the selling points of a web framework like ASP.NET Core is the ability to provide a dynamic app, customized to individual users. Many apps have the concept of an “account” with the service, which you can “sign in” to and get a different experience.
像 ASP.NET Core 这样的 Web 框架的卖点之一是能够提供针对个人用户定制的动态应用程序。许多应用程序都具有该服务的“帐户”概念,您可以“登录”该帐户并获得不同的体验。

Depending on the service, an account gives you varying things. On some apps you may have to sign in to get access to additional features, and on others you might see suggested articles. On an e-commerce app, you’d be able to place orders and view your past orders; on Stack Overflow you can post questions and answers; on a news site you might get a customized experience based on previous articles you’ve viewed.
根据服务的不同,账户会为您提供不同的内容。在某些应用程序上,您可能必须登录才能访问其他功能,而在其他应用程序上,您可能会看到推荐的文章。在电子商务应用程序上,您将能够下订单并查看您过去的订单;在 Stack Overflow 上,您可以发布问题和答案;在新闻网站上,您可能会根据您以前查看过的文章获得自定义体验。

When you think about adding users to your application, you typically have two aspects to consider:
当您考虑向应用程序添加用户时,通常需要考虑两个方面:

• Authentication—The process of creating users and letting them log in to your app
身份验证 - 创建用户并允许其登录应用程序的过程
• Authorization—Customizing the experience and controlling what users can do, based on the current logged-in user
授权 - 根据当前登录的用户自定义体验并控制用户可以执行的作

In this chapter I’m going to be discussing the first of these points, authentication and membership. In the next chapter I’ll tackle the second point, authorization. In section 23.1 I discuss the difference between authentication and authorization, how authentication works in a traditional ASP.NET Core web app, and ways you can architect your system to provide sign-in functionality. I don’t discuss API applications in detail in this chapter, though many of the authentication principles apply to both styles of app. I discuss API applications chapter 25.
在本章中,我将讨论第一点,身份验证和成员资格。在下一章中,我将讨论第二点,授权。在第 23.1 节中,我将讨论身份验证和授权之间的区别、身份验证在传统 ASP.NET Core Web 应用程序中的工作原理,以及构建系统以提供登录功能的方法。在本章中,我不会详细讨论 API 应用程序,尽管许多身份验证原则适用于这两种类型的应用程序。我将讨论 API 应用程序第 25 章。

In section 23.2 I introduce a user-management system called ASP.NET Core Identity (Identity for short). Identity integrates with Entity Framework Core (EF Core) and provides services for creating and managing users, storing and validating passwords, and signing users in and out of your app.
在 Section 23.2 中,我介绍了一个名为 ASP.NET Core Identity(简称 Identity)的用户管理系统。Identity 与 Entity Framework Core (EF Core) 集成,并提供用于创建和管理用户、存储和验证密码以及让用户登录和注销应用程序的服务。

In section 23.3 you’ll create an app using a default template that includes ASP.NET Core Identity out of the box. This gives you an app to explore and see the features Identity provides, as well as everything it doesn’t.
在 Section 23.3 中,您将使用默认模板创建一个应用程序,该模板包含开箱即用 ASP.NET Core Identity。这为您提供了一个应用程序来探索和查看 Identity 提供的功能,以及它不提供的所有内容。

Creating an app is great for seeing how the pieces fit together, but you’ll often need to add users and authentication to an existing app. In section 23.4 you’ll see the steps required to add ASP.NET Core Identity to an existing app.
创建应用程序非常适合查看各个部分如何组合在一起,但您通常需要向现有应用程序添加用户和身份验证。在 Section 23.4 中,您将看到将 ASP.NET Core Identity 添加到现有应用程序所需的步骤。

In sections 23.5 and 23.6 you’ll learn how to replace pages from the default Identity UI by scaffolding individual pages. In section 23.5 you’ll see how to customize the Razor templates to generate different HTML on the user registration page, and in section 23.6 you’ll learn how to customize the logic associated with a Razor Page. You’ll see how to store additional information about a user (such as their name or date of birth) and how to provide them permissions that you can later use to customize the app’s behavior (if the user is a VIP, for example).
在第 23.5 节和第 23.6 节中,您将学习如何通过搭建单个页面的基架来替换默认 Identity UI 中的页面。在第 23.5 节中,你将了解如何自定义 Razor 模板以在用户注册页上生成不同的 HTML,在第 23.6 节中,你将了解如何自定义与 Razor 页面关联的逻辑。您将了解如何存储有关用户的其他信息(例如他们的姓名或出生日期),以及如何为他们提供稍后可用于自定义应用程序行为的权限(例如,如果用户是 VIP)。

Before we look at the ASP.NET Core Identity system specifically, let’s take a look at authentication and authorization in ASP.NET Core—what’s happening when you sign in to a website and how you can design your apps to provide this functionality.
在我们具体研究 ASP.NET Core Identity 系统之前,让我们先看一下 ASP.NET Core 中的身份验证和授权 - 当您登录网站时会发生什么,以及如何设计您的应用程序来提供此功能。

23.1 Introducing authentication and authorization

23.1 身份验证和授权简介

When you add sign-in functionality to your app and control access to certain functions based on the currently signed-in user, you’re using two distinct aspects of security:
当您向应用添加登录功能并根据当前登录的用户控制对某些功能的访问时,您将使用两个不同的安全性方面:

• Authentication—The process of determining who you are
身份验证 - 确定您是谁的过程
• Authorization—The process of determining what you’re allowed to do
授权 - 确定允许您执行的作的过程

Generally you need to know who the user is before you can determine what they’re allowed to do, so authentication always comes first, followed by authorization. In this chapter we’re looking only at authentication; we’ll cover authorization in chapter 24.
通常,您需要先知道用户是谁,然后才能确定允许他们做什么,因此身份验证始终排在第一位,然后是授权。在本章中,我们只关注身份验证;我们将在第 24 章中介绍授权。

In this section I start by discussing how ASP.NET Core thinks about users, and I cover some of the terminology and concepts that are central to authentication. I found this to be the hardest part to grasp when I learned about authentication, so I’ll take it slow.
在本节中,我首先讨论 ASP.NET Core 如何看待用户,并介绍一些对身份验证至关重要的术语和概念。当我了解身份验证时,我发现这是最难掌握的部分,因此我会慢慢来。

Next, we’ll look at what it means to sign in to a traditional web app. After all, you only provide your password and sign into an app on a single page; how does the app know the request came from you for subsequent requests?
接下来,我们将了解登录到传统 Web 应用程序意味着什么。毕竟,您只需在单个页面上提供密码并登录应用程序;应用程序如何知道您的后续请求来自您?

23.1.1 Understanding users and claims in ASP.NET Core

23.1.1 了解 ASP.NET Core 中的用户和声明

The concept of a user is baked into ASP.NET Core. In chapter 3 you learned that the HTTP server, Kestrel, creates an HttpContext object for every request it receives. This object is responsible for storing all the details related to that request, such as the request URL, any headers sent, and the body of the request.
用户的概念已融入 ASP.NET Core。在第 3 章中,您了解了 HTTP 服务器 Kestrel 为它收到的每个请求创建一个 HttpContext 对象。此对象负责存储与该请求相关的所有详细信息,例如请求 URL、发送的任何标头以及请求正文。

The HttpContext object also exposes the current principal for a request as the User property. This is ASP.NET Core’s view of which user made the request. Any time your app needs to know who the current user is or what they’re allowed to do, it can look at the HttpContext.User principal.
HttpContext 对象还将请求的当前主体公开为 User 属性。这是 ASP.NET Core 对哪个用户发出请求的视图。每当你的应用程序需要知道当前用户是谁或允许他们做什么时,它都可以查看 HttpContext.User 主体。

DEFINITION You can think of the principal as the user of your app.
定义:您可以将主体视为应用程序的用户。

In ASP.NET Core, principals are implemented using the ClaimsPrincipal class, which has a collection of claims associated with it, as shown in figure 23.1.
在 ASP.NET Core 中,主体是使用 ClaimsPrincipal 类实现的,该类具有与之关联的声明集合,如图 23.1 所示。

alt text

Figure 23.1 The principal is the current user, implemented as ClaimsPrincipal. It contains a collection of Claims that describe the user.
图 23.1 主体是当前用户,实现为 ClaimsPrincipal。它包含描述用户的 Claims 集合。

You can think about claims as properties of the current user. For example, you could have claims for things like email, name, and date of birth.
您可以将声明视为当前用户的属性。例如,您可以对电子邮件、姓名和出生日期等内容提出索赔。

DEFINITION A claim is a single piece of information about a principal; it consists of a claim type and an optional value.
定义:索赔是有关委托人的单个信息;它由 Claim 类型和 Optional Value 组成。

Claims can also be indirectly related to permissions and authorization, so you could have a claim called HasAdminAccess or IsVipCustomer. These would be stored in the same way—as claims associated with the user principal.
声明也可以与权限和授权间接相关,因此您可以有一个名为 HasAdminAccess 或 IsVipCustomer 的声明。这些请求的存储方式与与用户主体关联的声明相同。

NOTE Earlier versions of ASP.NET used a role-based approach to security rather than a claims-based approach. The ClaimsPrincipal used in ASP.NET Core is compatible with this approach for legacy reasons, but you should use the claims-based approach for new apps.
注意:早期版本的 ASP.NET 使用基于角色的安全方法,而不是基于声明的方法。由于遗留原因,ASP.NET Core 中使用的 ClaimsPrincipal 与此方法兼容,但对于新应用,应使用基于声明的方法。

Kestrel assigns a user principal to every request that arrives at your app. Initially, that principal is a generic, anonymous, unauthenticated principal with no claims. How do you log in, and how does ASP.NET Core know that you’ve logged in on subsequent requests?
Kestrel 为到达应用程序的每个请求分配一个用户主体。最初,该委托人是通用的、匿名的、未经身份验证的委托人,没有声明。您如何登录,ASP.NET Core 如何知道您已登录后续请求?

In the next section we’ll look at how authentication works in a traditional web app using ASP.NET Core and the process of signing into a user account.
在下一节中,我们将了解使用 ASP.NET Core 在传统 Web 应用程序中进行身份验证的工作原理,以及登录用户帐户的过程。

23.1.2 Authentication in ASP.NET Core: Services and middleware

23.1.2 ASP.NET Core 中的身份验证:服务和中间件

Adding authentication to any web app involves a few moving parts. The same general process applies whether you’re building a traditional web app or a client-side app (though there are often differences in the latter, as I discuss in chapter 25):
向任何 Web 应用程序添加身份验证都涉及一些移动部件。无论您是构建传统的 Web 应用程序还是客户端应用程序,相同的一般过程都适用(尽管后者经常存在差异,正如我在第 25 章中讨论的那样):

  1. The client sends an identifier and a secret to the app to identify the current user. For example, you could send an email address (identifier) and a password (secret).
    客户端向应用程序发送标识符和密钥以识别当前用户。例如,您可以发送电子邮件地址 (identifier) 和密码 (secret)。

  2. The app verifies that the identifier corresponds to a user known by the app and that the corresponding secret is correct.
    应用程序验证标识符是否对应于应用程序已知的用户,以及相应的密钥是否正确。

  3. If the identifier and secret are valid, the app can set the principal for the current request, but it also needs a way of storing these details for subsequent requests. For traditional web apps, this is typically achieved by storing an encrypted version of the user principal in a cookie.
    如果标识符和密钥有效,则应用程序可以为当前请求设置主体,但它还需要一种方法来存储这些详细信息以供后续请求使用。对于传统的 Web 应用程序,这通常是通过将用户主体的加密版本存储在 Cookie 中来实现的。

This is the typical flow for most web apps, but in this section I’m going to look at how it works in ASP.NET Core. The overall process is the same, but it’s good to see how this pattern fits into the services, middleware, and Model-View-Controller (MVC) aspects of an ASP.NET Core application. We’ll step through the various pieces at play in a typical app when you sign in as a user, what that means, and how you can make subsequent requests as that user.
这是大多数 Web 应用程序的典型流程,但在本节中,我将介绍它在 ASP.NET Core 中的工作原理。整个过程是相同的,但很高兴看到此模式如何适应 ASP.NET Core 应用程序的服务、中间件和模型-视图-控制器 (MVC) 方面。我们将逐步介绍当您以用户身份登录时,典型应用程序中的各个部分、这意味着什么,以及您如何以该用户身份发出后续请求。

Signing in to an ASP.NET Core application
登录到 ASP.NET Core 应用程序

When you first arrive on a site and sign in to a traditional web app, the app will send you to a sign-in page and ask you to enter your username and password. After you submit the form to the server, the app redirects you to a new page, and you’re magically logged in! Figure 23.2 shows what’s happening behind the scenes in an ASP.NET Core app when you submit the form.
当您首次访问站点并登录到传统的 Web 应用程序时,该应用程序会将您转到登录页面,并要求您输入用户名和密码。将表单提交到服务器后,应用程序会将您重定向到新页面,然后您神奇地登录了!图 23.2 显示了当您提交表单时 ASP.NET Core 应用程序中的幕后情况。

alt text
Figure 23.2 Signing in to an ASP.NET Core application. SignInManager is responsible for setting HttpContext.User to the new principal and serializing the principal to the encrypted cookie.
图 23.2 登录到 ASP.NET Core 应用程序。SignInManager 负责将 HttpContext.User 设置为新主体,并将主体序列化为加密的 Cookie。

This figure shows the series of steps from the moment you submit the login form on a Razor Page to the point the redirect is returned to the browser. When the request first arrives, Kestrel creates an anonymous user principal and assigns it to the HttpContext.User property. The request is then routed to the Login.cshtml Razor Page, which reads the email and password from the request using model binding.
此图显示了从您在 Razor 页面上提交登录表单到将重定向返回到浏览器的一系列步骤。当请求首次到达时,Kestrel 会创建一个匿名用户主体,并将其分配给 HttpContext.User 属性。然后,该请求将路由到 Login.cshtml Razor 页面,该页面使用模型绑定从请求中读取电子邮件和密码。

The meaty work happens inside the SignInManager service. This is responsible for loading a user entity with the provided username from the database and validating that the password they provided is correct.
繁重的工作发生在 SignInManager 服务内部。这负责使用从数据库中提供的用户名加载用户实体,并验证他们提供的密码是否正确。

Warning Never store passwords in the database directly. They should be hashed using a strong one-way algorithm. The ASP.NET Core Identity system does this for you, but it’s always wise to reiterate this point!
警告:切勿将密码直接存储在数据库中。它们应该使用强大的单向算法进行哈希处理。ASP.NET Core Identity 系统为您执行此作,但重申这一点始终是明智的!

If the password is correct, SignInManager creates a new ClaimsPrincipal from the user entity it loaded from the database and adds the appropriate claims, such as the email address. It then replaces the old, anonymous HttpContext.User principal with the new, authenticated principal.
如果密码正确,SignInManager 将从它从数据库中加载的用户实体创建新的 ClaimsPrincipal,并添加相应的声明,例如电子邮件地址。然后,它将旧的匿名 HttpContext.User 主体替换为经过身份验证的新主体。

Finally, SignInManager serializes the principal, encrypts it, and stores it as a cookie. A cookie is a small piece of text that’s sent back and forth between the browser and your app along with each request, consisting of a name and a value.
最后,SignInManager 序列化主体,对其进行加密,并将其存储为 Cookie。Cookie 是一小段文本,它与每个请求一起在浏览器和应用程序之间来回发送,由名称和值组成。

This authentication process explains how you can set the user for a request when they first log in to your app, but what about subsequent requests? You send your password only when you first log in to an app, so how does the app know that it’s the same user making the request?
此身份验证过程说明了如何在用户首次登录您的应用程序时为用户设置请求,但后续请求呢?您仅在首次登录应用程序时发送密码,那么该应用程序如何知道它是发出请求的同一用户?

Authenticating users for subsequent requests
为后续请求对用户进行身份验证

The key to persisting your identity across multiple requests lies in the final step of figure 23.2, where you serialized the principal in a cookie. Browsers automatically send this cookie with all requests made to your app, so you don’t need to provide your password with every request.
在多个请求中保留身份的关键在于图 23.2 的最后一步,在该步骤中,您在 cookie 中序列化了主体。浏览器会自动将此 Cookie 与向您的应用发出的所有请求一起发送,因此您无需为每个请求提供密码。

ASP.NET Core uses the authentication cookie sent with the requests to rehydrate a ClaimsPrincipal and set the HttpContext.User principal for the request, as shown in figure 23.3. The important thing to note is when this process happens—in the AuthenticationMiddleware.
ASP.NET Core 使用随请求发送的身份验证 Cookie 来解除冻结 ClaimsPrincipal 并为请求设置 HttpContext.User 主体,如图 23.3 所示。需要注意的重要一点是此过程何时发生 — 在 AuthenticationMiddleware 中。

alt text

Figure 23.3 A subsequent request after signing in to an application. The cookie sent with the request contains the user principal, which is validated and used to authenticate the request.
图 23.3 登录应用程序后的后续请求。随请求发送的 Cookie 包含用户主体,该主体经过验证并用于对请求进行身份验证。

When a request containing the authentication cookie is received, Kestrel creates the default, unauthenticated, anonymous principal and assigns it to the HttpContext.User principal. Any middleware that runs before the AuthenticationMiddleware sees the request as unauthenticated, even if there’s a valid cookie.
收到包含身份验证 Cookie 的请求时,Kestrel 会创建默认的、未经身份验证的匿名主体,并将其分配给 HttpContext.User 主体。在 AuthenticationMiddleware 之前运行的任何中间件都会将请求视为未经身份验证,即使存在有效的 cookie。

Tip If it looks like your authentication system isn’t working, double-check your middleware pipeline. Only middleware that runs after AuthenticationMiddleware will see the request as authenticated.
提示:如果您的身份验证系统看起来无法正常工作,请仔细检查您的中间件管道。只有在 AuthenticationMiddleware 之后运行的中间件才会看到请求经过身份验证。

The AuthenticationMiddleware is responsible for setting the current user for a request. The middleware calls the authentication services, which reads the cookie from the request, decrypts it, and deserializes it to obtain the ClaimsPrincipal created when the user logged in.
AuthenticationMiddleware 负责为请求设置当前用户。中间件调用身份验证服务,该服务从请求中读取 Cookie,对其进行解密,然后对其进行反序列化,以获取在用户登录时创建的 ClaimsPrincipal。

The AuthenticationMiddleware sets the HttpContext.User principal to the new, authenticated principal. All subsequent middleware now knows the user principal for the request and can adjust its behavior accordingly (for example, displaying the user’s name on the home page or restricting access to some areas of the app).
AuthenticationMiddleware 将 HttpContext.User 主体设置为经过身份验证的新主体。现在,所有后续中间件都知道请求的用户主体,并可以相应地调整其行为(例如,在主页上显示用户名或限制对应用程序某些区域的访问)。

NOTE The AuthenticationMiddleware is responsible only for authenticating incoming requests and setting the ClaimsPrincipal if the request contains an authentication cookie. It is not responsible for redirecting unauthenticated requests to the login page or rejecting unauthorized requests; that is handled by the AuthorizationMiddleware, as you’ll see in chapter 24.
注意:AuthenticationMiddleware 只负责对传入请求进行身份验证,并在请求包含身份验证 Cookie 时设置 ClaimsPrincipal。它不负责将未经身份验证的请求重定向到登录页面或拒绝未经授权的请求;它由 AuthorizationMiddleware 处理,您将在第 24 章中看到。

The process described so far, in which a single app authenticates the user when they log in and sets a cookie that’s read on subsequent requests, is common with traditional web apps, but it isn’t the only possibility. In chapter 25 we’ll take a look at authentication for web API applications, used by client-side and mobile apps and at how the authentication system changes for those scenarios.
到目前为止描述的过程,即单个应用程序在用户登录时对用户进行身份验证,并设置在后续请求中读取的 cookie,这在传统 Web 应用程序中很常见,但并不是唯一的可能性。在第 25 章中,我们将介绍客户端和移动应用程序使用的 Web API 应用程序的身份验证,以及这些场景的身份验证系统如何变化。

Another thing to consider is where you store the authentication details for users of your app. In figure 23.2 I showed the authentication services loading the user authentication details from your app’s database, but that’s only one option.
要考虑的另一件事是存储应用程序用户的身份验证详细信息的位置。在图 23.2 中,我展示了从应用程序数据库中加载用户身份验证详细信息的身份验证服务,但这只是一个选项。

Another option is to delegate the authentication responsibilities to a third-party identity provider, such as Okta, Auth0, Azure Active Directory B2B/B2C, or even Facebook. These manage users for you, so user information and passwords are stored in their database rather than your own. The biggest advantage of this approach is that you don’t have to worry about making sure your customer data is safe; you can be pretty sure that a third party will protect it, as it’s their whole business.
另一种选择是将身份验证责任委托给第三方身份提供商,例如 Okta、Auth0、Azure Active Directory B2B/B2C 甚至 Facebook。这些 Bug 会为您管理用户,因此用户信息和密码存储在他们的数据库中,而不是您自己的数据库中。这种方法的最大优点是您不必担心确保客户数据的安全;您可以非常确定第三方会保护它,因为这是他们的全部业务。

Tip Wherever possible, I recommend this approach, as it delegates security responsibilities to someone else. You can’t lose your users’ details if you never had them! Make sure to understand the differences in providers, however. With a provider like Auth0, you would own the profiles created, whereas with a provider like Facebook, you don’t!
提示:我尽可能推荐这种方法,因为它将安全责任委托给其他人。如果您从未拥有用户的详细信息,您就不会丢失它们!但是,请务必了解提供程序之间的差异。使用像 Auth0 这样的提供商,您将拥有创建的配置文件,而使用像 Facebook 这样的提供商,您将不拥有!

Each provider provides instructions on how to integrate with their identity services, ideally using the OpenID Connect (OIDC) specification. This typically involves configuring some authentication services in your application, adding some configuration, and delegating the authentication process itself to the external provider. These providers can be used with your API apps too, as I discuss in chapter 25.
每个提供商都提供了有关如何与其身份服务集成的说明,最好使用 OpenID Connect (OIDC) 规范。这通常涉及在应用程序中配置一些身份验证服务、添加一些配置以及将身份验证过程本身委托给外部提供商。这些提供程序也可以用于您的 API 应用程序,正如我在第 25 章中讨论的那样。

NOTE Hooking up your apps and APIs to use an identity provider can require a fair amount of tedious configuration, both in the app and the identity provider, but if you follow the provider’s documentation you should have plain sailing. For example, you can follow the documentation for adding authentication to a traditional web app using Microsoft’s Identity Platform here: http://mng.bz/4D9w.
注意:将应用程序和 API 挂接以使用身份提供商可能需要在应用程序和身份提供商中进行大量繁琐的配置,但如果您遵循提供商的文档,您应该会一帆风顺。例如,您可以按照以下文档使用 Microsoft 的 Identity Platform 将身份验证添加到传统 Web 应用程序:http://mng.bz/4D9w

While I recommend using an external identity provider where possible, sometimes you really want to store all the authentication details of your users directly in your app. That’s the approach I describe in this chapter.
虽然我建议尽可能使用外部身份提供商,但有时您确实希望将用户的所有身份验证详细信息直接存储在您的应用程序中。这就是我在本章中描述的方法。

ASP.NET Core Identity (hereafter shortened to Identity) is a system that makes building the user-management aspect of your app. It handles all the boilerplate for saving and loading users to a database, as well as best practices for security, such as user lockout, password hashing, and multifactor authentication.
ASP.NET Core Identity(以下简称 Identity)是一个用于构建应用程序的用户管理方面的系统。它处理将用户保存和加载到数据库的所有样板,以及安全性最佳实践,例如用户锁定、密码哈希和多重身份验证。

DEFINITION Multifactor authentication (MFA), and the subset two-factor authentication (2FA) require both a password and an extra piece of information to sign in. This could involve sending a code to a user’s phone by Short Message Service (SMS) or using a mobile app to generate a code, for example.
定义:多重身份验证 (MFA) 和子集双重身份验证 (2FA) 需要密码和额外的信息才能登录。例如,这可能涉及通过短信服务 (SMS) 向用户的手机发送验证码,或使用移动应用生成验证码。

In the next section I’m going to talk about the ASP.NET Core Identity system, the problems it solves, when you’d want to use it, and when you might not want to use it. In section 23.3 we take a look at some code and see ASP.NET Core Identity in action.
在下一节中,我将讨论 ASP.NET Core Identity 系统、它解决的问题、何时要使用它以及何时可能不想使用它。在 Section 23.3 中,我们看了一些代码,并看到了 Core Identity ASP.NET 实际应用。

23.2 What is ASP.NET Core Identity?

23.2 什么是 ASP.NET Core Identity?

Whenever you need to add nontrivial behaviors to your application, you typically need to add users and authentication. That means you’ll need a way of persisting details about your users, such as their usernames and passwords.
每当需要向应用程序添加重要行为时,通常需要添加用户和身份验证。这意味着您需要一种方法来保留有关用户的详细信息,例如他们的用户名和密码。

This might seem like a relatively simple requirement, but given that this is related to security and people’s personal details, it’s important you get it right. As well as storing the claims for each user, it’s important to store passwords using a strong hashing algorithm to allow users to use MFA where possible and to protect against brute-force attacks, to name a few of the many requirements. Although it’s perfectly possible to write all the code to do this manually and to build your own authentication and membership system, I highly recommend you don’t.
这似乎是一个相对简单的要求,但考虑到这与安全和人们的个人详细信息有关,因此请务必正确处理。除了存储每个用户的声明外,使用强大的哈希算法存储密码也很重要,这样用户就可以尽可能使用 MFA 并防止暴力攻击,仅举几例。尽管完全可以编写所有代码来手动执行此作并构建您自己的身份验证和成员资格系统,但我强烈建议您不要这样做。

I’ve already mentioned third-party identity providers such as Auth0 and Azure Active Directory. These Software as a Service (SaaS) solutions take care of the user-management and authentication aspects of your app for you. If you’re in the process of moving apps to the cloud generally, solutions like these can make a lot of sense.
我已经提到了第三方身份提供商,例如 Auth0 和 Azure Active Directory。这些软件即服务 (SaaS) 解决方案为您处理应用程序的用户管理和身份验证方面。如果您通常正在将应用程序迁移到云,那么像这样的解决方案可能非常有意义。

If you can’t or don’t want to use these third-party solutions, I recommend you consider using the ASP.NET Core Identity system to store and manage user details in your database. ASP.NET Core Identity takes care of most of the boilerplate associated with authentication, but it remains flexible and lets you control the login process for users if you need to.
如果您不能或不想使用这些第三方解决方案,我建议您考虑使用 ASP.NET Core Identity 系统在您的数据库中存储和管理用户详细信息。ASP.NET Core Identity 负责与身份验证相关的大部分样板,但它仍然保持灵活性,并允许您根据需要控制用户的登录过程。

NOTE ASP.NET Core Identity is an evolution of the legacy .NET Framework ASP.NET Identity system, with some design improvements and update to work with ASP.NET Core.
注意: ASP.NET Core Identity 是旧版 .NET Framework ASP.NET Identity 系统的演变,经过一些设计改进和更新以与 ASP.NET Core 配合使用。

By default, ASP.NET Core Identity uses EF Core to store user details in the database. If you’re already using EF Core in your project, this is a perfect fit. Alternatively, it’s possible to write your own stores for loading and saving user details in another way.
默认情况下,ASP.NET Core Identity 使用 EF Core 将用户详细信息存储在数据库中。如果你已在项目中使用 EF Core,则这是一个完美的选择。或者,可以编写自己的 store 以另一种方式加载和保存用户详细信息。

Identity takes care of the low-level parts of user management, as shown in table 23.1. As you can see from this list, Identity gives you a lot, but not everything—by a long shot!
Identity 负责用户 Management 的低级部分,如 Table 23.1 所示。从这个列表中可以看出,Identity 能给你很多,但不是全部——很长一段时间!

Table 23.1 Which services are and aren’t handled by ASP.NET Core Identity
表 23.1 哪些服务由 ASP.NET Core Identity 处理,哪些服务不由 Core Identity 处理

Managed by ASP.NET Core Identity Requires implementing by the developer
Database schema for storing users and claims UI for logging in, creating, and managing users (Razor Pages or controllers); included in an optional package that provides a default UI
Creating a user in the database Sending email messages
Password validation and rules Customizing claims for users (adding new claims)
Handling user account lockout (to prevent brute-force attacks) Configuring third-party identity providers
Managing and generating MFA/2FA codes Integration into MFA such as sending SMS messages, time-based one-time password (TOTP) authenticator apps, or hardware keys
Generating password-reset tokens -
Saving additional claims to the database -
Managing third-party identity providers (for example, Facebook, Google, and Twitter) -

The biggest missing piece is the fact that you need to provide all the UI for the application, as well as tying all the individual Identity services together to create a functioning sign-in process. That’s a big missing piece, but it makes the Identity system extremely flexible.
最大的缺失部分是您需要为应用程序提供所有 UI,以及将所有单独的 Identity 服务捆绑在一起以创建有效的登录过程。这是一个很大的缺失部分,但它使 Identity 系统非常灵活。

Luckily, ASP.NET Core includes a helper NuGet library, Microsoft.AspNetCore.Identity.UI, that gives you the whole of the UI boilerplate for free. That’s over 30 Razor Pages with functionality for logging in, registering users, using 2FA, and using external login providers, among other features. You can still customize these pages if you need to, but having a whole login process working out of the box, with no code required on your part, is a huge win. We’ll look at this library and how you use it in sections 23.3 and 23.4.
幸运的是,ASP.NET Core 包含一个帮助程序 NuGet 库 Microsoft.AspNetCore.Identity.UI,它免费为您提供整个 UI 样板。这是 30 多个 Razor 页面,具有登录、注册用户、使用 2FA 和使用外部登录提供程序等功能。如果需要,您仍然可以自定义这些页面,但是拥有一个开箱即用的整个登录过程,而无需您编写任何代码,这是一个巨大的胜利。我们将在 23.3 和 23.4 节中介绍这个库以及你如何使用它。

For that reason, I strongly recommend using the default UI as a starting point, whether you’re creating an app or adding user management to an existing app. But the question remains as to when you should use Identity and when you should consider rolling your own.
因此,我强烈建议使用默认 UI 作为起点,无论您是创建应用程序还是向现有应用程序添加用户管理。但问题仍然存在,何时应该使用 Identity 以及何时应该考虑推出自己的 Identity。

I’m a big fan of Identity when you need to store your own users, so I tend to suggest it in most situations, as it handles a lot of security-related things for you that are easy to mess up. I’ve heard several arguments against it, some valid and others less so:
当您需要存储自己的用户时,我是 Identity 的忠实粉丝,因此我倾向于在大多数情况下建议使用它,因为它可以为您处理很多与安全相关的事情,这些事情很容易搞砸。我听到了几个反对它的论点,有些是有效的,有些则不太有效:

• I already have user authentication in my app. Great! In that case, you’re probably right, Identity may not be necessary. But does your custom implementation use MFA? Do you have account lockout? If not, and if you need to add them, considering Identity may be worthwhile.
我的应用程序中已经有用户身份验证。太好了!在这种情况下,您可能是对的,Identity 可能不是必需的。但是您的自定义实施是否使用 MFA?您是否有帐户锁定?如果没有,并且您需要添加它们,考虑 Identity 可能是值得的。

• I don’t want to use EF Core. That’s a reasonable stance. You could be using Dapper, some other object-relational mapper (ORM), or even a document database for your database access. Luckily, the database integration in Identity is pluggable, so you could swap out the EF Core integration and use your own database integration libraries instead.
我不想使用 EF Core。这是一个合理的立场。您可以使用 Dapper、其他一些对象关系映射器 (ORM),甚至是文档数据库来访问数据库。幸运的是,Identity 中的数据库集成是可插拔的,因此您可以换掉 EF Core 集成并改用自己的数据库集成库。

• My use case is too complex for Identity. Identity provides lower-level services for authentication, so you can compose the pieces however you like. It’s also extensible, so if you need to, for example, transform claims before creating a principal, you can.
我的用例对于 Identity 来说太复杂了。Identity 提供较低级别的身份验证服务,因此您可以根据自己的喜好组合各个部分。它也是可扩展的,因此,如果需要在创建主体之前转换声明,则可以。

• I don’t like the default Razor Pages UI. The default UI for Identity is entirely optional. You can still use the Identity services and user management but provide your own UI for logging in and registering users. However, be aware that although doing this gives you a lot of flexibility, it’s also easy to introduce a security flaw in your user-management system—the last place you want security flaws!
我不喜欢默认的 Razor Pages UI。Identity 的默认 UI 完全是可选的。您仍然可以使用 Identity 服务和用户管理,但提供自己的 UI 来登录和注册用户。但是,请注意,尽管这样做可以为您提供很大的灵活性,但也很容易在用户管理系统中引入安全漏洞 - 这是您最不希望出现安全漏洞的地方!

• I’m not using Bootstrap to style my application. The default Identity UI uses Bootstrap as a styling framework, the same as the default ASP.NET Core templates. Unfortunately, you can’t easily change that, so if you’re using a different framework or need to customize the HTML generated, you can still use Identity, but you’ll need to provide your own UI.
我没有使用 Bootstrap 来设置应用程序的样式。默认身份 UI 使用 Bootstrap 作为样式框架,与默认的 ASP.NET Core 模板相同。遗憾的是,您无法轻松更改此设置,因此,如果您使用的是其他框架或需要自定义生成的 HTML,您仍然可以使用 Identity,但需要提供自己的 UI。

• I don’t want to build my own identity system. I’m glad to hear it. Using an external identity provider like Azure Active Directory or Auth0 is a great way of shifting the responsibility and risk associated with storing users’ personal information to a third party.
我不想构建自己的身份系统。我很高兴听到这个消息。使用 Azure Active Directory 或 Auth0 等外部身份提供商是将与存储用户个人信息相关的责任和风险转移给第三方的好方法。

Any time you’re considering adding user management to your ASP.NET Core application, I’d recommend looking at Identity as a great option for doing so. In the next section I’ll demonstrate what Identity provides by creating a new Razor Pages application using the default Identity UI. In section 23.4 we’ll take that template and apply it to an existing app instead, and in sections 23.5 and 23.6 you’ll see how to override the default pages.
每当你考虑向 ASP.NET Core 应用程序添加用户管理时,我建议将 Identity 视为一个不错的选择。在下一节中,我将通过使用默认标识 UI 创建新的 Razor Pages 应用程序来演示标识提供的功能。在 23.4 节中,我们将获取该模板并将其应用于现有应用程序,在 23.5 和 23.6 节中,您将看到如何覆盖默认页面。

23.3 Creating a project that uses ASP.NET Core Identity

23.3 创建使用 ASP.NET Core Identity 的项目

I’ve covered authentication and Identity in general terms, but the best way to get a feel for it is to see some working code. In this section we’re going to look at the default code generated by the ASP.NET Core templates with Identity, how the project works, and where Identity fits in.
我已经大致介绍了身份验证和标识,但了解它的最佳方法是查看一些工作代码。在本节中,我们将了解使用 Identity 的 ASP.NET Core 模板生成的默认代码、项目的工作原理以及 Identity 的适用范围。

23.3.1 Creating the project from a template

23.3.1 从模板创建项目

You’ll start by using the Visual Studio templates to generate a simple Razor Pages application that uses Identity for storing individual user accounts in a database.
首先,使用 Visual Studio 模板生成一个简单的 Razor Pages 应用程序,该应用程序使用 Identity 将各个用户帐户存储在数据库中。

Tip You can create a similar project using the .NET CLI by running dotnet new webapp -au Individual. The Visual Studio template uses a LocalDB database, but the dotnet new template uses SQLite by default. To use LocalDB instead, run dotnet new webapp -au Individual --use-local-db.
提示:您可以通过运行 dotnet new webapp -au Individual 来使用 .NET CLI 创建类似的项目。Visual Studio 模板使用 LocalDB 数据库,但 dotnet 新模板默认使用 SQLite。要改用 LocalDB,请运行 dotnet new webapp -au Individual --use-local-db。

To create the template using Visual Studio, you must be using the 2022 version or later and have the .NET 7 software development kit (SDK) installed. Follow these steps:
要使用 Visual Studio 创建模板,您必须使用 2022 版本或更高版本,并安装 .NET 7 软件开发工具包 (SDK)。请执行以下步骤:

  1. Choose File > New > Project or choose Create a New Project on the splash screen.
    在初始屏幕上选择File > New > Project 创建新项目。

  2. From the list of templates, choose ASP.NET Core Web Application, ensuring that you select the C# language template.
    从模板列表中,选择 ASP.NET Core Web Application(核心 Web 应用程序),确保选择 C# 语言模板。

  3. On the next screen, enter a project name, location, and a solution name, and choose Create.
    在下一个屏幕上,输入项目名称、位置和解决方案名称,然后选择 Create (创建)。

  4. On the Additional Information screen, change the Authentication type to Individual Accounts, as shown in figure 23.4. Leave the other settings at their defaults, and choose Create to create the application.
    在 Additional Information 屏幕上,将 Authentication type 更改为 Individual Accounts,如图 23.4 所示。将其他设置保留为默认值,然后选择 Create (创建) 以创建应用程序。

Visual Studio automatically runs dotnet restore to restore all the necessary NuGet packages for the project.
Visual Studio 会自动运行 dotnet restore 来还原项目所需的所有 NuGet 包。

alt text
Figure 23.4 Choosing the authentication mode of the new ASP.NET Core application template in VS 2022
图 23.4 在 VS 2022 中选择新 ASP.NET Core 应用程序模板的身份验证模式

  1. Run the application to see the default app, as shown in figure 23.5.
    运行应用程序以查看默认应用程序,如图 23.5 所示。

NOTE The Visual Studio template configures the application to use LocalDB and includes EF Core migrations for SQL Server. If you want to use a different database provider, you can replace the configuration and migrations with your database of choice, as described in chapter 12.
注意:Visual Studio 模板将应用程序配置为使用 LocalDB,并包括 SQL Server 的 EF Core 迁移。如果要使用不同的数据库提供程序,可以将配置和迁移替换为您选择的数据库,如第 12 章所述。

alt text

Figure 23.5 The default template with individual account authentication looks similar to the no authentication template, with the addition of a Login widget at the top right of the page.
图 23.5 具有个人帐户身份验证的默认模板看起来类似于无身份验证模板,只是在页面右上角添加了一个 Login 小部件。

This template should look familiar, with one twist: you now have Register and Login buttons! Feel free to play with the template—creating a user, logging in and out—to get a feel for the app. Once you’re happy, look at the code generated by the template and the boilerplate it saved you from writing.
这个模板应该看起来很熟悉,但有一个变化:您现在有 Register 和 Login 按钮了!您可以随意使用模板 — 创建用户、登录和注销 — 以感受应用程序。满意后,请查看模板生成的代码以及它使您免于编写的样板。

Tip Don’t forget to run the included EF Core migrations before trying to create users. Run dotnet ef database update from the project folder.
提示:在尝试创建用户之前,请不要忘记运行包含的 EF Core 迁移。从项目文件夹运行 dotnet ef database update。

23.3.2 Exploring the template in Solution Explorer

23.3.2 在解决方案资源管理器中浏览模板

The project generated by the template, shown in figure 23.6, is similar to the default no-authentication template. That’s largely due to the default UI library, which brings in a big chunk of functionality without exposing you to the nitty-gritty details.
该模板生成的项目(如图 23.6 所示)类似于默认的 no-authentication 模板。这主要是由于默认的 UI 库,它带来了大量功能,而不会让您了解细节。

alt text

Figure 23.6 The project layout of the default template with individual authentication
图 23.6 使用单独身份验证的默认模板的项目布局

The biggest addition is the Areas folder in the root of your project, which contains an Identity subfolder. Areas are sometimes used for organizing sections of functionality. Each area can contain its own Pages folder, which is analogous to the main Pages folder in your application.
最大的新增功能是项目根目录中的 Areas 文件夹,其中包含一个 Identity 子文件夹。区域有时用于组织功能部分。每个区域都可以包含自己的 Pages 文件夹,该文件夹类似于应用程序中的主 Pages 文件夹。

DEFINITION Areas are used to group Razor Pages into separate hierarchies for organizational purposes. I rarely use areas and prefer to create subfolders in the main Pages folder instead. The one exception is the Identity UI, which uses a separate Identity area by default. For more details on areas, see Microsoft’s “Areas in ASP.NET Core” documentation: http://mng.bz/7Vw9.
定义:区域用于将 Razor 页面分组到单独的层次结构中,以便进行组织。我很少使用区域,更喜欢在主 Pages 文件夹中创建子文件夹。一个例外是 Identity UI,默认情况下,它使用单独的 Identity 区域。有关区域的更多详细信息,请参阅 Microsoft 的“ASP.NET Core 中的区域”文档:http://mng.bz/7Vw9

The Microsoft.AspNetCore.Identity.UI package creates Razor Pages in the Identity area. You can override any page in this default UI by creating a corresponding page in the Areas/Identity/Pages folder in your application. In figure 23.6, the default template adds a _ViewStart.cshtml file that overrides the template that is included as part of the default UI. This file contains the following code, which sets the default Identity UI Razor Pages to use your project’s default _Layout.cshtml file:
Microsoft.AspNetCore.Identity.UI 包在“标识”区域中创建 Razor Pages。您可以通过在应用程序的 Areas/Identity/Pages 文件夹中创建相应的页面来覆盖此默认 UI 中的任何页面。在图 23.6 中,默认模板添加了一个 _ViewStart.cshtml 文件,该文件将替代作为默认 UI 的一部分包含的模板。此文件包含以下代码,该代码将默认标识 UI Razor 页面设置为使用项目的默认 _Layout.cshtml 文件:

@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}

Some obvious questions at this point are “How do you know what’s included in the default UI?” and “Which files can you override?” You’ll see the answers to both in section 23.5, but in general you should try to avoid overriding files where possible. After all, the goal with the default UI is to reduce the amount of code you have to write!
此时,一些明显的问题是“您如何知道默认 UI 中包含哪些内容”和“您可以覆盖哪些文件?您将在 Section 23.5 中看到这两个问题的答案,但一般来说,您应该尽可能避免覆盖文件。毕竟,默认 UI 的目标是减少您必须编写的代码量!

The Data folder in your new project template contains your application’s EF Core DbContext, called ApplicationDbContext, and the migrations for configuring the database schema to use Identity. I’ll discuss this schema in more detail in section 23.3.3.
新项目模板中的 Data 文件夹包含应用程序的 EF Core DbContext(称为 ApplicationDbContext)和用于将数据库架构配置为使用 Identity 的迁移。我将在 Section 23.3.3 中更详细地讨论这个模式。

The final additional file included in this template compared with the no-authentication version is the partial Razor view Pages/Shared/_LoginPartial.cshtml. This provides the Register and Login links you saw in figure 23.5, and it’s rendered in the default Razor layout, _Layout.cshtml.
与无身份验证版本相比,此模板中包含的最后一个附加文件是部分 Razor 视图 Pages/Shared/_LoginPartial.cshtml。这提供了你在图 23.5 中看到的 Register 和 Login 链接,并呈现在默认的 Razor 布局 _Layout.cshtml 中。

If you look inside _LoginPartial.cshtml, you can see how routing works with areas by combining the Razor Page path with an {area} route parameter using Tag Helpers. For example, the Login link specifies that the Razor Page /Account/Login is in the Identity area using the asp-area attribute:
如果查看 _LoginPartial.cshtml,则可以通过使用标记帮助程序将 Razor Page 路径与 {area} 路由参数组合在一起,了解路由如何与区域配合使用。例如,Login 链接使用 asp-area 属性指定 Razor Page /Account/Login 位于 Identity 区域中:

<a asp-area="Identity" asp-page="/Account/Login">Login</a>

Tip You can reference Razor Pages in the Identity area by setting the area route value to Identity. You can use the asp-area attribute in Tag Helpers that generate links.
提示:可以通过将区域路由值设置为 Identity 来引用 Identity 区域中的 Razor Pages。您可以在生成链接的标记帮助程序中使用 asp-area 属性。

In addition to viewing the new files included thanks to ASP.NET Core Identity, open Program.cs and look at the changes there. The most obvious change is the additional configuration, which adds all the services Identity requires, as shown in the following listing.
除了查看 ASP.NET Core Identity 包含的新文件外,还可以打开 Program.cs 并查看其中的更改。最明显的变化是额外的配置,它添加了 Identity 所需的所有服务,如下面的清单所示。

Listing 23.1 Adding ASP.NET Core Identity services to ConfigureServices
清单 23.1 向 ConfigureServices 添加 ASP.NET Core Identity 服务

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

string connectionString = builder.Configuration     #A
    .GetConnectionString("DefaultConnection");    #A
builder.Services.AddDbContext<ApplicationDbContext>(options =>  #A
    options.UseSqlServer(connectionString));    #A

builder.Services.AddDatabaseDeveloperPageExceptionFilter();   #B

builder.Services.AddDefaultIdentity<IdentityUser>(options =>    #C
    options.SignIn.RequireConfirmedAccount = true)    #D
        .AddEntityFrameworkStores<ApplicationDbContext>();    #E
builder.Services.AddRazorPages();

// remaining configuration not show

❶ ASP.NET Core Identity uses EF Core, so it includes the standard EF Core configuration.
ASP.NET Core Identity 使用 EF Core,因此它包括标准 EF Core 配置。
❷ Adds optional database services to enhance the DeveloperExceptionPage
添加可选的数据库服务以增强 DeveloperExceptionPage
❸ Adds the Identity system, including the default UI, and configures the user type as IdentityUser
添加标识系统,包括默认 UI,并将用户类型配置为 IdentityUser
❹ Requires users to confirm their accounts (typically by email) before they log in
要求用户在登录前确认其帐户(通常通过电子邮件)
❺ Configures Identity to store its data in EF Core
配置标识以将其数据存储在 EF Core 中

The AddDefaultIdentity() extension method does several things:
AddDefaultIdentity() 扩展方法执行以下几项作:

• Adds the core ASP.NET Core Identity services.
添加核心 ASP.NET 核心身份服务。
• Configures the application user type to be IdentityUser. This is the entity model that is stored in the database and represents a “user” in your application. You can extend this type if you need to, but that’s not always necessary, as you’ll see in section 23.6.
将应用程序用户类型配置为 IdentityUser。这是存储在数据库中的实体模型,表示应用程序中的 “用户”。如果需要,您可以扩展此类型,但这并不总是必要的,如第 23.6 节所示。
• Adds the default UI Razor Pages for registering, logging in, and managing users.
添加用于注册、登录和管理用户的默认 UI Razor Pages。
• Configures token providers for generating MFA and email confirmation tokens.
配置用于生成 MFA 和电子邮件确认令牌的令牌提供程序。

Where is the authentication middleware?
身份验证中间件在哪里?

If you’re already familiar with previous versions of ASP.NET Core, you might be surprised to notice the lack of any authentication middleware in the default template. Given everything you’ve learned about how authentication works, that should be surprising!
如果您已经熟悉 ASP.NET Core 的早期版本,您可能会惊讶地注意到默认模板中缺少任何身份验证中间件。鉴于您学到的有关身份验证工作原理的所有信息,这应该令人惊讶!

The answer to this riddle is that the authentication middleware is in the pipeline, even though you can’t see it. As I discussed in chapter 4, WebApplication automatically adds many middleware components to the pipeline for you, including the routing middleware, the endpoint middleware, and—yes—the authentication middleware. So the reason you don’t see it in the pipeline is that it’s already been added.
这个谜题的答案是,身份验证中间件正在开发中,即使您看不到它。正如我在第 4 章中所讨论的,WebApplication 会自动将许多中间件组件添加到管道中,包括路由中间件、端点中间件,是的,还有身份验证中间件。因此,您在管道中没有看到它的原因是它已被添加。

In fact, WebApplication also automatically adds the authorization middleware to the pipeline, but in this case the template still calls UseAuthorization(). Why? For the same reason that the template also calls UseRouting(): to control exactly where in the pipeline the middleware is added.
事实上,WebApplication 还会自动将授权中间件添加到管道中,但在这种情况下,模板仍然调用 UseAuthorization()。为什么?出于与模板还调用 UseRouting() 相同的原因:以准确控制中间件在管道中的添加位置。

As I mentioned in chapter 4, you can override the automatically added middleware by adding it yourself manually. It’s crucial that the authorization middleware be placed after the routing middleware, and as mentioned in chapter 4, you typically want to place your routing middleware after the static file middleware. As the routing middleware needs to move, so does the authorization middleware!
正如我在第 4 章中提到的,你可以通过自己手动添加来覆盖自动添加的中间件。将授权中间件放在路由中间件之后至关重要,如第 4 章所述,您通常希望将路由中间件放在静态文件中间件之后。由于路由中间件需要移动,授权中间件也需要移动!

Traditionally, the authentication middleware is also placed after the routing middleware, before the authorization middleware, but this isn’t crucial. The only requirement is that it’s placed before any middleware that requires an authenticated user, such as the authorization middleware.
传统上,身份验证中间件也放在路由中间件之后,授权中间件之前,但这并不重要。唯一的要求是,它位于任何需要经过身份验证的用户的中间件(例如授权中间件)之前。

》If you wish, you can move the location of the authentication middleware by calling UseAuthentication() at the appropriate point. I prefer to limit the work done on requests where possible, so I typically take this approach, moving it between the call to UseRouting() and UseAuthorization():
如果需要,可以通过在适当的位置调用 UseAuthentication() 来移动身份验证中间件的位置。我更喜欢尽可能限制对请求所做的工作,因此我通常采用这种方法,在对 UseRouting() 和 UseAuthorization() 的调用之间移动它:

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.Run();

If you don’t place the authentication middleware at the correct point in the pipeline, you can run into strange bugs where users aren’t authenticated correctly or authorization policies aren’t applied correctly. The templates work out of the box, but you need to take care if you’re working with an existing application or moving middleware around.
如果您没有将身份验证中间件放置在管道中的正确位置,则可能会遇到奇怪的错误,即用户未正确进行身份验证或授权策略未正确应用。这些模板开箱即用,但如果您正在使用现有应用程序或移动中间件,则需要小心。

Now that you’ve got an overview of the additions made by Identity, we’ll look in a bit more detail at the database schema and how Identity stores users in the database.
现在,您已经大致了解了 Identity 所做的添加,我们将更详细地了解数据库架构以及 Identity 如何在数据库中存储用户。

23.3.3 The ASP.NET Core Identity data model

23.3.3 ASP.NET Core Identity 数据模型

Out of the box, and in the default templates, Identity uses EF Core to store user accounts. It provides a base DbContext that you can inherit from, called IdentityDbContext, which uses an IdentityUser as the user entity for your application.
在默认模板中,Identity 使用 EF Core 来存储用户帐户。它提供了一个可以从中继承的基本 DbContext,称为 IdentityDbContext,它使用 IdentityUser 作为应用程序的用户实体。

In the template, the app’s DbContext is called ApplicationDbContext. If you open this file, you’ll see it’s sparse; it inherits from the IdentityDbContext base class I described earlier, and that’s it. What does this base class give you? The easiest way to see is to update a database with the migrations and take a look.
在模板中,应用的 DbContext 称为 ApplicationDbContext。如果打开此文件,您将看到它是稀疏的;它继承自我前面介绍的 IdentityDbContext 基类,仅此而已。这个基类为您提供了什么?最简单的方法是使用迁移更新数据库并查看。

Applying the migrations is the same process as in chapter 12. Ensure that the connection string points to where you want to create the database, open a command prompt in your project folder, and run this command to update the database with the migrations:
应用迁移的过程与第 12 章中的过程相同。确保连接字符串指向要创建数据库的位置,在项目文件夹中打开命令提示符,然后运行以下命令以使用迁移更新数据库:

dotnet ef database update

Tip If you see an error after running the dotnet ef command, ensure that you have the .NET tool installed by following the instructions provided in section 12.3.1. Also make sure that you run the command from the project folder, not the solution folder.
提示:如果在运行 dotnet ef 命令后看到错误,请确保按照第 12.3.1 节中提供的说明安装了 .NET 工具。此外,请确保从项目文件夹(而不是解决方案文件夹)运行命令。

If the database doesn’t exist, the command-line interface (CLI) creates it. Figure 23.7 shows what the database looks like for the default template.
如果数据库不存在,则命令行界面 (CLI) 会创建该数据库。图 23.7 显示了默认模板的数据库外观。

Tip If you’re using MS SQL Server (or LocalDB), you can use the SQL Server Object Explorer in Visual Studio to browse tables and objects in your database. See Microsoft’s “How to: Connect to a Database and Browse Existing Objects” article for details: http://mng.bz/mg8r.
提示:如果您使用的是 MS SQL Server(或 LocalDB),则可以使用 Visual Studio 中的 SQL Server 对象资源管理器浏览数据库中的表和对象。有关详细信息,请参阅 Microsoft 的“如何:连接到数据库并浏览现有对象”一文:http://mng.bz/mg8r

alt text

Figure 23.7 The database schema used by ASP.NET Core Identity
图 23.7 ASP.NET Core Identity 使用的数据库架构

That’s a lot of tables! You shouldn’t need to interact with these tables directly (Identity handles that for you), but it doesn’t hurt to have a basic grasp of what they’re for:
好多表啊!您不需要直接与这些表交互(Identity 会为您处理),但对它们的用途有基本的了解并没有什么坏处:

EFMigrationsHistory—The standard EF Core migrations table that records which migrations have been applied.
EFMigrationsHistory - 标准 EF Core 迁移表,用于记录已应用的迁移。

• AspNetUsers—The user profile table itself. This is where IdentityUser is serialized to. We’ll take a closer look at this table shortly.
AspNetUsers — 用户配置文件表本身。这是 IdentityUser 序列化到的位置。我们稍后会仔细看看这个表格。

• AspNetUserClaims—The claims associated with a given user. A user can have many claims, so it’s modeled as a many-to-one relationship.
AspNetUserClaims - 与给定用户关联的声明。一个用户可以有多个声明,因此它被建模为多对一关系。

• AspNetUserLogins and AspNetUserTokens—These are related to third-party logins. When configured, these let users sign in with a Google or Facebook account (for example) instead of creating a password on your app.
AspNetUserLogins 和 AspNetUserTokens - 这些与第三方登录相关。配置后,这些表允许用户使用 Google 或 Facebook 帐户 (例如) 登录,而不是在您的应用程序上创建密码。

• AspNetUserRoles, AspNetRoles, and AspNetRoleClaims—These tables are somewhat of a legacy left over from the old role-based permission model of the pre-.NET 4.5 days, instead of the claims-based permission model. These tables let you define roles that multiple users can belong to. Each role can be assigned multiple claims. These claims are effectively inherited by a user principal when they are assigned that role.
AspNetUserRoles、AspNetRoles 和 AspNetRoleClaims - 这些表在某种程度上是 pre-.NET 4.5 天的基于角色的旧权限模型遗留下来的遗留问题,而不是基于声明的权限模型。这些表允许您定义多个用户可以属于的角色。每个角色都可以分配多个声明。当为用户主体分配该角色时,这些声明将由用户主体有效地继承。

You can explore these tables yourself, but the most interesting of them is the AspNetUsers table, shown in figure 23.8.
您可以自己浏览这些表,但其中最有趣的是 AspNetUsers 表,如图 23.8 所示。

alt text

Figure 23.8 The AspNetUsers table is used to store all the details required to authenticate a user.
图 23.8 AspNetUsers 表用于存储验证用户所需的所有详细信息。

Most of the columns in the AspNetUsers table are security-related—the user’s email, password hash, whether they have confirmed their email, whether they have MFA enabled, and so on. By default, there are no columns for additional information, like the user’s name.
AspNetUsers 表中的大多数列都与安全相关 — 用户的电子邮件、密码哈希、他们是否已确认其电子邮件、他们是否启用了 MFA 等。默认情况下,没有其他信息(如用户名)的列。

NOTE You can see from figure 23.8 that the primary key Id is stored as a string column. By default, Identity uses Guid for the identifier. To customize the data type, see the “Change the primary key type” section of Microsoft’s “Identity model customization in ASP.NET Core” documentation: http://mng.bz/5jdB.
注意:从图 23.8 中可以看出,主键 Id 存储为字符串列。默认情况下,Identity 使用 Guid 作为标识符。要自定义数据类型,请参阅 Microsoft 的“ASP.NET Core 中的身份模型自定义”文档的“更改主键类型”部分:http://mng.bz/5jdB

Any additional properties of the user are stored as claims in the AspNetUserClaims table associated with that user. This lets you add arbitrary additional information without having to change the database schema to accommodate it. Want to store the user’s date of birth? You could add a claim to that user; there’s no need to change the database schema. You’ll see this in action in section 23.6, when you add a Name claim to every new user.
用户的任何其他属性都作为声明存储在与该用户关联的 AspNetUserClaims 表中。这样,您就可以添加任意的附加信息,而不必更改数据库架构来容纳它。想要存储用户的出生日期?您可以向该用户添加声明;无需更改数据库架构。您将在第 23.6 节中看到这一点,当您为每个新用户添加 Name 声明时。

NOTE Adding claims is often the easiest way to extend the default IdentityUser, but you can add properties to the IdentityUser directly. This requires database changes but is nevertheless useful in many situations. You can read how to add custom data using this approach here: http://mng.bz/Xd61.
注意:添加声明通常是扩展默认 IdentityUser 的最简单方法,但您可以直接向 IdentityUser 添加属性。这需要更改数据库,但在许多情况下仍然很有用。您可以在此处阅读如何使用此方法添加自定义数据:http://mng.bz/Xd61

It’s important to understand the difference between the IdentityUser entity (stored in the AspNetUsers table) and the ClaimsPrincipal, which is exposed on HttpContext.User. When a user first logs in, an IdentityUser is loaded from the database. This entity is combined with additional claims for the user from the AspNetUserClaims table to create a ClaimsPrincipal. It’s this ClaimsPrincipal that is used for authentication and is serialized to the authentication cookie, not the IdentityUser.
了解 IdentityUser 实体(存储在 AspNetUsers 表中)和 ClaimsPrincipal(在 HttpContext.User 上公开)之间的区别非常重要。当用户首次登录时,将从数据库中加载 IdentityUser。此实体与 AspNetUserClaims 表中用户的其他声明组合在一起,以创建 ClaimsPrincipal。此 ClaimsPrincipal 用于身份验证,并序列化为身份验证 Cookie,而不是 IdentityUser。

It’s useful to have a mental model of the underlying database schema Identity uses, but in day-to-day work, you shouldn’t have to interact with it directly. That’s what Identity is for, after all! In the next section we’ll look at the other end of the scale: the UI of the app and what you get out of the box with the default UI.
拥有 Identity 使用的基础数据库架构的心智模型很有用,但在日常工作中,您不应该直接与之交互。毕竟,这就是 Identity 的意义所在!在下一节中,我们将了解天平的另一端:应用程序的 UI 以及您使用默认 UI 开箱即用的功能。

23.3.4 Interacting with ASP.NET Core Identity

23.3.4 与 ASP.NET Core Identity 交互

You’ll want to explore the default UI yourself to get a feel for how the pieces fit together, but in this section I’ll highlight what you get out of the box, as well as areas that typically require additional attention right away.
您需要亲自探索默认 UI,以了解各个部分是如何组合在一起的,但在本节中,我将重点介绍您开箱即用的功能,以及通常需要立即额外注意的领域。

The entry point to the default UI is the user registration page of the application, shown in figure 23.9. The register page enables users to sign up to your application by creating a new IdentityUser with an email and a password. After creating an account, users are redirected to a screen indicating that they should confirm their email. No email service is enabled by default, as this is dependent on your configuring an external email service. You can read how to enable email sending in Microsoft’s “Account confirmation and password recovery in ASP.NET Core” documentation at http://mng.bz/6gBo. Once you configure this, users will automatically receive an email with a link to confirm their account.
默认 UI 的入口点是应用程序的用户注册页面,如图 23.9 所示。通过注册页面,用户可以通过使用电子邮件和密码创建新的 IdentityUser 来注册您的应用程序。创建账户后,用户将被重定向到一个屏幕,指示他们应该确认他们的电子邮件。默认情况下,不启用任何电子邮件服务,因为这取决于您配置外部电子邮件服务。您可以在 http://mng.bz/6gBo 的 Microsoft 的“ASP.NET Core 中的帐户确认和密码恢复”文档中阅读如何启用电子邮件发送。配置此项后,用户将自动收到一封电子邮件,其中包含用于确认其帐户的链接。

alt text

Figure 23.9 The registration flow for users using the default Identity UI. Users enter an email and password and are redirected to a “confirm your email” page. This is a placeholder page by default, but if you enable email confirmation, this page will update appropriately.
图 23.9 使用默认 Identity UI 的用户的注册流程。用户输入电子邮件和密码,并被重定向到“确认您的电子邮件”页面。默认情况下,这是一个占位符页面,但如果您启用电子邮件确认,此页面将相应地更新。

By default, user emails must be unique (you can’t have two users with the same email), and the password must meet various length and complexity requirements. You can customize these options and more in the configuration lambda of the call to AddDefaultIdentity() in Program.cs, as shown in the following listing.
默认情况下,用户电子邮件必须是唯一的(您不能让两个用户使用同一电子邮件),并且密码必须满足各种长度和复杂性要求。您可以在 Program.cs 中调用 AddDefaultIdentity() 的配置 lambda 中自定义这些选项以及更多选项,如下面的清单所示。

Listing 23.2 Customizing Identity settings in ConfigureServices in Startup.cs
清单 23.2 在 Startup.cs 的 ConfigureServices 中自定义身份设置

builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
    options.SignIn.RequireConfirmedAccount = true;     #A
    options.Lockout.AllowedForNewUsers = true;    #B
    options.Password.RequiredLength = 12;               #C
    options.Password.RequireNonAlphanumeric = false;    #C
    options.Password.RequireDigit = false;              #C
})
.AddEntityFrameworkStores<AppDbContext>();

❶ Requires users to confirm their account by email before they can log in
要求用户在登录之前通过电子邮件确认其帐户
❷ Enables user lockout, to prevent brute-force attacks against user passwords
启用用户锁定,以防止对用户密码的暴力攻击
❸ Updates password requirements. Current guidance is to require long passwords.
更新密码要求。当前的指导是要求使用长密码。

After a user has registered with your application, they need to log in, as shown in figure 23.10. On the right side of the login page, the default UI templates describe how you, the developer, can configure external login providers, such as Facebook and Google. This is useful information for you, but it’s one of the reasons you may need to customize the default UI templates, as you’ll see in section 23.5.
用户注册到您的应用程序后,他们需要登录,如图 23.10 所示。在登录页面的右侧,默认 UI 模板描述了您(开发人员)如何配置外部登录提供程序,例如 Facebook 和 Google。这对您有用,但这也是您可能需要自定义默认 UI 模板的原因之一,如第 23.5 节所示。

alt text

Figure 23.10 Logging in with an existing user and managing the user account. The Login page describes how to configure external login providers, such as Facebook and Google. The user-management pages allow users to change their email and password and to configure MFA.
图 23.10 使用现有用户登录并管理用户帐户。Login (登录) 页面介绍了如何配置外部登录提供程序,例如 Facebook 和 Google。用户管理页面允许用户更改其电子邮件和密码以及配置 MFA。

Once a user has signed in, they can access the management pages of the identity UI. These allow users to change their email, change their password, configure MFA with an authenticator app, or delete all their personal data. Most of these functions work without any effort on your part, assuming that you’ve already configured an email-sending service.
用户登录后,他们可以访问身份 UI 的管理页面。这些允许用户更改他们的电子邮件、更改他们的密码、使用身份验证器应用程序配置 MFA 或删除他们的所有个人数据。这些函数中的大多数都无需您执行任何作即可工作,前提是您已经配置了电子邮件发送服务。

That covers everything you get in the default UI templates. It may seem somewhat minimal, but it covers a lot of the requirements that are common to almost all apps. Nevertheless, there are a few things you’ll nearly always want to customize:
这涵盖了您在默认 UI 模板中获得的所有内容。它可能看起来有些微不足道,但它涵盖了几乎所有应用程序通用的许多要求。不过,您几乎总是需要自定义一些内容:

• Configure an email-sending service, to enable account confirmation and password recovery, as described in Microsoft’s “Account confirmation and password recovery in ASP.NET Core” documentation: http://mng.bz/vzy7.
配置电子邮件发送服务,以启用帐户确认和密码恢复,如 Microsoft 的“ASP.NET Core 中的帐户确认和密码恢复”文档中所述:http://mng.bz/vzy7

• Add a QR code generator for the enable MFA page, as described in Microsoft’s “Enable QR Code generation for TOTP authenticator apps in ASP.NET Core” documentation: http://mng.bz/4Zmw.
为启用 MFA 页面添加 QR 码生成器,如 Microsoft 的“在 ASP.NET Core 中为 TOTP 验证器应用程序启用 QR 码生成”文档中所述:http://mng.bz/4Zmw

• Customize the register and login pages to remove the documentation link for enabling external services. You’ll see how to do this in section 23.5. Alternatively, you may want to disable user registration entirely, as described in Microsoft’s “Scaffold Identity in ASP.NET Core projects” documentation: http://mng.bz/QmMG.
自定义注册和登录页面,以删除用于启用外部服务的文档链接。您将在 Section 23.5 中看到如何执行此作。或者,您可能希望完全禁用用户注册,如 Microsoft 的“ASP.NET Core 项目中的基架身份”文档中所述:http://mng.bz/QmMG

• Collect additional information about users on the registration page. You’ll see how to do this in section 23.6.
在注册页面上收集有关用户的其他信息。您将在 Section 23.6 中看到如何执行此作。

There are many more ways you can extend or update the Identity system and lots of options available, so I encourage you to explore Microsoft’s “Overview of ASP.NET Core authentication” at http://mng.bz/XdGv to see your options. In the next section you’ll see how to achieve another common requirement: adding users to an existing application.
还有更多方法可以扩展或更新 Identity 系统,并且有很多可用选项,因此我鼓励您在 http://mng.bz/XdGv 上浏览 Microsoft 的“ASP.NET Core 身份验证概述”以查看您的选项。在下一节中,您将了解如何实现另一个常见要求:将用户添加到现有应用程序。

23.4 Adding ASP.NET Core Identity to an existing project

23.4 将 ASP.NET Core Identity 添加到现有工程

In this section we’re going to add users to an existing application. The initial app is a Razor Pages app, based on recipe application from chapter 12. This is a working app that you want to add user functionality to. In chapter 24 we’ll extend this work to restrict control regarding who’s allowed to edit recipes on the app.
在本节中,我们将向现有应用程序添加用户。初始应用是 Razor Pages 应用,基于第 12 章中的配方应用。这是您要向其添加用户功能的工作应用程序。在第 24 章中,我们将扩展这项工作,以限制对谁可以在应用程序上编辑配方的控制。

By the end of this section, you’ll have an application with a registration page, a login screen, and a manage account screen, like the default templates. You’ll also have a persistent widget in the top right of the screen showing the login status of the current user, as shown in figure 23.11.
在本部分结束时,您将拥有一个应用程序,其中包含注册页面、登录屏幕和管理帐户屏幕,就像默认模板一样。屏幕右上角还有一个持久小部件,显示当前用户的登录状态,如图 23.11 所示。

alt text

Figure 23.11 The recipe app after adding authentication, showing the login widget
图 23.11 添加身份验证后的配方应用程序,显示登录小部件

As in section 23.3, I’m not going to customize any of the defaults at this point, so we won’t set up external login providers, email confirmation, or MFA. I’m concerned only with adding ASP.NET Core Identity to an existing app that’s already using EF Core.
与 Section 23.3 一样,我此时不打算自定义任何默认值,因此我们不会设置外部登录提供程序、电子邮件确认或 MFA。我只关心将 ASP.NET Core Identity 添加到已在使用 EF Core 的现有应用程序。

Tip It’s worth making sure you’re comfortable with the new project templates before you go about adding Identity to an existing project. Create a test app, and consider setting up an external login provider, configuring an email provider, and enabling MFA. This will take a bit of time, but it’ll be invaluable for deciphering errors when you come to adding Identity to existing apps.
提示在将 Identity 添加到现有项目之前,值得确保您熟悉新的项目模板。创建测试应用程序,并考虑设置外部登录提供程序、配置电子邮件提供程序并启用 MFA。这将花费一些时间,但在您将 Identity 添加到现有应用程序时,它对于破译错误非常宝贵。

To add Identity to your app, you’ll need to do the following:
要将 Identity 添加到您的应用程序,您需要执行以下作:

  1. Add the ASP.NET Core Identity NuGet packages.
    添加 ASP.NET Core Identity NuGet 包。
  2. Add the required Identity services to the dependency injection (DI) container.
    将所需的身份服务添加到依赖关系注入 (DI) 容器中。
  3. Update the EF Core data model with the Identity entities.
    使用 Identity 实体更新 EF Core 数据模型。
  4. Update your Razor Pages and layouts to provide links to the Identity UI.
    更新 Razor 页面和布局,以提供指向标识 UI 的链接。

This section tackles each of these steps in turn. At the end of section 23.4 you’ll have successfully added user accounts to the recipe app.
本节将依次介绍这些步骤中的每一个。在第 23.4 节结束时,您将成功地将用户帐户添加到配方应用程序。

23.4.1 Configuring the ASP.NET Core Identity services

23.4.1 配置 ASP.NET Core Identity 服务
You can add ASP.NET Core Identity with the default UI to an existing app by referencing two NuGet packages:
您可以通过引用两个 NuGet 包,将带有默认 UI 的 ASP.NET Core Identity 添加到现有应用程序:

• Microsoft.AspNetCore.Identity.EntityFrameworkCore—Provides all the core Identity services and integration with EF Core
Microsoft.AspNetCore.Identity.EntityFrameworkCore - 提供所有核心身份服务以及与 EF Core的集成
• Microsoft.AspNetCore.Identity.UI—Provides the default UI Razor Pages
Microsoft.AspNetCore.Identity.UI - 提供默认 UI Razor 页面

Update your project .csproj file to include these two packages:
更新项目 .csproj 文件以包含以下两个包:

<PackageReference
    Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore"
    Version="7.0.0" />
<PackageReference
    Include="Microsoft.AspNetCore.Identity.UI" Version="7.0.0" />

These packages bring in all the additional required dependencies you need to add Identity with the default UI. Be sure to run dotnet restore after adding them to your project.
这些包引入了使用默认 UI 添加 Identity 所需的所有额外必需依赖项。请务必在将它们添加到项目后运行 dotnet restore。

Once you’ve added the Identity packages, you can update your Program.cs file to include the Identity services, as shown in the following listing. This is similar to the default template setup you saw in listing 23.1, but make sure to reference your existing AppDbContext.
添加 Identity 包后,您可以更新 Program.cs 文件以包含 Identity 服务,如以下清单所示。这类似于您在清单 23.1 中看到的默认模板设置,但请确保引用您现有的 AppDbContext。

Listing 23.3 Adding ASP.NET Core Identity services to the recipe app
清单 23.3 将 ASP.NET Core Identity 服务添加到 recipe 应用程序

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>   #A
    options.UseSqlite(builder.Configuration    #A
        .GetConnectionString("DefaultConnection")!));   #A

builder.Services.AddDefaultIdentity<ApplicationUser>(options =>      #B
        options.SignIn.RequireConfirmedAccount = true)       #B
    .AddEntityFrameworkStores<AppDbContext>();     #C

builder.Services.AddRazorPages();
builder.Services.AddScoped<RecipeService>();

❶ The existing service configuration is unchanged.
现有服务配置保持不变。
❷ Adds the Identity services to the DI container and uses a custom user type, ApplicationUser
将身份服务添加到 DI 容器并使用自定义用户类型 ApplicationUser
❸ Makes sure you use the name of your existing DbContext app
确保您使用现有 DbContext 应用程序的名称

This adds all the necessary services and configures Identity to use EF Core. I’ve introduced a new type here, ApplicationUser, which we’ll use to customize our user entity later. You’ll see how to add this type in section 23.4.2.
这将添加所有必要的服务,并将 Identity 配置为使用 EF Core。我在这里引入了一个新类型 ApplicationUser,我们稍后将使用它来自定义我们的用户实体。您将在 Section 23.4.2 中看到如何添加此类型。

The next step is optional: add the AuthenticationMiddleware after the call to UseRouting() on WebApplication, as shown in the following listing. As I mentioned previously, the authentication middleware is added automatically by WebApplication, so this step is optional. I prefer to delay authentication until after the call to UseRouting(), as it eliminates the need to perform unnecessary work decrypting the authentication cookie for requests that don’t reach the routing middleware, such as requests for static files.
下一步是可选的:在调用 WebApplication 上的 UseRouting() 之后添加 AuthenticationMiddleware,如下面的清单所示。正如我前面提到的,身份验证中间件是由 WebApplication 自动添加的,因此此步骤是可选的。我更喜欢将身份验证延迟到调用 UseRouting() 之后,因为这样就无需为未到达路由中间件的请求(例如静态文件请求)执行不必要的身份验证 Cookie 解密工作。

Listing 23.4 Adding AuthenticationMiddleware to the recipe app
列表 23.4 将 AuthenticationMiddleware 添加到 recipe 应用程序

app.UseStaticFiles();            #A

app.UseRouting();

app.UseAuthentication();        #B
app.UseAuthorization();          #C

app.MapRazorPages();
app.Run

❶ StaticFileMiddleware will never see requests as authenticated, even after you sign in.
StaticFileMiddleware 永远不会将请求视为已验证,即使在你登录后也是如此。
❷ Adds AuthenticationMiddleware after UseRouting() and before UseAuthorization
在 UseRouting() 之后和 UseAuthorization之前添加 AuthenticationMiddleware
❸ Middleware after AuthenticationMiddleware can read the user principal from HttpContext.User.
AuthenticationMiddleware 之后的中间件可以从 HttpContext.User 读取用户主体。

You’ve configured your app to use Identity, so the next step is updating EF Core’s data model. You’re already using EF Core in this app, so you need to update your database schema to include the tables that Identity requires.
你已将应用配置为使用 Identity,因此下一步是更新 EF Core 的数据模型。你已在此应用中使用 EF Core,因此需要更新数据库架构以包含 Identity 所需的表。

23.4.2 Updating the EF Core data model to support Identity

23.4.2 更新 EF Core 数据模型以支持身份

The code in listing 23.3 won’t compile, as it references the ApplicationUser type, which doesn’t yet exist. Create the ApplicationUser in the Data folder, using the following line:
清单 23.3 中的代码无法编译,因为它引用了尚不存在的 ApplicationUser 类型。使用以下行在 Data 文件夹中创建 ApplicationUser:

public class ApplicationUser : IdentityUser { }

It’s not strictly necessary to create a custom user type in this case (for example, the default templates use the raw IdentityUser), but I find it’s easier to add the derived type now rather than try to retrofit it later if you need to add extra properties to your user type.
在这种情况下,并非绝对需要创建自定义用户类型(例如,默认模板使用原始 IdentityUser),但我发现,如果您需要向用户类型添加额外的属性,现在添加派生类型比以后尝试修改它更容易。

In section 23.3.3 you saw that Identity provides a DbContext called IdentityDbContext, which you can inherit from. The IdentityDbContext base class includes the necessary DbSet to store your user entities using EF Core.
在第 23.3.3 节中,您看到 Identity 提供了一个名为 IdentityDbContext 的 DbContext,您可以从中继承。IdentityDbContext 基类包括使用 EF Core 存储用户实体所需的 DbSet。

Updating an existing DbContext for Identity is simple: update your app’s DbContext to inherit from IdentityDbContext (which itself inherits from DbContext), as shown in the following listing. We’re using the generic version of the base Identity context in this case and providing the ApplicationUser type.
更新现有的 DbContext for Identity 很简单:更新应用程序的 DbContext 以从 IdentityDbContext(它本身继承自 DbContext)继承,如下面的清单所示。在本例中,我们使用基本 Identity 上下文的通用版本,并提供 ApplicationUser 类型。

Listing 23.5 Updating AppDbContext to use IdentityDbContext
列表 23.5 更新 AppDbContext 以使用 IdentityDbContext

public class AppDbContext : IdentityDbContext<ApplicationUser>    #A
{
    public AppDbContext(DbContextOptions<AppDbContext> options)  #B
        : base(options)                                          #B
    { }                                                          #B

    public DbSet<Recipe> Recipes { get; set; }                   #B
}

❶ Updates to inherit from the Identity context instead of directly from DbContext
更新以从 Identity 上下文继承,而不是直接从 DbContext继承
❷ The remainder of the class remains the same.
类的其余部分保持不变。

Effectively, by updating the base class of your context in this way, you’ve added a whole load of new entities to EF Core’s data model. As you saw in chapter 12, whenever EF Core’s data model changes, you need to create a new migration and apply those changes to the database.
实际上,通过以这种方式更新上下文的基类,你已向 EF Core 的数据模型添加了大量新实体。如第 12 章所示,每当 EF Core 的数据模型发生更改时,都需要创建新的迁移并将这些更改应用于数据库。

At this point, your app should compile, so you can add a new migration called AddIdentitySchema using
此时,你的应用应该会编译,因此你可以使用 AddIdentitySchema 添加名为 AddIdentitySchema 的新迁移

dotnet ef migrations add AddIdentitySchema

The final step is updating your application’s Razor Pages and layouts to reference the default identity UI. Normally, adding 30 new Razor Pages to your application would be a lot of work, but using the default Identity UI makes it a breeze.
最后一步是更新应用程序的 Razor Pages 和布局以引用默认标识 UI。通常,向应用程序添加 30 个新的 Razor 页面会是一项艰巨的工作,但使用默认标识 UI 会变得轻而易举。

23.4.3 Updating the Razor views to link to the Identity UI

23.4.3 更新 Razor 视图以链接到身份 UI

Technically, you don’t have to update your Razor Pages to reference the pages included in the default UI, but you probably want to add the login widget to your app’s layout at a minimum. You’ll also want to make sure that your Identity Razor Pages use the same base Layout.cshtml as the rest of your application.
从技术上讲,您不必更新 Razor Pages 来引用默认 UI 中包含的页面,但您可能希望至少将登录小组件添加到应用程序的布局中。还需要确保 Identity Razor 页面使用与应用程序其余部分相同的基 Layout.cshtml。

We’ll start by fixing the layout for your Identity pages. Create a file at the “magic” path Areas/Identity/Pages/_ViewStart.cshtml, and add the following contents:
首先,我们将修复 Identity 页面的布局。在“magic”路径 Areas/Identity/Pages/_ViewStart.cshtml 处创建一个文件,并添加以下内容:

@{ Layout = "/Pages/Shared/_Layout.cshtml"; }

This sets the default layout for your Identity pages to your application’s default layout. Next, add a _LoginPartial.cshtml file in Pages/Shared to define the login widget, as shown in the following listing. This is pretty much identical to the template generated by the default template, but it uses our custom ApplicationUser instead of the default IdentityUser.
这会将 Identity 页面的默认布局设置为应用程序的默认布局。接下来,在 Pages/Shared 中添加 _LoginPartial.cshtml 文件以定义登录小组件,如下面的清单所示。这与默认模板生成的模板几乎相同,但它使用我们的自定义 ApplicationUser 而不是默认的 IdentityUser。

Listing 23.6 Adding a _LoginPartial.cshtml to an existing app
列表 23.6 将 _LoginPartial.cshtml 添加到现有应用程序

@using Microsoft.AspNetCore.Identity
@using RecipeApplication.Data;                 #A
@inject SignInManager<ApplicationUser> SignInManager    #B
@inject UserManager<ApplicationUser> UserManager        #B

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
  <li class="nav-item">
    <a  class="nav-link text-dark" asp-area="Identity"
    asp-page="/Account/Manage/Index" title="Manage">
        Hello @User.Identity.Name!</a>
  </li>
    <li class="nav-item">
      <form class="form-inline" asp-page="/Account/Logout"
      asp-route-returnUrl="@Url.Page("/", new { area = "" })"
      asp-area="Identity" method="post" >
        <button  class="nav-link btn btn-link text-dark"
          type="submit">Logout</button>
        </form>
    </li>
}
else
{
  <li class="nav-item">
    <a class="nav-link text-dark" asp-area="Identity"
      asp-page="/Account/Register">Register</a>
  </li>
  <li class="nav-item">
    <a class="nav-link text-dark" asp-area="Identity"
      asp-page="/Account/Login">Login</a>
  </li>
}
</ul>

❶ Updates to your project’s namespace that contains ApplicationUser
更新包含 ApplicationUser的项目命名空间
❷ The default template uses IdentityUser. Update to use ApplicationUser instead.
默认模板使用 IdentityUser。更新以改用 ApplicationUser。

This partial shows the current login status of the user and provides links to register or sign in. All that remains is to render the partial by calling
此部分显示用户的当前登录状态,并提供用于注册或登录的链接。剩下的就是通过调用

<partial name="_LoginPartial" />

in the main layout file of your app, _Layout.cshtml.
在应用程序的主布局文件中,_Layout.cshtml。

And there you have it: you’ve added Identity to an existing application. The default UI makes doing this relatively simple, and you can be sure you haven’t introduced any security holes by building your own UI!
好了:您已将 Identity 添加到现有应用程序。默认 UI 使此作相对简单,您可以确保通过构建自己的 UI 没有引入任何安全漏洞!

As I described in section 23.3.4, there are some features that the default UI doesn’t provide and you need to implement yourself, such as email confirmation and MFA QR code generation. It’s also common to find that you want to update a single page here and there. In the next section I’ll show how you can replace a page in the default UI, without having to rebuild the entire UI yourself.
正如我在第 23.3.4 节中所描述的,默认 UI 不提供一些功能,您需要自行实现,例如电子邮件确认和 MFA QR 码生成。你也经常会发现你想在这里和那里更新单个页面。在下一节中,我将展示如何替换默认 UI 中的页面,而不必自己重新构建整个 UI。

23.5 Customizing a page in ASP.NET Core Identity’s default UI

23.5 在 ASP.NET Core Identity 的默认 UI 中自定义页面

In this section you’ll learn how to use scaffolding to replace individual pages in the default Identity UI. You’ll learn to scaffold a page so that it overrides the default UI, allowing you to customize both the Razor template and the PageModel page handlers.
在本节中,您将学习如何使用基架替换默认 Identity UI 中的各个页面。您将学习如何搭建页面基架,使其覆盖默认 UI,从而允许您自定义 Razor 模板和 PageModel 页面处理程序。

Having Identity provide the whole UI for your application is great in theory, but in practice there are a few wrinkles, as you saw in section 23.3.4. The default UI provides as much as it can, but there are some things you may want to tweak. For example, both the login and register pages describe how to configure external login providers for your ASP.NET Core applications, as you saw in figures 23.12 and 23.13. That’s useful information for you as a developer, but it’s not something you want to be showing to your users. Another often-cited requirement is the desire to change the look and feel of one or more pages.
让 Identity 为您的应用程序提供整个 UI 在理论上很好,但在实践中存在一些问题,正如您在第 23.3.4 节中看到的那样。默认 UI 提供了尽可能多的功能,但您可能需要调整一些内容。例如,登录和注册页面都描述了如何为 ASP.NET Core 应用程序配置外部登录提供程序,如图 23.12 和 23.13 所示。这对开发人员来说很有用,但不是您希望向用户展示的信息。另一个经常被引用的要求是希望更改一个或多个页面的外观。

Luckily, the default Identity UI is designed to be incrementally replaceable, so you can override a single page without having to rebuild the entire UI yourself. On top of that, both Visual Studio and the .NET CLI have functions that allow you to scaffold any (or all) of the pages in the default UI so that you don’t have to start from scratch when you want to tweak a page.
幸运的是,默认的 Identity UI 设计为可增量替换,因此您可以覆盖单个页面,而无需自己重新构建整个 UI。最重要的是,Visual Studio 和 .NET CLI 都具有允许您在默认 UI 中搭建任何(或所有)页面的基架的功能,这样当您想要调整页面时,就不必从头开始。

DEFINITION Scaffolding is the process of generating files in your project that serve as the basis for customization. The Identity scaffolder adds Razor Pages in the correct locations so they override equivalent pages with the default UI. Initially, the code in the scaffolded pages matches that in the default Identity UI, but you are free to customize it.
定义:基架是在项目中生成文件作为自定义基础的过程。Identity 基架将 Razor Pages 添加到正确的位置,以便它们使用默认 UI 覆盖等效页面。最初,基架页面中的代码与默认 Identity UI 中的代码匹配,但您可以自由自定义它。

As an example of the changes you can easily make, we’ll scaffold the registration page and remove the additional information section about external providers. The following steps describe how to scaffold the Register.cshtml page in Visual Studio:
作为您可以轻松进行的更改的示例,我们将搭建注册页面并删除有关外部提供商的其他信息部分。以下步骤介绍如何在 Visual Studio 中搭建 Register.cshtml 页面的基架:

  1. Add the Microsoft.VisualStudio.Web.CodeGeneration.Design and Microsoft .EntityFrameworkCore.Tools NuGet packages to your project file, if they’re not already added. Visual Studio uses these packages to scaffold your application correctly, and without them you may get an error running the scaffolder:
    添加 Microsoft.VisualStudio.Web.CodeGeneration.Design 和 Microsoft 。EntityFrameworkCore.Tools NuGet 包添加到项目文件中(如果尚未添加)。Visual Studio 使用这些包来正确搭建应用程序基架,如果没有它们,运行 Scaffolder 时可能会遇到错误:
<PackageReference Version="7.0.0"
    Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" />
<PackageReference Version="7.0.0"
    Include="Microsoft.EntityFrameworkCore.Tools" />
  1. Ensure that your project builds. If it doesn’t build, the scaffolder will fail before adding your new pages.
    确保您的项目构建成功。如果它没有构建,则 scaffolder 将在添加新页面之前失败。

  2. Right-click your project, and choose Add > New Scaffolded Item from the contextual menu.
    右键单击您的项目,然后从上下文菜单中选择 Add > New Scaffolded Item (添加新基架项)。

  3. In the selection dialog box, choose Identity from the category, and choose Add.
    在选择对话框中,从类别中选择 Identity (身份),然后选择 Add (添加)。

  4. In the Add Identity dialog box, select the Account/Register page, and select your application’s AppDbContext as the Data context class, as shown in figure 23.12. Choose Add to scaffold the page.
    在 Add Identity 对话框中,选择 Account/Register 页面,然后选择应用程序的 AppDbContext 作为 Data 上下文类,如图 23.12 所示。选择 Add (添加) 以搭建页面基架。

alt text

Figure 23.12 Using Visual Studio to scaffold Identity pages. The generated Razor Pages will override the versions provided by the default UI.
图 23.12 使用 Visual Studio 搭建 Identity 页面的基架。生成的 Razor Pages 将替代默认 UI 提供的版本。

Tip To scaffold the registration page using the .NET CLI, install the required tools and packages as described in Microsoft’s “Scaffold Identity in ASP.NET Core projects” documentation: http://mng.bz/QPRv. Then run dotnet aspnet-codegenerator identity -dc RecipeApplication.Data.AppDbContext --files "Account.Register".
提示:要使用 .NET CLI 搭建注册页面的基架,请按照 Microsoft 的“ASP.NET Core 项目中的基架标识”文档中所述安装所需的工具和包:http://mng.bz/QPRv。然后运行 dotnet aspnet-codegenerator identity -dc RecipeApplication.Data.AppDbContext --files “Account.Register”。

Visual Studio builds your application and then generates the Register.cshtml page for you, placing it in the Areas/Identity/Pages/Account folder. It also generates several supporting files, as shown in figure 23.13. These are required mostly to ensure that your new Register.cshtml page can reference the remaining pages in the default Identity UI.
Visual Studio 生成应用程序,然后生成 Register.cshtml 页面,将其放置在 Areas/Identity/Pages/Account 文件夹中。它还会生成几个支持文件,如图 23.13 所示。这些主要是为了确保新的 Register.cshtml 页面可以引用默认标识 UI 中的其余页面。

alt text

Figure 23.13 The scaffolder generates the Register.cshtml Razor Page, along with supporting files required to integrate with the remainder of the default Identity UI.
图 23.13 基架生成 Register.cshtml Razor 页面,以及与默认标识 UI 的其余部分集成所需的支持文件。

We’re interested in the Register.cshtml page, as we want to customize the UI on the Register page, but if we look inside the code-behind page, Register.cshtml.cs, we see how much complexity the default Identity UI is hiding from us. It’s not insurmountable (we’ll customize the page handler in section 23.6), but it’s always good to avoid writing code if we can help it.
我们对 Register.cshtml 页面感兴趣,因为我们希望自定义 Register 页面上的 UI,但如果我们查看代码隐藏页面 Register.cshtml.cs,我们会看到默认身份 UI 对我们隐藏了多少复杂性。这并不是不可克服的(我们将在 Section 23.6 中自定义页面处理程序),但如果我们可以提供帮助,避免编写代码总是好的。

Now that you have the Razor template in your application, you can customize it to your heart’s content. The downside is that you’re now maintaining more code than you were with the default UI. You didn’t have to write it, but you may still have to update it when a new version of ASP.NET Core is released.
现在,您的应用程序中已有 Razor 模板,您可以根据自己的喜好对其进行自定义。缺点是,您现在维护的代码比使用默认 UI 时要多。您不必编写它,但在 ASP.NET Core 的新版本发布时,您可能仍需要更新它。

I like to use a bit of a trick when it comes to overriding the default Identity UI like this. In many cases, you don’t want to change the page handlers for the Razor Page—only the Razor view. You can achieve this by deleting the Register.cshtml.cs PageModel file, and pointing your newly scaffolded .cshtml file at the original PageModel, which is part of the default UI NuGet package.
在覆盖像这样的默认身份 UI 时,我喜欢使用一些技巧。在许多情况下,您不希望更改 Razor 页面的页面处理程序,而只需要更改 Razor 视图。为此,您可以删除Register.cshtml.cs PageModel 文件,并将新搭建的 .cshtml 文件指向原始 PageModel,该 PageModel 是默认 UI NuGet 包的一部分。

The other benefit of this approach is that you can delete some of the other files that were autoscaffolded. In total, you can make the following changes:
此方法的另一个好处是,您可以删除一些自动基架的其他文件。总的来说,您可以进行以下更改:

• Update the @model directive in Register.cshtml to point to the default UI PageModel:
更新 Register.cshtml 中的 @model 指令以指向默认 UI PageModel:

@model Microsoft.AspNetCore.Identity.UI.V5.Pages.Account.Internal.RegisterModel

• Update Areas/Identity/Pages/_ViewImports.cshtml to the following:
将 Areas/Identity/Pages/_ViewImports.cshtml 更新为以下内容:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

• Delete Areas/Identity/Pages/_ValidationScriptsPartial.cshtml.
• Delete Areas/Identity/Pages/Account/Register.cshtml.cs.
• Delete Areas/Identity/Pages/Account/_ViewImports.cshtml.

After making all these changes, you’ll have the best of both worlds: you can update the default UI Razor Pages HTML without taking on the responsibility of maintaining the default UI code-behind.
进行所有这些更改后,你将获得两全其美的效果:可以更新默认 UI Razor Pages HTML,而无需负责维护默认 UI 代码隐藏。

Tip In the source code for the book, you can see these changes in action, where the Register view has been customized to remove the references to external identity providers.
提示:在本书的源代码中,您可以看到这些实际更改,其中 Register 视图已自定义以删除对外部身份提供者的引用。

Unfortunately, it’s not always possible to use the default UI PageModel. Sometimes you need to update the page handlers, such as when you want to change the functionality of your Identity area rather than only the look and feel. A common requirement is needing to store additional information about a user, as you’ll see in the next section.
遗憾的是,并非总是可以使用默认的 UI PageModel。有时,您需要更新页面处理程序,例如,当您想要更改 Identity 区域的功能,而不仅仅是外观时。一个常见的要求是需要存储有关用户的其他信息,您将在下一节中看到。

23.6 Managing users: Adding custom data to users

23.6 管理用户:向用户添加自定义数据

In this section you’ll see how to customize the ClaimsPrincipal assigned to your users by adding claims to the AspNetUserClaims table when the user is created. You’ll also see how to access these claims in your Razor Pages and templates.
在本节中,您将了解如何通过在创建用户时向 AspNetUserClaims 表添加声明来自定义分配给用户的 ClaimsPrincipal。您还将了解如何在 Razor 页面和模板中访问这些声明。

Often, the next step after adding Identity to an application is customizing it. The default templates require only an email and password to register. What if you need more details, like a friendly name for the user? Also, I’ve mentioned that we use claims for security, so what if you want to add a claim called IsAdmin to certain users?
通常,将 Identity 添加到应用程序后的下一步是对其进行自定义。默认模板只需要电子邮件和密码即可注册。如果您需要更多详细信息,例如用户的友好名称,该怎么办?此外,我还提到过我们使用声明来实现安全性,那么,如果您想向某些用户添加一个名为 IsAdmin 的声明,该怎么办?

You know that every user principal has a collection of claims, so conceptually, adding any claim requires adding it to the user’s collection. There are two main times that you would want to grant a claim to a user:
您知道每个用户主体都有一个声明集合,因此从概念上讲,添加任何声明都需要将其添加到用户的集合中。您希望向用户授予声明的主要时间有两个:

• For every user, when they register on the app—For example, you might want to add a Name field to the Register form and add that as a claim to the user when they register.
对于每个用户,当他们在应用程序上注册时 - 例如,您可能希望将“名称”字段添加到“注册”表单中,并在用户注册时将其作为声明添加到用户。
• Manually, after the user has registered—This is common for claims used as permissions, where an existing user might want to add an IsAdmin claim to a specific user after they have registered on the app.
在用户注册后手动 - 这在用作权限的声明中很常见,其中现有用户可能希望在特定用户注册应用程序后向特定用户添加 IsAdmin 声明。

In this section I’ll show you the first approach, automatically adding new claims to a user when they’re created. The latter approach is more flexible and ultimately is the approach many apps will need, especially line-of-business apps. Luckily, there’s nothing conceptually difficult to it; it requires a simple UI that lets you view users and add a claim through the same mechanism I’ll show here.
在本节中,我将向您展示第一种方法,即在创建用户时自动向用户添加新声明。后一种方法更灵活,最终是许多应用程序需要的方法,尤其是业务线应用程序。幸运的是,它在概念上没有什么困难;它需要一个简单的 UI,允许您通过我将在此处展示的相同机制查看用户并添加声明。

Tip Another common approach is to customize the IdentityUser entity, by adding a Name property, for example. This approach is sometimes easier to work with if you want to give users the ability to edit that property. Microsoft’s “Add, download, and delete custom user data to Identity in an ASP.NET Core project” documentation describes the steps required to achieve that: http://mng.bz/aoe7.
提示:另一种常见方法是自定义 IdentityUser 实体,例如,通过添加 Name 属性。如果您想让用户能够编辑该属性,这种方法有时更容易使用。Microsoft 的“在 ASP.NET Core 项目中向 Identity 添加、下载和删除自定义用户数据”文档介绍了实现此目的所需的步骤:http://mng.bz/aoe7

Let’s say you want to add a new Claim to a user, called FullName. A typical approach would be as follows:
假设您要向名为 FullName 的用户添加新声明。典型的方法如下:

  1. Scaffold the Register.cshtml Razor Page, as you did in section 23.5.
    搭建 Register.cshtml Razor 页面的基架,就像在第 23.5 节中所做的那样。

  2. Add a Name field to the InputModel in the Register.cshtml.cs PageModel.
    将 Name 字段添加到 Register.cshtml.cs PageModel 中的 InputModel。

  3. Add a Name input field to the Register.cshtml Razor view template.
    将 Name 输入字段添加到 Register.cshtml Razor 视图模板。

  4. Create the new ApplicationUser entity as before in the OnPost() page handler by calling CreateAsync on UserManager<ApplicationUser>.
    像以前一样,通过在 UserManager 上调用 CreateAsync 在 OnPost() 页面处理程序中创建新的 UserManager<ApplicationUser>实体。

  5. Add a new Claim to the user by calling UserManager.AddClaimAsync().
    通过调用 UserManager.AddClaimAsync() 向用户添加新的声明。

  6. Continue the method as before, sending a confirmation email or signing the user in if email confirmation is not required.
    像以前一样继续该方法,发送确认电子邮件,如果不需要电子邮件确认,则让用户登录。

Steps 1–3 are fairly self-explanatory and require only updating the existing templates with the new field. Steps 4–6 take place in Register.cshtml.cs in the OnPostAsync() page handler, which is summarized in the following listing. In practice, the page handler has more error checking, boilerplate, extra features, and abstraction. I’ve simplified the code in listing 23.7 to focus on the additional lines that add the extra Claim to the ApplicationUser; you can find the full code in the sample code for this chapter.
步骤 1-3 相当不言自明,只需要使用新字段更新现有模板。步骤 4-6 在 OnPostAsync() 页面处理程序中Register.cshtml.cs进行,下面的清单对此进行了总结。在实践中,页面处理程序具有更多的错误检查、样板、额外功能和抽象。我简化了清单 23.7 中的代码,以专注于向 ApplicationUser 添加额外 Claim 的其他行;您可以在本章的示例代码中找到完整代码。

Listing 23.7 Adding a custom claim to a new user in the Register.cshtml.cs page
清单 23.7 在 Register.cshtml.cs 页面中为新用户添加自定义声明

public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser {                    #A
            UserName = Input.Email, Email = Input.Email };  #A
        var result = await _userManager.CreateAsync(      #B
            user, Input.Password);                        #B
        if (result.Succeeded)
        {
            var claim = new Claim("FullName", Input.Name);   #C
            await _userManager.AddClaimAsync(user, claim);   #D
            var code = await _userManager                      #E
                .GenerateEmailConfirmationTokenAsync(user);    #E
            await _emailSender.SendEmailAsync(                 #E
                 Input.Email, "Confirm your email", code );    #E
            await _signInManager.SignInAsync(user);   #F
            return LocalRedirect(returnUrl);
        }
        foreach (var error in result.Errors)         #G
        {                                            #G
            ModelState.AddModelError(                #G
                string.Empty, error.Description);    #G
        }                                            #G
    }
       return Page();                                #G
}

❶ Creates an instance of the ApplicationUser entity
创建 ApplicationUser 实体的实例
❷ Validates that the provided password meets requirements, and creates the user in the database
验证提供的密码是否满足要求,并在数据库中创建用户
❸ Creates a claim, with a string name of “FullName” and the provided value
创建字符串名称为“FullName”且提供的值的声明
❹ Adds the new claim to the ApplicationUser’s collection
将新声明添加到 ApplicationUser 的集合
❺ Sends a confirmation email to the user, if you have configured the email sender
向用户发送确认电子邮件(如果您已配置电子邮件发件人)
❻ Signs the user in by setting the HttpContext.User; the principal will include the custom claim
通过设置 HttpContext.User 来登录用户;主体将包含自定义声明
❼ There was a problem creating the user. Adds the errors to the ModelState and redisplays the page.
创建用户时出现问题。将错误添加到 ModelState 并重新显示页面。

Tip Listing 23.7 shows how you can add extra claims at registration time, but you will often need to add more data later, such as permission-related claims or other information. You will need to create additional endpoints and pages for adding this data, securing the pages as appropriate (so that users can’t update their own permissions, for example).
提示:清单 23.7 展示了如何在注册时添加额外的声明,但您通常需要稍后添加更多数据,例如与权限相关的声明或其他信息。您需要创建其他终端节点和页面来添加此数据,并根据需要保护页面(例如,使用户无法更新自己的权限)。

This is all that’s required to add the new claim, but you’re not using it anywhere currently. What if you want to display it? Well, you’ve added a claim to the ClaimsPrincipal, which was assigned to the HttpContext.User property when you called SignInAsync. That means you can retrieve the claims anywhere you have access to the ClaimsPrincipal—including in your page handlers and in view templates. For example, you could display the user’s FullName claim anywhere in a Razor template with the following statement:
这就是添加新声明所需的全部内容,但您目前没有在任何地方使用它。如果要显示它怎么办?嗯,您已经向 ClaimsPrincipal 添加了一个声明,该声明在调用 SignInAsync 时分配给 HttpContext.User 属性。这意味着您可以在任何有权访问 ClaimsPrincipal 的位置检索声明,包括在页面处理程序和视图模板中。例如,您可以使用以下语句在 Razor 模板中的任意位置显示用户的 FullName 声明:

@User.Claims.FirstOrDefault(x=>x.Type == "FullName")?.Value

This finds the first claim on the current user principal with a Type of "FullName" and prints the assigned value (or, if the claim is not found, prints nothing). The Identity system even includes a handy extension method that tidies up this LINQ expression (found in the System.Security.Claims namespace):
这将在当前用户主体上查找 Type 为 “FullName” 的第一个声明,并打印分配的值(或者,如果未找到声明,则不打印任何内容)。Identity 系统甚至包括一个方便的扩展方法,用于整理此 LINQ 表达式(位于 System.Security.Claims 命名空间中):

@User.FindFirstValue("FullName")

With that last tidbit, we’ve reached the end of this chapter on ASP.NET Core Identity. I hope you’ve come to appreciate the amount of effort using Identity can save you, especially when you make use of the default Identity UI package.
说到最后的花絮,我们关于 ASP.NET Core Identity 的本章已经结束了。我希望您已经意识到使用 Identity 可以节省大量精力,尤其是在使用默认的 Identity UI 包时。

Adding user accounts and authentication to an app is typically the first step in customizing your app further. Once you have authentication, you can have authorization, which lets you lock down certain actions in your app, based on the current user. In the next chapter you’ll learn about the ASP.NET Core authorization system and how you can use it to customize your apps; in particular, the recipe application, which is coming along nicely!
向应用程序添加用户帐户和身份验证通常是进一步自定义应用程序的第一步。获得身份验证后,您可以获得授权,从而允许您根据当前用户锁定应用程序中的某些作。在下一章中,您将了解 ASP.NET Core 授权系统以及如何使用它来自定义您的应用程序;特别是 recipe 应用程序,它进展顺利!

23.7 Summary

23.7 总结

Authentication is the process of determining who you are, and authorization is the process of determining what you’re allowed to do. You need to authenticate users before you can apply authorization.
身份验证是确定您是谁的过程,授权是确定允许您执行哪些作的过程。您需要先对用户进行身份验证,然后才能应用授权。

Every request in ASP.NET Core is associated with a user, also known as a principal. By default, without authentication, this is an anonymous user. You can use the claims principal to behave differently depending on who made a request.
ASP.NET Core 中的每个请求都与一个用户(也称为委托人)相关联。默认情况下,如果不进行身份验证,则此用户为匿名用户。您可以使用 claims principal 根据发出请求的人员来执行不同的行为。

The current principal for a request is exposed on HttpContext.User. You can access this value from your Razor Pages and views to find out properties of the user such as their, ID, name, or email.
请求的当前主体在 HttpContext.User 上公开。可以从 Razor 页面和视图访问此值,以查找用户的属性,例如他们的 ID、名称或电子邮件。

Every user has a collection of claims. These claims are single pieces of information about the user. Claims could be properties of the physical user, such as Name and Email, or they could be related to things the user has, such as HasAdminAccess or IsVipCustomer.
每个用户都有一个声明集合。这些声明是有关用户的单个信息。声明可以是物理用户的属性,例如 Name 和 Email,也可以与用户拥有的内容相关,例如 HasAdminAccess 或 IsVipCustomer。

Legacy versions of ASP.NET used roles instead of claims. You can still use roles if you need to, but you should typically use claims where possible.
旧版本的 ASP.NET 使用角色而不是声明。如果需要,您仍然可以使用角色,但通常应尽可能使用声明。

Authentication in ASP.NET Core is provided by AuthenticationMiddleware and a number of authentication services. These services are responsible for setting the current principal when a user logs in, saving it to a cookie, and loading the principal from the cookie on subsequent requests.
ASP.NET Core 中的身份验证由 AuthenticationMiddleware 和许多身份验证服务提供。这些服务负责在用户登录时设置当前主体,将其保存到 Cookie,并在后续请求中从 Cookie 加载主体。

The AuthenticationMiddleware is added automatically by WebApplication. You can ensure that it’s inserted at a specific point in the middleware pipeline by calling UseAuthentication(). It must be placed before any middleware that requires authentication, such as UseAuthorization().
AuthenticationMiddleware 由 WebApplication 自动添加。您可以通过调用 UseAuthentication() 来确保它插入到中间件管道中的特定点。它必须放在任何需要身份验证的中间件之前,例如 UseAuthorization()。

ASP.NET Core Identity handles low-level services needed for storing users in a database, ensuring that their passwords are stored safely, and for logging users in and out. You must provide the UI for the functionality yourself and wire it up to the Identity subsystem.
ASP.NET Core Identity 处理将用户存储在数据库中、确保其密码安全存储以及登录和注销用户所需的低级服务。您必须自己提供该功能的 UI,并将其连接到 Identity 子系统。

The Microsoft.AspNetCore.Identity.UI package provides a default UI for the Identity system and includes email confirmation, MFA, and external login provider support. You need to do some additional configuration to enable these features.
Microsoft.AspNetCore.Identity.UI 包为标识系统提供默认 UI,并包括电子邮件确认、MFA 和外部登录提供程序支持。您需要进行一些额外的配置才能启用这些功能。

The default template for Web Application with Individual Account Authentication uses ASP.NET Core Identity to store users in the database with EF Core. It includes all the boilerplate code required to wire the UI up to the Identity system.
具有个人帐户身份验证的 Web 应用程序的默认模板使用 ASP.NET Core Identity 将用户存储在具有 EF Core 的数据库中。它包括将 UI 连接到标识系统所需的所有样板代码。

You can use the UserManager<T> class to create new user accounts, load them from the database, and change their passwords. SignInManager is used to sign a user in and out by assigning the principal for the request and by setting an authentication cookie. The default UI uses these classes for you, to facilitate user registration and login.
您可以使用UserManager<T>该类创建新的用户帐户,从数据库中加载它们,并更改其密码。SignInManager 用于通过为请求分配主体和设置身份验证 cookie 来登录和注销用户。默认 UI 会为您使用这些类,以方便用户注册和登录。

You can update an EF Core DbContext to support Identity by deriving from IdentityDbContext, where TUser is a class that derives from IdentityUser.
您可以通过从 IdentityDbContext 派生来更新 EF Core DbContext 以支持 Identity,其中 TUser 是从 IdentityUser 派生的类。

You can add additional claims to a user using the UserManager.AddClaimAsync(TUser user, Claim claim) method. These claims are added to the HttpContext.User object when the user logs in to your app.
您可以使用 UserManager 向用户添加其他声明。AddClaimAsync(TUser user, Claim claim) 方法。当用户登录到您的应用时,这些声明将添加到 HttpContext.User 对象中。

Claims consist of a type and a value. Both values are strings. You can use standard values for types exposed on the ClaimTypes class, such as ClaimTypes.GivenName and ClaimTypes.FirstName, or you can use a custom string, such as "FullName".
声明由类型和值组成。这两个值都是字符串。您可以对 ClaimTypes 类上公开的类型使用标准值,例如 ClaimTypes.GivenName 和 ClaimTypes.FirstName,也可以使用自定义字符串,例如“FullName”。

ASP.NET Core in Action 22 Creating custom MVC and Razor Page filters

22 Creating custom MVC and Razor Page filters
22 创建自定义 MVC 和 Razor 页面筛选器

This chapter covers
本章涵盖

• Creating custom filters to refactor complex action methods
创建自定义筛选器以重构复杂的作方法
• Using authorization filters to protect your action methods and Razor Pages
使用授权筛选器保护作方法和 Razor 页面
• Short-circuiting the filter pipeline to bypass action and page handler execution
使筛选器管道短路以绕过作和页面处理程序执行
• Injecting dependencies into filters
将依赖项注入筛选器

In chapter 21 I introduced the Model-View-Controller (MVC) and Razor Pages filter pipeline and showed where it fits into the life cycle of a request. You learned how to apply filters to your action method, controllers, and Razor Pages, and the effect of scope on the filter execution order.
在第 21 章中,我介绍了模型-视图-控制器 (MVC) 和 Razor Pages 过滤器管道,并展示了它在请求生命周期中的位置。你了解了如何将筛选器应用于作方法、控制器和 Razor Pages,以及范围对筛选器执行顺序的影响。

In this chapter you’ll take that knowledge and apply it to a concrete example. You’ll learn to create custom filters that you can use in your own apps and how to use them to reduce duplicate code in your action methods.
在本章中,您将利用这些知识并将其应用于具体示例。您将学习如何创建可在自己的应用程序中使用的自定义过滤器,以及如何使用它们来减少作方法中的重复代码。

In section 22.1 I take you through the filter types in detail, how they fit into the MVC pipeline, and what to use them for. For each one, I’ll provide example implementations that you might use in your own application and describe the built-in options available.
在 Section 22.1 中,我将向您详细介绍过滤器类型,它们如何适应 MVC 管道,以及使用它们的用途。对于每个选项,我将提供您可以在自己的应用程序中使用的示例实现,并描述可用的内置选项。

A key feature of filters is the ability to short-circuit a request by generating a response and halting progression through the filter pipeline. This is similar to the way short-circuiting works in middleware, but there are subtle differences for MVC filters. On top of that, the exact behavior is slightly different for each filter, and I cover that in section 22.2.
过滤器的一个关键功能是能够通过生成响应并停止通过过滤器管道的进程来短路请求。这类似于中间件中短路的工作方式,但 MVC 筛选器存在细微的差异。最重要的是,每个过滤器的确切行为略有不同,我在 22.2 节中介绍了这一点。

You typically add MVC filters to the pipeline by implementing them as attributes added to your controller classes, action methods, and Razor Pages. Unfortunately, you can’t easily use dependency injection (DI) with attributes due to the limitations of C#. In section 22.3 I show you how to use the ServiceFilterAttribute and TypeFilterAttribute base classes to enable DI in your filters.
通常,通过将 MVC 筛选器实现为添加到控制器类、作方法和 Razor Pages 的属性,将 MVC 筛选器添加到管道中。遗憾的是,由于 C# 的限制,你不能轻松地将依赖项注入 (DI) 与属性一起使用。在第 22.3 节中,我将向您展示如何使用 ServiceFilterAttribute 和 TypeFilterAttribute 基类在过滤器中启用 DI。

We covered all the background for filters in chapter 21, so in the next section we jump straight into the code and start creating custom MVC filters.
我们在第 21 章中介绍了过滤器的所有背景,因此在下一节中,我们将直接进入代码并开始创建自定义 MVC 过滤器。

22.1 Creating custom filters for your application

22.1 为您的应用程序创建自定义过滤器

ASP.NET Core includes several filters that you can use out of the box, but often the most useful filters are the custom ones that are specific to your own apps. In this section we’ll work through each of the six types of filters I covered in chapter 21. I’ll explain in more detail what they’re for and when you should use them. I’ll point out examples of these filters that are part of ASP.NET Core itself, and you’ll see how to create custom filters for an example application.
ASP.NET Core 包含多个开箱即用的筛选器,但通常最有用的筛选器是特定于你自己的应用程序的自定义筛选器。在本节中,我们将介绍我在第 21 章中介绍的六种类型的过滤器中的每一种。我将更详细地解释它们的用途以及何时应该使用它们。我将指出这些筛选器的示例,这些筛选器是 ASP.NET Core 本身的一部分,您将了解如何为示例应用程序创建自定义筛选器。

To give you something realistic to work with, we’ll start with a web API controller for accessing the recipe application from chapter 12. This controller contains two actions: one for fetching a RecipeDetailViewModel and another for updating a Recipe with new values. The following listing shows your starting point for this chapter, including both action methods.
为了给你一些实际的使用,我们将从一个 Web API 控制器开始,用于访问第 12 章中的配方应用程序。此控制器包含两个作:一个用于获取 RecipeDetailViewModel,另一个用于使用新值更新 Recipe。下面的清单显示了本章的起点,包括两种作方法。

Listing 22.1 Recipe web API controller before refactoring to use filters
列表 22.1 重构以使用过滤器之前的配方 Web API 控制器

[Route("api/recipe")]
public class RecipeApiController : ControllerBase
{
    private readonly bool IsEnabled = true;            #A
    public RecipeService _service; 
    public RecipeApiController(RecipeService service) 
    { 
        _service = service;
    } 

    [HttpGet("{id}")]
    public IActionResult Get(int id)
    {
        if (!IsEnabled) { return BadRequest(); }   #B
        try
        {
            if (!_service.DoesRecipeExist(id))   #C
            {                                    #C
                return NotFound();               #C
            }                                    #C
            var detail = _service.GetRecipeDetail(id);    #D
            Response.GetTypedHeaders().LastModified =     #E
                detail.LastModified;                      #E
            return Ok(detail);    #F
        }
        catch (Exception ex)                #G
        {                                   #G
            return GetErrorResponse(ex);    #G
        }                                   #G
    }

    [HttpPost("{id}")]
    public IActionResult Edit(
        int id, [FromBody] UpdateRecipeCommand command)
    {
        if (!IsEnabled) { return BadRequest(); }     #H
        try
        {
            if (!ModelState.IsValid)             #I
            {                                    #I
                return BadRequest(ModelState);   #I
            }                                    #I
            if (!_service.DoesRecipeExist(id))    #J
            {                                     #J
                return NotFound();                #J
            }                                     #J
            _service.UpdateRecipe(command);    #K
            return Ok();                       #K
        }
        catch (Exception ex)               #L
        {                                  #L
            return GetErrorResponse(ex);   #L
        }                                  #L
    }

    private static IActionResult GetErrorResponse(Exception ex)
    {
        var error = new ProblemDetails         
        {
            Title = "An error occurred",         
            Detail = context.Exception.Message,
            Status = 500,                      
            Type = "https://httpstatuses.com/500"
        };                                              

        return new ObjectResult(error)
        {
            StatusCode = 500
        };
    }
}

❶ This field would be passed in as configuration and is used to control access to actions.
此字段将作为配置传入,用于控制对作的访问。
❷ If the API isn’t enabled, blocks further execution
如果未启用 API,则阻止进一步执行
❸ If the requested Recipe doesn’t exist, returns a 404 response
如果请求的配方不存在,则返回 404 响应
❹ Fetches RecipeDetailViewModel
获取 RecipeDetailViewModel
❺ Sets the Last-Modified response header to the value in the model
将 Last-Modified 响应标头设置为模型中的值
❻ Returns the view model with a 200 response
返回响应为 200 的视图模型
❼ If an exception occurs, catches it and returns the error in an expected format, as a 500 error
如果发生异常,则捕获它并以预期的格式返回错误, 作为 500 错误
❽ If the API isn’t enabled, blocks further execution
如果未启用 API,则阻止进一步执行
❾ Validates the binding model and returns a 400 response if there are errors
验证绑定模型并在出现错误时返回 400 响应
❿ If the requested Recipe doesn’t exist, returns a 404 response
如果请求的配方不存在,则返回 404 响应
⓫ Updates the Recipe from the command and returns a 200 response
从命令更新配方并返回 200 响应
⓬ If an exception occurs, catches it and returns the error in an expected format, as a 500 error
如果发生异常,则捕获它并以预期的格式返回错误, 作为 500 错误

These action methods currently have a lot of code to them, which hides the intent of each action. There’s also quite a lot of duplication between the methods, such as checking that the Recipe entity exists and formatting exceptions.
这些 action method 目前有很多代码,这隐藏了每个 action 的意图。方法之间也存在相当多的重复,例如检查 Recipe 实体是否存在和格式化异常。

In this section you’re going to refactor this controller to use filters for all the code in the methods that’s unrelated to the intent of each action. By the end of the chapter you’ll have a much simpler controller controller that’s far easier to understand, as shown here.
在本节中,您将重构此控制器,以便对方法中与每个作的意图无关的所有代码使用过滤器。在本章结束时,您将拥有一个更简单的控制器控制器,它更容易理解,如下所示。

Listing 22.2 Recipe web API controller after refactoring to use filters
列表 22.2 重构为使用过滤器后的配方 Web API 控制器

[Route("api/recipe")]
[ValidateModel]      #A
[HandleException]    #A
[FeatureEnabled(IsEnabled = true)]     #A
public class RecipeApiController : ControllerBase
{
    public RecipeService _service;
    public RecipeApiController(RecipeService service)
    {
        _service = service;
    }

    [HttpGet("{id}")]
    [EnsureRecipeExists]    #B
    [AddLastModifiedHeader]    #B
    public IActionResult Get(int id)
    {
        var detail = _service.GetRecipeDetail(id);     #C
        return Ok(detail);                             #C
    }

    [HttpPost("{id}")]
    [EnsureRecipeExists]        #D
    public IActionResult Edit(
        int id, [FromBody] UpdateRecipeCommand command)
    {
        _service.UpdateRecipe(command);    #E
        return Ok();                       #E
    }
}

❶ The filters encapsulate the majority of logic common to multiple action methods.
过滤器封装了多个方法通用的大多数逻辑。
❷ Placing filters at the action level limits them to a single action.
将过滤器放在作级别会将它们限制为单个作。
❸ The intent of the action, return a Recipe view model, is much clearer.
作的意图(返回配方视图模型)要清晰得多。
❹ Placing filters at the action level can control the order in which they execute.
将过滤器放在作级别可以控制它们的执行顺序。
❺ The intent of the action, update a Recipe, is much clearer.
作 update a Recipe 的意图要明确得多。

I think you’ll have to agree that the controller in listing 22.2 is much easier to read! In this section you’ll refactor the controller bit by bit, removing cross-cutting code to get to something more manageable. All the filters we’ll create in this section will use the sync filter interfaces. I’ll leave it to you, as an exercise, to create their async counterparts. We’ll start by looking at authorization filters and how they relate to security in ASP.NET Core.
我想你得同意,清单 22.2 中的控制器要容易得多!在本节中,您将逐步重构控制器,删除横切代码以获得更易于管理的内容。我们将在本节中创建的所有过滤器都将使用 sync 过滤器接口。作为练习,我将留给您创建它们的异步对应项。首先,我们将了解授权筛选器以及它们与 ASP.NET Core 中的安全性有何关系。

22.1.1 Authorization filters: Protecting your APIs

22.1.1 授权过滤器:保护您的 API

Authentication and authorization are related, fundamental concepts in security that we’ll be looking at in detail in chapters 23 and 24.
身份验证和授权是安全中相关的基本概念,我们将在第 23 章和第 24 章中详细介绍。

DEFINITION Authentication is concerned with determining who made a request. Authorization is concerned with what a user is allowed to access.
定义:身份验证与确定谁发出了请求有关。授权与允许用户访问的内容有关。

Authorization filters run first in the MVC filter pipeline, before any other filters. They control access to the action method by immediately short-circuiting the pipeline when a request doesn’t meet the necessary requirements.
授权筛选器首先在 MVC 筛选器管道中运行,然后再运行任何其他筛选器。当请求不满足必要的要求时,它们通过立即使管道短路来控制对作方法的访问。

ASP.NET Core has a built-in authorization framework that you should use when you need to protect your MVC application or your web APIs. You can configure this framework with custom policies that let you finely control access to your actions.
ASP.NET Core 有一个内置的授权框架,当您需要保护 MVC 应用程序或 Web API 时,您应该使用该框架。您可以使用自定义策略配置此框架,以便精细控制对作的访问。

Tip It’s possible to write your own authorization filters by implementing IAuthorizationFilter or IAsyncAuthorizationFilter, but I strongly advise against it. The ASP.NET Core authorization framework is highly configurable and should meet all your needs.
提示:通过实现 IAuthorizationFilter 或 IAsyncAuthorizationFilter 可以编写自己的授权筛选器,但我强烈建议不要这样做。ASP.NET Core 授权框架是高度可配置的,应该可以满足您的所有需求。

At the heart of MVC authorization is an authorization filter, AuthorizeFilter, which you can add to the filter pipeline by decorating your actions or controllers with the [Authorize] attribute. In its simplest form, adding the [Authorize] attribute to an action, as in the following listing, means that the request must be made by an authenticated user to be allowed to continue. If you’re not logged in, it will short-circuit the pipeline, returning a 401 Unauthorized response to the browser.
MVC 授权的核心是授权筛选器 AuthorizeFilter,您可以通过使用 [Authorize] 属性修饰作或控制器来将其添加到筛选器管道中。在最简单的形式中,将 [Authorize] 属性添加到作中,如下面的清单所示,意味着请求必须由经过身份验证的用户发出才能继续。如果您未登录,它将使管道短路,向浏览器返回 401 Unauthorized 响应。

Listing 22.3 Adding [Authorize] to an action method
清单 22.3 向作方法添加 [Authorize]

public class RecipeApiController : ControllerBase
{
    public IActionResult Get(int id)    #A
    {
        // method body
    }

    [Authorize]                  #B
    public IActionResult Edit(                             #C
        int id, [FromBody] UpdateRecipeCommand command)    #C
    {
        // method body
    }
}

❶ The Get method has no [Authorize] attribute, so it can be executed by anyone.
Get 方法没有 [Authorize] 属性,因此任何人都可以执行它。
❷ Adds the AuthorizeFilter to the filter pipeline using [Authorize]
使用 [Authorize]将 AuthorizeFilter 添加到筛选器管道
❸ The Edit method can be executed only if you’re logged in.
只有在您登录后才能执行 Edit 方法。

As with all filters, you can apply the [Authorize] attribute at the controller level to protect all the actions on a controller, to a Razor Page to protect all the page handler methods in a page, or even globally to protect every endpoint in your app.
与所有筛选器一样,可以在控制器级别应用 [Authorize] 属性以保护控制器上的所有作,将 [Authorize] 属性应用于 Razor 页面以保护页面中的所有页面处理程序方法,甚至可以全局应用以保护应用中的每个终结点。

NOTE We’ll explore authorization in detail in chapter 24, including how to add more detailed requirements so that only specific sets of users can execute an action.
注意:我们将在第 24 章中详细探讨授权,包括如何添加更详细的要求,以便只有特定的用户集才能执行作。

The next filters in the pipeline are resource filters. In the next section you’ll extract some of the common code from RecipeApiController and see how easy it is to create a short-circuiting filter.
管道中的下一个筛选器是资源筛选器。在下一节中,您将从 RecipeApiController 中提取一些通用代码,并了解创建短路过滤器是多么容易。

22.1.2 Resource filters: Short-circuiting your action methods

22.1.2 资源过滤器:使作方法短路

Resource filters are the first general-purpose filters in the MVC filter pipeline. In chapter 21 you saw minimal examples of both sync and async resource filters, which logged to the console. In your own apps, you can use resource filters for a wide range of purposes, thanks to the fact that they execute so early (and late) in the filter pipeline.
资源筛选器是 MVC 筛选器管道中的第一个通用筛选器。在第 21 章中,您看到了 sync 和 async 资源过滤器的最小示例,它们记录到控制台中。在您自己的应用程序中,您可以将资源筛选条件用于多种用途,这要归功于它们在筛选管道中执行得如此早(和延迟)的事实。

The ASP.NET Core framework includes a few implementations of resource filters you can use in your apps:
ASP.NET Core 框架包含一些可在应用中使用的资源筛选器实现:

• ConsumesAttribute—Can be used to restrict the allowed formats an action method can accept. If your action is decorated with [Consumes("application/json")], but the client sends the request as Extensible Markup Language (XML), the resource filter will short-circuit the pipeline and return a 415 Unsupported Media Type response.
ConsumesAttribute - 可用于限制作方法可以接受的允许格式。如果您的作使用 [Consumes(“application/json”)] 修饰,但客户端以可扩展标记语言 (XML) 的形式发送请求,则资源筛选器将使管道短路并返回 415 不支持的媒体类型响应。
• SkipStatusCodePagesAttribute—This filter prevents the StatusCodePagesMiddleware from running for the response. This can be useful if, for example, you have both web API controllers and Razor Pages in the same application. You can apply this attribute to the controllers to ensure that API error responses are passed untouched, but all error responses from Razor Pages are handled by the middleware.
SkipStatusCodePagesAttribute - 此筛选器可防止 StatusCodePagesMiddleware 针对响应运行。例如,如果同一应用程序中同时具有 Web API 控制器和 Razor Pages,这可能非常有用。可以将此属性应用于控制器,以确保 API 错误响应原封不动地传递,但来自 Razor Pages 的所有错误响应都由中间件处理。

Resource filters are useful when you want to ensure that the filter runs early in the pipeline, before model binding. They provide an early hook into the pipeline for your logic so you can quickly short-circuit the request if you need to.
当您想要确保筛选条件在模型绑定之前在管道中尽早运行时,资源筛选条件非常有用。它们为您的 logic 提供了管道的早期钩子,因此您可以在需要时快速短路请求。

Look back at listing 22.1 and see whether you can refactor any of the code into a resource filter. One candidate line appears at the start of both the Get and Edit methods:
回顾一下清单 22.1,看看你是否可以将任何代码重构为资源过滤器。一个候选行出现在 Get 和 Edit 方法的开头:

if (!IsEnabled) { return BadRequest(); }

This line of code is a feature toggle that you can use to disable the availability of the whole API, based on the IsEnabled field. In practice, you’d probably load the IsEnabled field from a database or configuration file so you could control the availability dynamically at runtime, but for this example I’m using a hardcoded value.
这行代码是一个功能切换,可用于根据 IsEnabled 字段禁用整个 API 的可用性。在实践中,您可能会从数据库或配置文件加载 IsEnabled 字段,以便您可以在运行时动态控制可用性,但对于此示例,我使用的是硬编码值。

Tip To read more about using feature toggles in your applications, see my series “Adding feature flags to an ASP.NET Core app” at http://mng.bz/2e40.
提示:要了解有关在应用程序中使用功能切换的更多信息,请参阅我在 http://mng.bz/2e40 上的系列文章“向 ASP.NET Core 应用程序添加功能标志”。

This piece of code is self-contained cross-cutting logic, which is somewhat orthogonal to the main intent of each action method—a perfect candidate for a filter. You want to execute the feature toggle early in the pipeline, before any other logic, so a resource filter makes sense.
这段代码是自包含的横切逻辑,它与每个作方法的主要意图在某种程度上正交,是过滤器的完美候选者。您希望在管道的早期、任何其他逻辑之前执行功能切换,因此资源筛选条件是有意义的。

Tip Technically, you could also use an authorization filter for this example, but I’m following my own advice of “Don’t write your own authorization filters!”
提示:从技术上讲,您也可以对此示例使用授权过滤器,但我遵循我自己的建议,即“不要编写自己的授权过滤器!

The next listing shows an implementation of FeatureEnabledAttribute, which extracts the logic from the action methods and moves it into the filter. I’ve also exposed the IsEnabled field as a property on the filter.
下一个清单显示了 FeatureEnabledAttribute 的实现,它从作方法中提取逻辑并将其移动到过滤器中。我还将 IsEnabled 字段作为筛选器上的属性公开。

Listing 22.4 The FeatureEnabledAttribute resource filter
列表 22.4 FeatureEnabledAttribute 资源过滤器

public class FeatureEnabledAttribute : Attribute, IResourceFilter
{
    public bool IsEnabled { get; set; }   #A
    public void OnResourceExecuting(        #B
        ResourceExecutingContext context)   #B
    {
        if (!IsEnabled)                                #C
        {                                              #C
            context.Result = new BadRequestResult();   #C
        }                                              #C
    }
    public void OnResourceExecuted(             #D
        ResourceExecutedContext context) { }    #D
}

❶ Defines whether the feature is enabled
定义是否启用该功能
❷ Executes before model binding, early in the filter pipeline
在模型绑定之前执行,在过滤器管道的早期
❸ If the feature isn’t enabled, short-circuits the pipeline by setting the context.Result property
如果未启用该功能,则通过设置上下文来短路管道。结果属性
❹ Must be implemented to satisfy IResourceFilter, but not needed in this case
必须实现以满足 IResourceFilter,但在这种情况下不需要

This simple resource filter demonstrates a few important concepts, which are applicable to most filter types:
这个简单的资源筛选条件演示了几个重要的概念,这些概念适用于大多数筛选条件类型:

• The filter is an attribute as well as a filter. This lets you decorate your controller, action methods, and Razor Pages with it using [FeatureEnabled(IsEnabled = true)].
过滤器既是属性又是过滤器。这样,您就可以使用 [FeatureEnabled(IsEnabled = true)] 使用它来装饰控制器、作方法和 Razor 页面。

• The filter interface consists of two methods: Executing, which runs before model binding, and Executed, which runs after the result has executed. You must implement both, even if you only need one for your use case.
筛选器接口由两种方法组成:Executing (在模型绑定之前运行)和 Executed (在结果执行之后运行)。您必须同时实施这两个项目,即使您的用例只需要一个项目。

• The filter execution methods provide a context object. This provides access to, among other things, the HttpContext for the request and metadata about the action method that was selected.
过滤器执行方法提供上下文对象。这提供了对请求的 HttpContext 和有关所选作方法的元数据等的访问。

• To short-circuit the pipeline, set the context.Result property to an IactionResult instance. The framework will execute this result to generate the response, bypassing any remaining filters in the pipeline and skipping the action method (or page handler) entirely. In this example, if the feature isn’t enabled, you bypass the pipeline by returning BadRequestResult, which returns a 400 error to the client.
要使管道短路,请设置上下文。Result 属性设置为 IactionResult 实例。框架将执行此结果以生成响应,绕过管道中的任何剩余筛选器,并完全跳过作方法(或页面处理程序)。在此示例中,如果未启用该功能,则通过返回 BadRequestResult 来绕过管道,这会向客户端返回 400 错误。

By moving this logic into the resource filter, you can remove it from your action methods and instead decorate the whole API controller with a simple attribute:
通过将此逻辑移动到资源过滤器中,您可以将其从 action 方法中删除,而是使用 simple 属性装饰整个 API 控制器:

[FeatureEnabled(IsEnabled = true)]
[Route("api/recipe")]
public class RecipeApiController : ControllerBase

You’ve extracted only two lines of code from your action methods so far, but you’re on the right track. In the next section we’ll move on to action filters and extract two more filters from the action method code.
到目前为止,您只从 action 方法中提取了两行代码,但您走在正确的轨道上。在下一节中,我们将继续讨论作筛选器,并从作方法代码中提取另外两个筛选器。

22.1.3 Action filters: Customizing model binding and action results

22.1.3作过滤器:自定义模型绑定和作结果

Action filters run just after model binding, before the action method executes. Thanks to this positioning, action filters can access all the arguments that will be used to execute the action method, which makes them a powerful way of extracting common logic out of your actions.
Action筛选器在模型绑定之后,在作方法执行之前运行。由于这种定位,动作过滤器可以访问将用于执行动作方法的所有参数,这使它们成为从动作中提取通用逻辑的强大方式。

On top of this, they run after the action method has executed and can completely change or replace the IActionResult returned by the action if you want. They can even handle exceptions thrown in the action.
最重要的是,它们在作方法执行后运行,并且可以根据需要完全更改或替换作返回的 IActionResult。它们甚至可以处理作中引发的异常。

NOTE Action filters don’t execute for Razor Pages. Similarly, page filters don’t execute for action methods.
注意:不会对 Razor Pages 执行作筛选器。同样,页面过滤器不会对作方法执行。

The ASP.NET Core framework includes several action filters out of the box. One of these commonly used filters is ResponseCacheFilter, which sets HTTP caching headers on your action-method responses.
ASP.NET Core 框架包含多个现成的作筛选器。其中一个常用的过滤器是 ResponseCacheFilter,它在作方法响应上设置 HTTP 缓存标头。

NOTE I have described filters as being attributes, but that’s not always the case. For example, the action filter is called ResponseCacheFilter, but this type is internal to the ASP.NET Core framework. To apply the filter, you use the public [ResponseCache] attribute instead, and the framework automatically configures the ResponseCacheFilter as appropriate. This separation between attribute and filter is largely an artifact of the internal design, but it can be useful, as you’ll see in section 22.3.
注意:我曾将过滤器描述为属性,但情况并非总是如此。例如,作筛选器称为 ResponseCacheFilter,但此类型是 ASP.NET Core 框架的内部类型。若要应用筛选器,请改用公共 [ResponseCache] 属性,框架会根据需要自动配置 ResponseCacheFilter。attribute 和 filter 之间的这种分离在很大程度上是内部设计的产物,但它可能很有用,正如您将在 Section 22.3 中看到的那样。

Response caching vs. output caching
响应缓存与输出缓存

Caching is a broad topic that aims to improve the performance of an application over the naive approach. But caching can also make debugging issues difficult and may even be undesirable in some situations. Consequently, I often apply ResponseCacheFilter to my action methods to set HTTP caching headers that disable caching! You can read about this and other approaches to caching in Microsoft’s “Response caching in ASP.NET Core” documentation at http://mng.bz/2eGd.
缓存是一个广泛的主题,旨在通过简单的方法提高应用程序的性能。但是,缓存也会使调试问题变得困难,在某些情况下甚至可能是不可取的。因此,我经常将 ResponseCacheFilter 应用于我的作方法,以设置禁用缓存的 HTTP 缓存标头!您可以在 http://mng.bz/2eGd 的 Microsoft 的“ASP.NET Core 中的响应缓存”文档中阅读有关此方法和其他缓存方法的信息。

Note that the ResponseCacheFilter applies cache control headers only to your outgoing responses; it doesn’t cache the response on the server. These headers tell the client (such as a browser) whether it can skip sending a request and reuse the response. If you have relatively static endpoints, this can massively reduce the load on your app.
请注意,ResponseCacheFilter 仅将缓存控制标头应用于您的传出响应;它不会在服务器上缓存响应。这些标头告诉客户端(例如浏览器)是否可以跳过发送请求并重用响应。如果您有相对静态的终端节点,这可以大大减少应用程序的负载。

This is different from output caching, introduced in .NET 7. Output caching involves storing a generated response on the server and reusing it for subsequent requests. In the simplest case, the response is stored in memory and reused for appropriate requests, but you can configure ASP.NET Core to store the output elsewhere, such as a database.
这与 .NET 7 中引入的输出缓存不同。输出缓存涉及将生成的响应存储在服务器上,并将其重新用于后续请求。在最简单的情况下,响应存储在内存中并重复用于适当的请求,但您可以将 ASP.NET Core 配置为将输出存储在其他位置,例如数据库。

Output caching is generally more configurable than response caching, as you can choose exactly what to cache and when to invalidate it, but it is also much more resource-heavy. For details on how to enable output caching for an endpoint, see the documentation at http://mng.bz/Bmlv.
输出缓存通常比响应缓存更易于配置,因为您可以准确选择要缓存的内容以及何时使其失效,但它的资源消耗也要大得多。有关如何为终端节点启用输出缓存的详细信息,请参阅 http://mng.bz/Bmlv 中的文档。

The real power of action filters comes when you build filters tailored to your own apps by extracting common code from your action methods. To demonstrate, I’m going to create two custom filters for RecipeApiController:
当您通过从作方法中提取通用代码来构建针对自己的应用程序量身定制的过滤器时,作过滤器的真正强大之处在于。为了演示,我将为 RecipeApiController 创建两个自定义过滤器:

• ValidateModelAttribute—This will return BadRequestResult if the model state indicates that the binding model is invalid and will short-circuit the action execution. This attribute used to be a staple of my web API applications, but the [ApiController] attribute now handles this (and more) for you. Nevertheless, I think it’s useful to understand what’s going on behind the scenes.
ValidateModelAttribute - 如果模型状态指示绑定模型无效,并且将使作执行短路,则将返回 BadRequestResult。此属性曾经是我的 Web API 应用程序的主要内容,但 [ApiController] 属性现在为您处理此 (以及更多) 。尽管如此,我认为了解幕后发生的事情是有用的。

• EnsureRecipeExistsAttribute—This uses each action method’s id argument to validate that the requested Recipe entity exists before the action method runs. If the Recipe doesn’t exist, the filter returns NotFoundResult and short-circuits the pipeline.
EnsureRecipeExistsAttribute - 在作方法运行之前,使用每个作方法的 id 参数来验证请求的配方实体是否存在。如果 Recipe 不存在,则筛选条件将返回 NotFoundResult 并使管道短路。

As you saw in chapter 16, the MVC framework automatically validates your binding models before executing your actions and Razor Page handlers, but it’s up to you to decide what to do about it. For web API controllers, it’s common to return a 400 Bad Request response containing a list of the errors, as shown in figure 22.1.
正如您在第 16 章中所看到的,MVC 框架会在执行您的作和 Razor Page 处理程序之前自动验证您的绑定模型,但由您决定如何处理它。对于 Web API 控制器,通常会返回包含错误列表的 400 Bad Request 响应,如图 22.1 所示。

alt text

Figure 22.1 Posting data to a web API using Postman. The data is bound to the action method’s binding model and validated. If validation fails, it’s common to return a 400 Bad Request response with a list of the validation errors.
图 22.1 使用 Postman 将数据发布到 Web API。数据将绑定到作方法的绑定模型并进行验证。如果验证失败,通常会返回 400 Bad Request 响应,其中包含验证错误列表。

You should ordinarily use the [ApiController] attribute on your web API controllers, which gives you this behavior (and uses Problem Details responses) automatically. But if you can’t or don’t want to use that attribute, you can create a custom action filter instead. The following listing shows a basic implementation that is similar to the behavior you get with the [ApiController] attribute.
通常应在 Web API 控制器上使用 [ApiController] 属性,该属性会自动提供此行为 (并使用问题详细信息响应) 。但是,如果您不能或不想使用该属性,则可以改为创建自定义作筛选条件。下面的列表显示了一个基本实现,它类似于您使用 [ApiController] 属性获得的行为。

Listing 22.5 The action filter for validating ModelState
列表 22.5 用于验证 ModelState 的动作过滤器

public class ValidateModelAttribute : ActionFilterAttribute      #A
{
    public override void OnActionExecuting(    #B
        ActionExecutingContext context)        #B
    {
        if (!context.ModelState.IsValid)    #C
        {
            context.Result =                                      #D
                new BadRequestObjectResult(context.ModelState);   #D
        }
    }
}

❶ For convenience, you derive from the ActionFilterAttribute base class.
为方便起见,您可以从 ActionFilterAttribute 基类派生。
❷ Overrides the Executing method to run the filter before the Action executes
重写 Executing 方法以在 Action 执行之前运行过滤器
❸ Model binding and validation have already run at this point, so you can check the state.
此时模型绑定和验证已经运行,因此您可以检查状态。
❹ If the model isn’t valid, sets the Result property, which short-circuits the action execution
如果模型无效,则设置 Result 属性,这将使作执行短路

This attribute is self-explanatory and follows a similar pattern to the resource filter in section 22.1.2, but with a few interesting points:
此属性是不言自明的,并且遵循与第 22.1.2 节中的资源过滤器类似的模式,但有一些有趣的点:

• I have derived from the abstract ActionFilterAttribute. This class implements IActionFilter and IResultFilter, as well as their async counterparts, so you can override the methods you need as appropriate. This prevents needing to add an unused OnActionExecuted() method, but using the base class is entirely optional and a matter of preference.
我从抽象的 ActionFilterAttribute 派生而来。此类实现 IActionFilter 和 IResultFilter 以及它们的异步对应项,因此您可以根据需要重写所需的方法。这样就不需要添加未使用的 OnActionExecuted() 方法,但使用基类完全是可选的,并且是首选项问题。

• Action filters run after model binding has taken place, so context.ModelState contains the validation errors if validation failed.
Action筛选器在模型绑定发生后运行,因此 context.ModelState 包含验证错误(如果验证失败)。

• Setting the Result property on context short-circuits the pipeline. But due to the position of the action filter stage, only the action method execution and later action filters are bypassed; all the other stages of the pipeline run as though the action executed as normal.
在上下文上设置 Result 属性会使管道短路。但是,由于作筛选器阶段的位置,仅绕过作方法执行和以后的作筛选器;管道的所有其他阶段都像正常执行作一样运行。

If you apply this action filter to your RecipeApiController, you can remove this code from the start of both the action methods, as it will run automatically in the filter pipeline:
如果你将此作筛选器应用于 RecipeApiController,则可以从两个作方法的开头删除此代码,因为它将在筛选器管道中自动运行:

if (!ModelState.IsValid)
{
    return BadRequest(ModelState);
}

You’ll use a similar approach to remove the duplicate code that checks whether the id provided as an argument to the action methods corresponds to an existing Recipe entity.
您将使用类似的方法来删除重复代码,该代码检查作为作方法的参数提供的 id 是否对应于现有 Recipe 实体。

The following listing shows the EnsureRecipeExistsAttribute action filter. This uses an instance of RecipeService to check whether the Recipe exists and returns a 404 Not Found if it doesn’t.
以下清单显示了 EnsureRecipeExistsAttribute作筛选器。这将使用 RecipeService 的实例来检查 Recipe 是否存在,如果不存在,则返回 404 Not Found。

Listing 22.6 An action filter to check whether a Recipe exists
清单 22.6 用于检查 Recipe 是否存在的 action filter

public class EnsureRecipeExistsAtribute : ActionFilterAttribute
{
    public override void OnActionExecuting(
        ActionExecutingContext context)
    {
        var service = context.HttpContext.RequestServices    #A
            .GetService<RecipeService>();    #A
        var recipeId = (int) context.ActionArguments["id"];  #B
        if (!service.DoesRecipeExist(recipeId))    #C
        {
            context.Result = new NotFoundResult();    #D
        }
    }
}

❶ Fetches an instance of RecipeService from the DI container
从 DI 容器中获取 RecipeService 的实例
❷ Retrieves the id parameter that will be passed to the action method when it executes
检索执行时将传递给作方法的 id 参数
❸ Checks whether a Recipe entity with the given RecipeId exists
检查具有给定 RecipeId 的 Recipe 实体是否存在
❹ If it doesn’t exist, returns a 404 Not Found result and short-circuits the pipeline
如果不存在,则返回 404 Not Found 结果并短路管道

As before, you’ve derived from ActionFilterAttribute for simplicity and overridden the OnActionExecuting method. The main functionality of the filter relies on the DoesRecipeExist() method of RecipeService, so the first step is to obtain an instance of RecipeService. The context parameter provides access to the HttpContext for the request, which in turn lets you access the DI container and use RequestServices.GetService() to return an instance of RecipeService.
与以前一样,为了简单起见,您从 ActionFilterAttribute 派生并重写了 OnActionExecuting 方法。过滤器的主要功能依赖于 RecipeService 的 DoesRecipeExist() 方法,因此第一步是获取 RecipeService 的实例。context 参数提供对请求的 HttpContext 的访问,这反过来又允许您访问 DI 容器并使用 RequestServices.GetService() 返回 RecipeService 的实例。

Warning This technique for obtaining dependencies is known as service location and is generally considered to be an antipattern. In section 22.3 I’ll show you a better way to use the DI container to inject dependencies into your filters.
警告:这种用于获取依赖项的技术称为 服务定位,通常被视为反模式。在 Section 22.3 中,我将向您展示一种更好的方法 使用 DI 容器将依赖项注入过滤器。

As well as RecipeService, the other piece of information you need is the id argument of the Get and Edit action methods. In action filters, model binding has already occurred, so the arguments that the framework will use to execute the action method are already known and are exposed on context.ActionArguments.
除了 RecipeService 之外,您还需要的另一条信息是 Get 和 Edit作方法的 id 参数。在作筛选器中,模型绑定已经发生,因此框架将用于执行作方法的参数是已知的,并在上下文中公开。ActionArguments 的 API 参数。

The action arguments are exposed as Dictionary<string, object>, so you can obtain the id parameter using the "id" string key. Remember to cast the object to the correct type.
action参数公开为 Dictionary<string, object>,因此您可以使用 “id” 字符串键获取 id 参数。请记住将对象强制转换为正确的类型。

Tip Whenever I see magic strings like this, I always try to replace them by using the nameof operator. Unfortunately, nameof won’t work for method arguments like this, so be careful when refactoring your code. I suggest explicitly applying the action filter to the action method (instead of globally, or to a controller) to remind you about that implicit coupling.
提示:每当我看到这样的魔术字符串时,我总是尝试使用 nameof 运算符替换它们。不幸的是,nameof 不适用于这样的方法参数,因此在重构代码时要小心。我建议将 action 过滤器显式地应用于 action 方法(而不是全局应用,或应用于控制器),以提醒您这种隐式耦合。

With RecipeService and id in place, it’s a case of checking whether the identifier corresponds to an existing Recipe entity and if not, setting context.Result to NotFoundResult. This short-circuits the pipeline and bypasses the action method altogether.
有了 RecipeService 和 id,就可以检查标识符是否对应于现有的 Recipe 实体,如果不是,则设置 context。Result 设置为 NotFoundResult。这会使管道短路并完全绕过 action 方法。

NOTE Remember that you can have multiple action filters running in a single stage. Short-circuiting the pipeline by setting context.Result prevents later filters in the stage from running and bypasses the action method execution.
注意:请记住,您可以在单个阶段中运行多个作筛选器。通过设置 context 使管道短路。Result 会阻止阶段中以后的过滤器运行,并绕过作方法的执行。

Before we move on, it’s worth mentioning a special case for action filters. The ControllerBase base class implements IActionFilter and IAsyncActionFilter itself. If you find yourself creating an action filter for a single controller and want to apply it to every action in that controller, you can override the appropriate methods on your controller instead, as in the following listing.
在我们继续之前,值得一提的是作过滤器的一个特殊情况。ControllerBase 基类实现 IActionFilter 和 IAsyncActionFilter 本身。如果您发现自己为单个控制器创建了一个作过滤器,并希望将其应用于该控制器中的每个作,则可以改为覆盖控制器上的相应方法,如下面的清单所示。

Listing 22.7 Overriding action filter methods directly on ControllerBase
清单 22.7 直接在 ControllerBase 上覆盖动作过滤器方法

public class HomeController : ControllerBase      #A
{
    public override void OnActionExecuting(     #B
        ActionExecutingContext context)         #B
    { }                                         #B
    public override void OnActionExecuted(    #C
        ActionExecutedContext context)        #C
    { }                                       #C
}

❶ Derives from the ControllerBase class
派生自 ControllerBase 类
❷ Runs before any other action filters for every action in the controller
在控制器中每个动作的任何其他动作过滤器之前运行
❸ Runs after all other action filters for every action in the controller
在控制器中每个动作的所有其他动作过滤器之后运行

If you override these methods on your controller, they’ll run in the action filter stage of the filter pipeline for every action on the controller. The OnActionExecuting method runs before any other action filters, regardless of ordering or scope, and the OnActionExecuted method runs after all other action filters.
如果您在控制器上覆盖这些方法,则它们将在控制器上每个作的过滤器管道的作过滤器阶段中运行。OnActionExecuting 方法在任何其他作筛选器之前运行,而不管顺序或范围如何,而 OnActionExecuted 方法在所有其他作筛选器之后运行。

Tip The controller implementation can be useful in some cases, but you can’t control the ordering related to other filters. Personally, I generally prefer to break logic into explicit, declarative filter attributes, but it depends on the situation, and as always, the choice is yours.
提示:控制器实现在某些情况下可能很有用,但您无法控制与其他过滤器相关的 Sequences。就个人而言,我通常更喜欢将 logic 分解为显式的声明性 filter 属性,但这取决于具体情况,并且一如既往,选择权在您手中。

With the resource and action filters complete, your controller is looking much tidier, but there’s one aspect in particular that would be nice to remove: the exception handling. In the next section we’ll look at how to create a custom exception filter for your controller and why you might want to do this instead of using exception handling middleware.
完成资源和作筛选器后,您的控制器看起来更加整洁,但有一个方面特别值得删除:异常处理。在下一节中,我们将了解如何为控制器创建自定义异常筛选器,以及为什么您可能希望执行此作而不是使用异常处理中间件。

22.1.4 Exception filters: Custom exception handling for your action methods

22.1.4 异常过滤器:作方法的自定义异常处理

In chapter 4 I went into some depth about types of error-handling middleware you can add to your apps. These let you catch exceptions thrown from any later middleware and handle them appropriately. If you’re using exception handling middleware, you may be wondering why we need exception filters at all.
在第 4 章中,我深入探讨了您可以添加到应用程序中的错误处理中间件的类型。这些允许您捕获任何后续中间件引发的异常并适当地处理它们。如果你正在使用异常处理中间件,你可能想知道为什么我们需要异常过滤器。

The answer to this is pretty much the same as I outlined in chapter 21: filters are great for cross-cutting concerns, when you need behavior that’s specific to MVC or that should only apply to certain routes.
这个问题的答案与我在第 21 章中概述的几乎相同:当您需要特定于 MVC 的行为或应仅适用于某些路由的行为时,过滤器非常适合横切关注点。

Both of these can apply in exception handling. Exception filters are part of the MVC framework, so they have access to the context in which the error occurred, such as the action or Razor Page that was executing. This can be useful for logging additional details when errors occur, such as the action parameters that caused the error.
这两者都可以应用于异常处理。异常筛选器是 MVC 框架的一部分,因此它们有权访问发生错误的上下文,例如正在执行的作或 Razor Page。这对于在发生错误时记录其他详细信息(如导致错误的作参数)非常有用。

Warning If you use exception filters to record action method arguments, make sure you’re not storing sensitive data in your logs, such as passwords or credit card details.
警告:如果您使用异常筛选条件来记录作方法参数,请确保您没有在日志中存储敏感数据,例如密码或信用卡详细信息。

You can also use exception filters to handle errors from different routes in different ways. Imagine you have both Razor Pages and web API controllers in your app, as we do in the recipe app. What happens when an exception is thrown by a Razor Page?
您还可以使用异常筛选条件以不同方式处理来自不同路由的错误。假设您的应用程序中同时有 Razor Pages 和 Web API 控制器,就像我们在配方应用程序中所做的那样。当 Razor Page 引发异常时会发生什么情况?

As you saw in chapter 4, the exception travels back up the middleware pipeline and is caught by exception handler middleware. The exception handler middleware reexecutes the pipeline and generates an HTML error page.
正如您在第 4 章中看到的,异常沿中间件管道向上传输,并被异常处理程序中间件捕获。异常处理程序中间件将重新执行管道并生成 HTML 错误页。

That’s great for your Razor Pages, but what about exceptions in your web API controllers? If your API throws an exception and consequently returns HTML generated by the exception handler middleware, that’s going to break a client that called the API expecting a JavaScript Object Notation (JSON) response!
这对 Razor Pages 来说非常有用,但 Web API 控制器中的异常呢?如果您的 API 引发异常,并因此返回异常处理程序中间件生成的 HTML,这将破坏调用 API 的客户端,该客户端需要 JavaScript 对象表示法 (JSON) 响应!

Tip The added complexity introduced by having to handle these two very different clients is the reason I prefer to create separate applications for APIs and server-rendered apps.
提示:必须处理这两个截然不同的客户端所带来的复杂性增加了,这就是我更喜欢为 API 和服务器呈现的应用程序创建单独的应用程序的原因。

Instead, exception filters let you handle the exception in the filter pipeline and generate an appropriate response body for API clients. The exception handler middleware intercepts only errors without a body, so it will let the modified web API response pass untouched.
相反,异常筛选条件允许您处理筛选条件管道中的异常,并为 API 客户端生成适当的响应正文。异常处理程序中间件仅拦截没有正文的错误,因此它将允许修改后的 Web API 响应原封不动地通过。

NOTE The [ApiController] attribute converts error StatusCodeResults to a ProblemDetails object, but it doesn’t catch exceptions.
注意:[ApiController] 属性将错误 StatusCodeResults 转换为 ProblemDetails 对象,但它不会捕获异常。

Exception filters can catch exceptions from more than your action methods and page handlers. They’ll run if an exception occurs at these times:
异常筛选器可以捕获来自多个作方法和页面处理程序的异常。如果在以下时间发生异常,它们将运行:

• During model binding or validation
在模型绑定或验证期间
• When the action method or page handler is executing
当作方法或页面处理程序正在执行时
• When an action filter or page filter is executing
当作过滤器或页面过滤器正在执行时

You should note that exception filters won’t catch exceptions thrown in any filters other than action and page filters, so it’s important that your resource and result filters don’t throw exceptions. Similarly, they won’t catch exceptions thrown when executing an IActionResult, such as when rendering a Razor view to HTML.
您应该注意,异常筛选器不会捕获除作和页面筛选器之外的任何筛选器中引发的异常,因此您的资源和结果筛选器不会引发异常非常重要。同样,它们不会捕获在执行 IActionResult 时引发的异常,例如在将 Razor 视图呈现为 HTML 时。

Now that you know why you might want an exception filter, go ahead and implement one for RecipeApiController, as shown next. This lets you safely remove the try-catch block from your action methods, knowing that your filter will catch any errors.
现在,您知道为什么可能需要异常过滤器,请继续为 RecipeApiController 实现一个异常过滤器,如下所示。这样,您就可以安全地从作方法中删除 try-catch 块,因为您知道过滤器将捕获任何错误。

Listing 22.8 The HandleExceptionAttribute exception filter
示例 22.8 HandleExceptionAttribute 异常过滤器

public class HandleExceptionAttribute : ExceptionFilterAttribute      #A
{
    public override void OnException(ExceptionContext context)      #B
    {
        var error = new ProblemDetails               #C
        {                                            #C
            Title = "An error occurred",             #C
            Detail = context.Exception.Message,      #C
            Status = 500,                            #C
            Type = " https://httpwg.org/specs/rfc9110.html#status.500"  #C
        };                                           #C

        context.Result = new ObjectResult(error)    #D
        {                                           #D
            StatusCode = 500                        #D
        };                                          #D
        context.ExceptionHandled = true;    #E
    }
}

❶ ExceptionFilterAttribute is an abstract base class that implements IExceptionFilter.
ExceptionFilterAttribute 是实现IExceptionFilter 的抽象基类。

❷ There’s only a single method to override for IExceptionFilter.
只有一个方法可以覆盖 IExceptionFilter。

❸ Building a problem details object to return in the response
构建要在响应中返回的问题详细信息对象

❹ Creates an ObjectResult to serialize the ProblemDetails and to set the response status code
创建一个 ObjectResult 来序列化 ProblemDetails 并设置响应状态代码

❺ Marks the exception as handled to prevent it propagating into the middleware pipeline
将异常标记为已处理,以防止其传播到中间件管道中

It’s quite common to have an exception filter in your application if you are mixing API controllers and Razor Pages in your application, but they’re not always necessary. If you can handle all the exceptions in your application with a single piece of middleware, ditch the exception filters and go with that instead.
如果在应用程序中混合使用 API 控制器和 Razor Pages,则应用程序中的异常筛选器很常见,但它们并不总是必要的。如果可以使用单个中间件处理应用程序中的所有异常,请放弃异常筛选器,改用它。

You’re almost done refactoring your RecipeApiController. You have one more filter type to add: result filters. Custom result filters tend to be relatively rare in the apps I’ve written, but they have their uses, as you’ll see.
您几乎完成了 RecipeApiController 的重构。您还需要添加一种筛选条件类型:结果筛选条件。自定义结果过滤器在我编写的应用程序中往往相对较少,但正如您将看到的,它们有其用途。

22.1.5 Result filters: Customizing action results before they execute

22.1.5 结果过滤器:在执行作结果之前自定义作结果

If everything runs successfully in the pipeline, and there’s no short-circuiting, the next stage of the pipeline after action filters is result filters. These run before and after the IActionResult returned by the action method (or action filters) is executed.
如果管道中的所有内容都成功运行,并且没有短路,则作筛选器后管道的下一阶段是结果筛选器。这些作在执行作方法(或作筛选器)返回的 IActionResult 之前和之后运行。

Warning If the pipeline is short-circuited by setting context.Result, the result filter stage won’t run, but the IActionResult will still be executed to generate the response. The exceptions to this rule are action and page filters, which only short-circuit the action execution, as you saw in chapter 21. Result filters run as normal, as though the action or page handler itself generated the response.
警告:如果通过设置 context 使 pipeline 短路。Result,则结果筛选阶段不会运行,但仍会执行 IActionResult 以生成响应。此规则的例外情况是 action 和 page 过滤器,它们只会使 action 执行短路,如第 21 章所示。结果筛选器照常运行,就像作或页面处理程序本身生成响应一样。

Result filters run immediately after action filters, so many of their use cases are similar, but you typically use result filters to customize the way the IActionResult executes. For example, ASP.NET Core has several result filters built into its framework:
结果筛选条件在作筛选条件之后立即运行,因此它们的许多使用案例相似,但您通常使用结果筛选条件来自定义 IActionResult 的执行方式。例如,ASP.NET Core 的框架中内置了多个结果筛选器:

• ProducesAttribute—This forces a web API result to be serialized to a specific output format. For example, decorating your action method with [Produces ("application/xml")] forces the formatters to try to format the response as XML, even if the client doesn’t list XML in its Accept header.
ProducesAttribute - 强制将 Web API 结果序列化为特定输出格式。例如,使用 [Produces (“application/xml”)] 修饰作方法会强制格式化程序尝试将响应格式化为 XML,即使客户端未在其 Accept 标头中列出 XML。

• FormatFilterAttribute—Decorating an action method with this filter tells the formatter to look for a route value or query string parameter called format and to use that to determine the output format. For example, you could call /api/recipe/11?format=json and FormatFilter will format the response as JSON or call api/recipe/11?format=xml and get the response as XML.
FormatFilterAttribute - 使用此过滤器修饰作方法会告知格式化程序查找名为 format 的路由值或查询字符串参数,并使用它来确定输出格式。例如,您可以调用 /api/recipe/11?format=json,FormatFilter 会将响应格式化为 JSON,或者调用 api/recipe/11?format=xml 并获取 XML 形式的响应。

NOTE Remember that you need to explicitly configure the XML formatters if you want to serialize to XML, as described in chapter 20. For details on formatting results based on the URL, see my blog entry on the topic: http://mng.bz/1rYV.
注意:请记住,如果要序列化为 XML,则需要显式配置 XML 格式化程序,如第 20 章所述。有关基于 URL 设置结果格式的详细信息,请参阅我关于主题 http://mng.bz/1rYV 的博客文章。

As well as controlling the output formatters, you can use result filters to make any last-minute adjustments before IActionResult is executed and the response is generated.
除了控制输出格式化程序外,您还可以使用结果筛选器在执行 IActionResult 并生成响应之前进行任何最后一刻的调整。

As an example of the kind of flexibility available, in the following listing I demonstrate setting the LastModified header, based on the object returned from the action. This is a somewhat contrived example—it’s specific enough to a single action that it likely doesn’t warrant being moved to a result filter—but I hope you get the idea.
作为可用灵活性类型的一个示例,在下面的清单中,我演示了如何根据从作返回的对象设置 LastModified 标头。这是一个有点人为的示例 — 它对单个作足够具体,以至于它不一定需要移动到结果筛选器 — 但我希望您能理解。

Listing 22.9 Setting a response header in a result filter
清单 22.9 在结果过滤器中设置响应头

public class AddLastModifedHeaderAttribute : ResultFilterAttribute    #A
{
    public override void OnResultExecuting(     #B
        ResultExecutingContext context)         #B
    {
        if (context.Result is OkObjectResult result          #C
            && result.Value is RecipeDetailViewModel detail)    #D
        {
            var viewModelDate = detail.LastModified;            #E
            context.HttpContext.Response                        #E
              .GetTypedHeaders().LastModified = viewModelDate;  #E
        }
    }
}

❶ ResultFilterAttribute provides a useful base class you can override.
ResultFilterAttribute 提供了一个可以重写的有用基类。
❷ You could also override the Executed method, but the response would already be sent by then.
你也可以重写 Executed 方法,但那时响应已经发送了。
❸ Checks whether the action result returned a 200 Ok result with a view model.
检查作结果是否返回了视图模型的 200 Ok 结果。
❹ Checks whether the view model type is RecipeDetailViewModel . . .
检查视图模型类型是否为 RecipeDetailViewModel . . .
❺ . . . and if it is, fetches the LastModified property and sets the Last-Modified header in the response
. . . .如果是,则获取 LastModified 属性并在响应中设置 Last-Modified 标头

I’ve used another helper base class here, ResultFilterAttribute, so you need to override only a single method to implement the filter. Fetch the current IActionResult, exposed on context.Result, and check that it’s an OkObjectResult instance with a RecipeDetailViewModel value. If it is, fetch the LastModified field from the view model and add a Last-Modified header to the response.
我在这里使用了另一个帮助程序基类 ResultFilterAttribute,因此您只需重写一个方法即可实现筛选器。提取在上下文中公开的当前 IActionResult。Result,并检查它是否是具有 RecipeDetailViewModel 值的 OkObjectResult 实例。如果是,请从视图模型中提取 LastModified 字段,并将 Last-Modified 标头添加到响应中。

Tip GetTypedHeaders() is an extension method that provides strongly typed access to request and response headers. It takes care of parsing and formatting the values for you. You can find it in the Microsoft.AspNetCore.Http namespace.
提示GetTypedHeaders() 是一种扩展方法,它提供对请求和响应标头的强类型访问。它负责为您解析和格式化值。您可以在 Microsoft.AspNetCore.Http 命名空间中找到它。

As with resource and action filters, result filters can implement a method that runs after the result has executed: OnResultExecuted. You can use this method, for example, to inspect exceptions that happened during the execution of IActionResult.
与资源和作筛选器一样,结果筛选器可以实现在结果执行后运行的方法:OnResultExecuted。例如,您可以使用此方法检查在执行 IActionResult 期间发生的异常。

Warning Generally, you can’t modify the response in the OnResultExecuted method, as you may have already started streaming the response to the client.
警告:通常,您无法在 OnResultExecuted 方法中修改响应,因为您可能已经开始将响应流式传输到客户端。

We’ve finished simplifying the RecipeApiController now. By extracting various pieces of functionality to filters, the original controller in listing 22.1 has been simplified to the version in listing 22.2. This is obviously a somewhat extreme and contrived demonstration, and I’m not advocating that filters should always be your go-to option.
我们现在已经完成了 RecipeApiController 的简化。通过将各种功能提取到过滤器中,清单 22.1 中的原始控制器已简化为清单 22.2 中的版本。这显然是一个有点极端和做作的演示,我并不是提倡过滤器应该始终是您的首选。

Tip Filters should be a last resort in most cases. Where possible, it is often preferable to use a simple private method in a controller, or to push functionality into the domain instead of using filters. Filters should generally be used to extract repetitive, HTTP-related, or common cross-cutting code from your controllers.
提示:在大多数情况下,过滤器应该是最后的手段。在可能的情况下,通常最好在控制器中使用简单的私有方法,或者将功能推送到域中而不是使用过滤器。过滤器通常用于从控制器中提取重复的、与 HTTP 相关的或常见的横切代码。

There’s still one more filter we haven’t looked at yet, because it applies only to Razor Pages: page filters.
还有一个筛选器我们还没有查看,因为它仅适用于 Razor Pages:页面筛选器。

22.1.6 Page filters: Customizing model binding for Razor Pages

22.1.6 页面筛选器:自定义 Razor 页面的模型绑定

As already discussed, action filters apply only to controllers and actions; they have no effect on Razor Pages. Similarly, page filters have no effect on controllers and actions. Nevertheless, page filters and action filters fulfill similar roles.
如前所述,作筛选器仅适用于控制器和作;它们对 Razor Pages 没有影响。同样,页面过滤器对控制器和作也没有影响。不过,页面过滤器和作过滤器的作用相似。

As is the case for action filters, the ASP.NET Core framework includes several page filters out of the box. One of these is the Razor Page equivalent of the caching action filter, ResponseCacheFilter, called PageResponseCacheFilter. This works identically to the action-filter equivalent I described in section 22.1.3, setting HTTP caching headers on your Razor Page responses.
与作筛选器一样,ASP.NET Core 框架包含多个现成的页面筛选器。其中一个是缓存作筛选器的 Razor Page 等效项 ResponseCacheFilter,称为 PageResponseCacheFilter。这与我在第 22.1.3 节 在 Razor Page 响应上设置 HTTP 缓存标头中描述的等效作过滤器的工作原理相同。

Page filters are somewhat unusual, as they implement three methods, as discussed in section 22.1.2. In practice, I’ve rarely seen a page filter that implements all three. It’s unusual to need to run code immediately after page handler selection and before model validation. It’s far more common to perform a role directly analogous to action filters. The following listing shows a page filter equivalent to the EnsureRecipeExistsAttribute action filter.
页面过滤器有些不寻常,因为它们实现了三种方法,如 Section 22.1.2 中所述。在实践中,我很少见过实现所有这三个的页面过滤器。在选择页面处理程序之后和模型验证之前需要立即运行代码是不常见的。执行直接类似于作筛选器的角色更为常见。以下清单显示了与 EnsureRecipeExistsAttribute作筛选器等效的页面筛选器。

Listing 22.10 A page filter to check whether a Recipe exists
清单 22.10 用于检查 Recipe 是否存在的页面过滤器

public class PageEnsureRecipeExistsAttribute : Attribute, IPageFilter  #A
{
    public void OnPageHandlerSelected(          #B
        PageHandlerSelectedContext context)     #B
    {}                                          #B

    public void OnPageHandlerExecuting(         #C
        PageHandlerExecutingContext context)    #C
    {
        var service = context.HttpContext.RequestServices        #D
            .GetService<RecipeService>();  #D
        var recipeId = (int) context.HandlerArguments["id"];     #E
        if (!service.DoesRecipeExist(recipeId))        #F
        {
            context.Result = new NotFoundResult();   #G
        }
    }

    public void OnPageHandlerExecuted(        #H
        PageHandlerExecutedContext context)   #H
    { }                                       #H
}

❶ Implements IPageFilter and as an attribute so you can decorate the Razor Page PageModel
实现 IPageFilter 并作为属性,以便您可以装饰 Razor Page PageModel

❷ Executed after handler selection and before model binding—not used in this example
在处理程序选择之后和模型绑定之前执行 - 本例中未使用

❸ Executed after model binding and validation, and before page handler execution
在模型绑定和验证之后以及页面处理程序执行之前执行

❹ Fetches an instance of RecipeService from the DI container
从 DI 容器中获取 RecipeService 的实例

❺ Retrieves the id parameter that will be passed to the page handler method when it executes
检索 id 参数,该参数将在执行时传递给页面处理程序方法

❻ Checks whether a Recipe entity with the given RecipeId exists . . .
检查是否存在具有给定 RecipeId 的 Recipe 实体 . . .

❼ . . . and if it doesn’t exist, returns a 404 Not Found result and short-circuits the pipeline
. . . .如果不存在,则返回 404 Not Found 结果,并在页面处理程序执行(或短路)后将管道

❽ Executed after page handler execution (or short-circuiting)—not used in this example
Executed 短路 — 本例中未使用

The page filter is similar to the action filter equivalent. The most obvious difference is the need to implement three methods to satisfy the IPageFilter interface. You’ll commonly want to implement the OnPageHandlerExecuting method, which runs after model binding and validation, and before the page handler executes.
页面过滤器类似于等效的动作过滤器。最明显的区别是需要实现三种方法来满足 IPageFilter 接口。您通常需要实现 OnPageHandlerExecuting 方法,该方法在模型绑定和验证之后、页面处理程序执行之前运行。

A subtle difference between the action filter code and the page filter code is that the action filter accesses the model-bound action arguments using context.ActionArguments. The page filter uses context.HandlerArguments in the example, but there’s also another option.
作筛选条件代码和页面筛选条件代码之间的细微差别是,作筛选条件使用上下文访问模型绑定的作参数。ActionArguments 的 API 参数。页面过滤器使用 context。HandlerArguments 的 HandlerArguments 进行匹配,但还有另一个选项。

Remember from chapter 16 that Razor Pages often bind to public properties on the PageModel using the [BindProperty] attribute. You can access those properties directly instead of using magic strings by casting a HandlerInstance property to the correct PageModel type and accessing the property directly, as in this example:
请记住,在第 16 章中,Razor Pages 通常使用 [BindProperty] 属性绑定到 PageModel 上的公共属性。你可以通过将 HandlerInstance 属性强制转换为正确的 PageModel 类型并直接访问该属性,直接访问这些属性,而不是使用魔术字符串,如下例所示:

var recipeId = ((ViewRecipePageModel)context.HandlerInstance).Id

This is similar to the way the ControllerBase class implements IActionFilter and PageModel implements IPageFilter and IAsyncPageFilterT. If you want to create an action filter for a single Razor Page, you could save yourself the trouble of creating a separate page filter and override these methods directly in your Razor Page.
这类似于 ControllerBase 类实现 IActionFilter 和 PageModel 实现 IPageFilter 和 IAsyncPageFilterT 的方式。如果要为单个 Razor 页面创建作筛选器,则可以省去创建单独页面筛选器的麻烦,并直接在 Razor 页面中重写这些方法。

Tip I generally find it’s not worth the hassle of using page filters unless you have a common requirement. The extra level of indirection that page filters add, coupled with the typically bespoke nature of individual Razor Pages, means that I normally find they aren’t worth using. Your mileage may vary, of course, but don’t jump to them as a first option.
提示:我通常发现不值得使用页面过滤器的麻烦,除非你有共同的要求。页面过滤器添加的额外间接级别,再加上单个 Razor 页面的典型定制性质,意味着我通常会发现它们不值得使用。当然,您的里程可能会有所不同,但不要将它们作为首选。

That brings us to the end of this detailed look at each of the filters in the MVC pipeline. Looking back and comparing listings 22.1 and 22.2, you can see filters allowed us to refactor the controllers and make the intent of each action method much clearer. Writing your code in this way makes it easier to reason about, as each filter and action has a single responsibility.
这样,我们就结束了对 MVC 管道中每个过滤器的详细介绍。回顾并比较清单 22.1 和 22.2,您可以看到过滤器允许我们重构控制器,并使每个作方法的意图更加清晰。以这种方式编写代码可以更轻松地进行推理,因为每个过滤器和作都有单一的责任。

In the next section we’ll take a slight detour into exactly what happens when you short-circuit a filter. I’ve described how to do this, by setting the context.Result property on a filter, but I haven’t described exactly what happens. For example, what if there are multiple filters in the stage when it’s short-circuited? Do those still run?
在下一节中,我们将稍微绕道介绍一下当 filter 短路时会发生什么。我已经介绍了如何通过设置上下文来执行此作。Result 属性,但我还没有具体描述会发生什么。例如,如果 stage 中有多个 filter 短路怎么办?那些还在运行吗?

22.2 Understanding pipeline short-circuiting

22.2 了解管道短路

In this short section you’ll learn about the details of filter-pipeline short-circuiting. You’ll see what happens to the other filters in a stage when the pipeline is short-circuited and how to short-circuit each type of filter.
在这个简短的部分中,您将了解 filter-pipeline 短路的详细信息。您将看到当管道短路时,某个阶段中的其他过滤器会发生什么情况,以及如何使每种类型的过滤器短路。

A brief warning: the topic of filter short-circuiting can be a little confusing. Unlike middleware short-circuiting, which is cut-and-dried, the filter pipeline is a bit more nuanced. Luckily, you won’t often need to dig into it, but when you do, you’ll be glad for the detail.
一个简短的警告:滤波器短路的话题可能有点令人困惑。与中间件短路不同,筛选器管道更加微妙。幸运的是,您通常不需要深入研究它,但当您这样做时,您会为细节感到高兴。

You short-circuit the authorization, resource, action, page, and result filters by setting context.Result to IActionResult. Setting an action result in this way causes some or all of the remaining pipeline to be bypassed. But the filter pipeline isn’t entirely linear, as you saw in chapter 21, so short-circuiting doesn’t always do an about-face back down the pipeline. For example, short-circuited action filters bypass only action method execution; the result filters and result execution stages still run.
您可以通过设置 context 来短路 authorization、resource、action、page 和 result 过滤器。Result 设置为 IActionResult。以这种方式设置作结果会导致绕过部分或全部剩余管道。但是 filter pipeline 并不是完全线性的,正如你在第 21 章中看到的那样,所以短路并不总是在管道上做一个反转。例如,短路的动作过滤器仅绕过动作方法的执行;结果筛选条件和结果执行阶段仍在运行。

The other difficultly is what happens if you have more than one filter in a stage. Let’s say you have three resource filters executing in a pipeline. What happens if the second filter causes a short circuit? Any remaining filters are bypassed, but the first resource filter has already run its Executing command, as shown in figure 22.2. This earlier filter gets to run its Executed command too, with context.Cancelled = true, indicating that a filter in that stage (the resource filter stage) short-circuited the pipeline.
另一个困难是在一个阶段中有多个过滤器时会发生什么。假设您在一个管道中执行了三个资源筛选器。如果第二个滤波器导致短路怎么办?任何剩余的过滤器都将被绕过,但第一个资源过滤器已经运行了其 Executing 命令,如图 22.2 所示。这个前面的过滤器也可以运行它的 Executed 命令,其中包含 context。Cancelled = true,指示该阶段(资源筛选器阶段)中的筛选器使管道短路。

alt text

Figure 22.2 The effect of short-circuiting a resource filter on other resource filters in that stage. Later filters in the stage won’t run at all, but earlier filters run their OnResourceExecuted function.
图 22.2 在该阶段中,将资源过滤器短路对其他资源过滤器的影响。阶段中较晚的筛选器根本不会运行,但较早的筛选器会运行其 OnResourceExecuted 函数。

Running result filters after short-circuits with IAlwaysRunResultFilter
使用 IAlwaysRunResultFilter

Result filters are designed to wrap the execution of an IActionResult returned by an action method or action filter so that you can customize how the action result is executed. However, this customization doesn’t apply to the IActionResult set when you short-circuit the filter pipeline by setting context.Result in an authorization filter, resource filter, or exception filter.
在短路后运行结果筛选器 结果筛选器旨在包装作方法或作筛选器返回的 IActionResult 的执行,以便您可以自定义作结果的执行方式。但是,当您通过设置 context 使筛选器管道短路时,此自定义不适用于 IActionResult 集。生成授权筛选条件、资源筛选条件或异常筛选条件。

That’s often not a problem, as many result filters are designed to handle “happy path” transformations. But sometimes you want to make sure that a transformation is always applied to an IActionResult, regardless of whether it was returned by an action method or a short-circuiting filter.
这通常不是问题,因为许多结果筛选器都旨在处理 “happy path” 转换。但有时你希望确保转换始终应用于 IActionResult,而不管它是由作方法还是短路筛选器返回的。

For those cases, you can implement IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter. These interfaces extend (and are identical) to the standard result filter interfaces, so they run like normal result filters in the filter pipeline. But these interfaces mark the filter to also run after an authorization filter, resource filter, or exception filter short-circuits the pipeline, where standard result filters won’t run.
对于这些情况,您可以实现 IAlwaysRunResultFilter 或 IAsyncAlwaysRunResultFilter。这些接口扩展(并且相同)到标准结果筛选器接口,因此它们像筛选器管道中的普通结果筛选器一样运行。但是,这些接口将筛选器标记为在授权筛选器、资源筛选器或异常筛选器使管道短路后也运行,其中标准结果筛选器不会运行。

You can use IAlwaysRunResultFilter to ensure that certain action results are always updated. For example, the documentation shows how to use an IAlwaysRunResultFilter to convert a 415 StatusCodeResult to a 422 StatusCodeResult, regardless of the source of the action result. See the “IAlwaysRunResultFilter and IAsyncAlwaysRunResultFilter” section of Microsoft’s “Filters in ASP.NET Core” documentation: http://mng.bz/JDo0.
您可以使用 IAlwaysRunResultFilter 来确保某些作结果始终更新。例如,该文档显示了如何使用 IAlwaysRunResultFilter 将 415 StatusCodeResult 转换为 422 StatusCodeResult,而不管作结果的来源如何。请参阅 Microsoft 的“ASP.NET Core 中的筛选器”文档的“IAlwaysRunResultFilter 和 IAsyncAlwaysRunResultFilter”部分:http://mng.bz/JDo0

Understanding which other filters run when you short-circuit a filter can be somewhat of a chore, but I’ve summarized each filter in table 22.1. You’ll also find it useful to refer to the pipeline diagrams in chapter 21 to visualize the shape of the pipeline when thinking about short circuits.
了解在使 filter 短路时运行哪些其他 filters 可能有点麻烦,但我在表 22.1 中总结了每个 filter 。在考虑短路时,您还会发现参考第 21 章中的流水线图以可视化流水线的形状很有用。

Table 22.1 The effect of short-circuiting filters on filter-pipeline execution
表 22.1 短路 filters 对 filter-pipeline 执行的影响

Filter type How to short-circuit? What else runs?
Authorization filters Set context.Result. Runs only IAlwaysRunResultFilters.
Resource filters Set context.Result. Resource-filter *Executed functions from earlier filters run with context.Cancelled = true. Runs IAlwaysRunResultFilters before executing the IActionResult.
Action filters Set context.Result. Bypasses only action method execution. Action filters earlier in the pipeline run their Executed methods with context.Cancelled = true, then result filters, result execution, and resource filters’ Executed methods all run as normal.
Page filters Set context.Result in OnPageHandlerSelected. Bypasses only page handler execution. Page filters earlier in the pipeline run their Executed methods with context.Cancelled = true, then result filters, result execution, and resource filters’ Executed methods all run as normal.
Exception filters Set context.Result and Exception.Handled = true. All resource-filter *Executed functions run. Runs IAlwaysRunResultFilters before executing the IActionResult.
Result filters Set context.Cancelled = true. Result filters earlier in the pipeline run their Executed functions with context.Cancelled = true. All resource-filter Executed functions run as normal.

The most interesting point here is that short-circuiting an action filter (or a page filter) doesn’t short-circuit much of the pipeline at all. In fact, it bypasses only later action filters and the action method execution itself. By building primarily action filters, you can ensure that other filters, such as result filters that define the output format, run as usual, even when your action filters short-circuit.
这里最有趣的一点是,短路作筛选器(或页面筛选器)根本不会使管道的大部分短路。事实上,它只绕过后面的作筛选器和作方法执行本身。通过主要构建作筛选器,您可以确保其他筛选器(例如定义输出格式的结果筛选器)照常运行,即使作筛选器短路时也是如此。

The last thing I’d like to talk about in this chapter is how to use DI with your filters. You saw in chapters 8 and 9 that DI is integral to ASP.NET Core, and in the next section you’ll see how to design your filters so that the framework can inject service dependencies into them for you.
本章我想谈的最后一件事是如何将 DI 与你的过滤器一起使用。您在第 8 章和第 9 章中看到了 DI 是 ASP.NET Core 不可或缺的一部分,在下一节中,您将了解如何设计过滤器,以便框架可以为您注入服务依赖项。

22.3 Using dependency injection with filter attributes

22.3 将依赖项注入与 filter 属性一起使用

In this section you’ll learn how to inject services into your filters so you can take advantage of the simplicity of DI in your filters. You’ll learn to use two helper filters to achieve this, TypeFilterAttribute and ServiceFilterAttribute, and you’ll see how they can be used to simplify the action filter you defined in section 22.1.3.
在本节中,您将学习如何将服务注入过滤器,以便您可以在过滤器中利用 DI 的简单性。您将学习使用两个辅助过滤器来实现此目的,TypeFilterAttribute和ServiceFilterAttribute,并且您将了解如何使用它们来简化您在第 22.1.3 节中定义的作过滤器。

The filters we’ve created so far have been created as attributes. This is useful for applying filters to action methods and controllers, but it means you can’t use DI to inject services into the constructor. C# attributes don’t let you pass dependencies into their constructors (other than constant values), and they’re created as singletons, so there’s only a single instance of an attribute for the lifetime of your app. So what happens if you need to access a transient or scoped service from inside the singleton attribute?
到目前为止,我们创建的过滤器已创建为 attributes。这对于将筛选器应用于作方法和控制器非常有用,但这意味着您不能使用 DI 将服务注入构造函数。C# 属性不允许将依赖项传递到其构造函数中(常量值除外),并且它们被创建为单一实例,因此在应用的生命周期内只有一个属性实例。那么,如果您需要从 singleton 属性内部访问临时或范围服务,会发生什么情况呢?

Listing 22.6 showed one way of doing this, using a pseudo-service locator pattern to reach into the DI container and pluck out RecipeService at runtime. This works but is generally frowned upon as a pattern in favor of proper DI. So how can you add DI to your filters?
清单 22.6 展示了一种实现此目的的方法,使用伪服务定位器模式进入 DI 容器并在运行时提取 RecipeService。这有效,但通常不被看作是一种支持适当 DI 的模式。那么如何将 DI 添加到过滤器中呢?

The key is to split the filter in two. Instead of creating a class that’s both an attribute and a filter, create a filter class that contains the functionality and an attribute that tells the framework when and where to use the filter.
关键是将过滤器一分为二。不要创建一个既是属性又是筛选器的类,而应创建一个包含功能和属性的筛选器类,该类告诉框架何时何地使用筛选器。

Let’s apply this to the action filter from listing 22.6. Previously, I derived from ActionFilterAttribute and obtained an instance of RecipeService from the context passed to the method. In the following listing I show two classes, EnsureRecipeExistsFilter and EnsureRecipeExistsAttribute. The filter class is responsible for the functionality and takes in RecipeService as a constructor dependency.
让我们将其应用于清单 22.6 中的 action filter。以前,我从 ActionFilterAttribute 派生,并从传递给该方法的上下文中获取 RecipeService 的实例。在下面的清单中,我显示了两个类,EnsureRecipeExistsFilter 和 EnsureRecipeExistsAttribute。filter 类负责功能,并将 RecipeService 作为构造函数依赖项。

Listing 22.11 Using DI in a filter by not deriving from Attribute
清单 22.11 在过滤器中使用 DI 而不是从 Attribute 派生

public class EnsureRecipeExistsFilter : IActionFilter    #A
{
    private readonly RecipeService _service;                #B
    public EnsureRecipeExistsFilter(RecipeService service)  #B
    {                                                       #B
        _service = service;                                 #B
    }                                                       #B
    public void OnActionExecuting(ActionExecutingContext context)  #C
    {                                                              #C
        var recipeId = (int) context.ActionArguments["id"];        #C
        if (!_service.DoesRecipeExist(recipeId))                   #C
        {                                                          #C
            context.Result = new NotFoundResult();                 #C
        }                                                          #C
    }                                                              #C

    public void OnActionExecuted(ActionExecutedContext context) { }   #D
}

public class EnsureRecipeExistsAttribute : TypeFilterAttribute    #E
{
    public EnsureRecipeExistsAttribute()               #F
        : base(typeof(EnsureRecipeExistsFilter)) {}    #F
}

❶ Doesn’t derive from an Attribute class
不从 Attribute 类派生
❷ RecipeService is injected into the constructor.
RecipeService 被注入到构造函数中。
❸ The rest of the method remains the same.
方法的其余部分保持不变。
❹ You must implement the Executed action to satisfy the interface.
您必须实现 Executed作才能满足接口。
❺ Derives from TypeFilter, which is used to fill dependencies using the DI container
派生自 TypeFilter,用于使用 DI 容器填充依赖项
❻ Passes the type EnsureRecipeExistsFilter as an argument to the base TypeFilter constructor
将类型 EnsureRecipeExistsFilter 作为参数传递给基本 TypeFilter 构造函数

EnsureRecipeExistsFilter is a valid filter; you could use it on its own by adding it as a global filter (as global filters don’t need to be attributes). But you can’t use it directly by decorating controller classes and action methods, as it’s not an attribute. That’s where EnsureRecipeExistsAttribute comes in.
EnsureRecipeExistsFilter 是有效的筛选器;您可以通过将其添加为全局过滤器来单独使用它(因为全局过滤器不需要是属性)。但是你不能通过装饰控制器类和作方法来直接使用它,因为它不是一个属性。这就是 EnsureRecipeExistsAttribute 的用武之地。

You can decorate your methods with EnsureRecipeExistsAttribute instead. This attribute inherits from TypeFilterAttribute and passes the Type of filter to create as an argument to the base constructor. This attribute acts as a factory for EnsureRecipeExistsFilter by implementing IFilterFactory.
您可以改用 EnsureRecipeExistsAttribute 来修饰您的方法。此属性继承自 TypeFilterAttribute,并将要创建的过滤器的 Type 作为参数传递给基本构造函数。此属性通过实现 IFilterFactory 充当 EnsureRecipeExistsFilter 的工厂。

When ASP.NET Core initially loads your app, it scans your actions and controllers, looking for filters and filter factories. It uses these to form a filter pipeline for every action in your app, as shown in figure 22.3.
当 ASP.NET Core 最初加载您的应用程序时,它会扫描您的作和控制器,查找过滤器和过滤器工厂。它使用这些数据为应用程序中的每个作形成一个 filter pipeline ,如图 22.3 所示。

alt text

Figure 22.3 The framework scans your app on startup to find both filters and attributes that implement IFilterFactory. At runtime, the framework calls CreateInstance() to get an instance of the filter
图 22.3 框架在启动时扫描您的应用程序,以查找实现 IFilterFactory 的过滤器和属性。在运行时,框架调用 CreateInstance() 来获取过滤器的实例

When an action decorated with EnsureRecipeExistsAttribute is called, the framework calls CreateInstance() on the IFilterFactory attribute. This creates a new instance of EnsureRecipeExistsFilter and uses the DI container to populate its dependencies (RecipeService).
调用使用 EnsureRecipeExistsAttribute 修饰的作时,框架将对 IFilterFactory 属性调用 CreateInstance()。这将创建一个新的 EnsureRecipeExistsFilter 实例,并使用 DI 容器填充其依赖项 (RecipeService)。

By using this IFilterFactory approach, you get the best of both worlds: you can decorate your controllers and actions with attributes, and you can use DI in your filters. Out of the box, two similar classes provide this functionality, which have slightly different behaviors:
通过使用这种 IFilterFactory 方法,您可以两全其美:您可以使用属性装饰控制器和作,并且可以在过滤器中使用 DI。开箱即用,两个类似的类提供了此功能,它们的行为略有不同:

• TypeFilterAttribute—Loads all the filter’s dependencies from the DI container and uses them to create a new instance of the filter.
TypeFilterAttribute - 从 DI 容器中加载所有筛选器的依赖项,并使用它们创建筛选器的新实例。

• ServiceFilterAttribute—Loads the filter itself from the DI container. The DI container takes care of the service lifetime and building the dependency graph. Unfortunately, you must also explicitly register your filter with the DI container:
ServiceFilterAttribute - 从 DI 容器加载筛选器本身。DI 容器负责服务生命周期并构建依赖项关系图。遗憾的是,您还必须向 DI 容器显式注册过滤器:

builder.Services.AddTransient<EnsureRecipeExistsFilter>();

Tip You can register your services with any lifetime you choose. If your service is registered as a singleton, you can consider setting the IsReusable flag, as described in the documentation: http://mng.bz/d1JD.
提示您可以使用您选择的任何生命周期来注册您的服务。如果您的服务注册为单一实例,则可以考虑设置 IsReusable 标志,如文档中所述:http://mng.bz/d1JD

If you choose to use ServiceFilterAttribute instead of TypeFilterAttribute, and register the EnsureRecipeExistsFilter as a service in the DI container, you can apply the ServiceFilterAttribute directly to an action method:
如果您选择使用 ServiceFilterAttribute 而不是 TypeFilterAttribute,并在 DI 容器中将 EnsureRecipeExistsFilter 注册为服务,则可以将 ServiceFilterAttribute 直接应用于作方法:

[ServiceFilter(typeof(EnsureRecipeExistsFilter))]
public IActionResult Index() => Ok();

Whether you choose to use TypeFilterAttribute or ServiceFilterAttribute is somewhat a matter of preference, and you can always implement a custom IFilterFactory if you need to. The key takeaway is that you can now use DI in your filters. If you don’t need to use DI for a filter, implement it as an attribute directly, for simplicity.
选择使用 TypeFilterAttribute 还是 ServiceFilterAttribute 在某种程度上是一个首选项问题,如果需要,您始终可以实现自定义 IFilterFactory。关键要点是您现在可以在过滤器中使用 DI。如果您不需要将 DI 用于过滤器,请直接将其作为属性实现,以便简单起见。

Tip I like to create my filters as a nested class of the attribute class when using this pattern. This keeps all the code nicely contained in a single file and indicates the relationship between the classes.
提示:使用此模式时,我喜欢将过滤器创建为 attribute 类的嵌套类。这将使所有代码很好地包含在单个文件中,并指示类之间的关系。

That brings us to the end of this chapter on the filter pipeline. Filters are a somewhat advanced topic, in that they aren’t strictly necessary for building basic apps, but I find them extremely useful for ensuring that my controller and action methods are simple and easy to understand.
这将我们带到了本章关于过滤器管道的结尾。筛选器是一个比较高级的主题,因为它们对于构建基本应用程序并不是绝对必要的,但我发现它们对于确保我的控制器和作方法简单易懂非常有用。

In the next chapter we’ll take our first look at securing your app. We’ll discuss the difference between authentication and authorization, the concept of identity in ASP.NET Core, and how you can use the ASP.NET Core Identity system to let users register and log in to your app.
在下一章中,我们将首先了解如何保护您的应用程序。我们将讨论身份验证和授权之间的区别、ASP.NET Core 中的身份概念,以及如何使用 ASP.NET Core Identity 系统让用户注册和登录您的应用。

22.4 Summary

22.4 总结

The filter pipeline executes as part of the MVC or Razor Pages execution. It consists of authorization filters, resource filters, action filters, page filters, exception filters, and result filters.
筛选器管道作为 MVC 或 Razor Pages 执行的一部分执行。它由授权筛选条件、资源筛选条件、作筛选条件、页面筛选条件、异常筛选条件和结果筛选条件组成。

ASP.NET Core includes many built-in filters, but you can also create custom filters tailored to your application. You can use custom filters to extract common cross-cutting functionality out of your MVC controllers and Razor Pages, reducing duplication and ensuring consistency across your endpoints.
ASP.NET Core 包含许多内置筛选器,但您也可以创建针对您的应用程序定制的自定义筛选器。您可以使用自定义筛选器从 MVC 控制器和 Razor 页面中提取常见的横切功能,从而减少重复并确保端点之间的一致性。

Authorization filters run first in the pipeline and control access to APIs. ASP.NET Core includes an [Authorization] attribute that you can apply to action methods so that only logged-in users can execute the action.
授权过滤器首先在管道中运行并控制对 API 的访问。ASP.NET Core 包含一个 [Authorization] 属性,您可以将其应用于作方法,以便只有登录用户才能执行该作。

Resource filters run after authorization filters and again after an IActionResult has been executed. They can be used to short-circuit the pipeline so that an action method is never executed. They can also be used to customize the model-binding process for an action method.
资源筛选条件在授权筛选条件之后运行,并在执行 IActionResult 后再次运行。它们可用于使管道短路,以便永远不会执行作方法。它们还可用于自定义作方法的模型绑定过程。

Action filters run after model binding has occurred and before an action method executes. They also run after the action method has executed. They can be used to extract common code out of an action method to prevent duplication. They don’t execute for Razor Pages, only for MVC controllers.
作筛选器在模型绑定发生之后和作方法执行之前运行。它们还会在执行作方法后运行。它们可用于从 action method 中提取公共代码,以防止重复。它们不为 Razor Pages 执行,只为 MVC 控制器执行。

The ControllerBase base class also implements IActionFilter and IAsyncActionFilter. They run at the start and end of the action filter pipeline, regardless of the ordering or scope of other action filters. They can be used to create action filters that are specific to one controller.
ControllerBase 基类还实现 IActionFilter 和 IAsyncActionFilter。它们在作筛选器管道的开头和结尾运行,而不管其他作筛选器的顺序或范围如何。它们可用于创建特定于一个控制器的作筛选器。

Page filters run three times: after page handler selection, after model binding, and after the page handler method executes. You can use page filters for similar purposes as action filters. Page filters execute only for Razor Pages; they don’t run for MVC controllers.
页面过滤器运行三次:选择页面处理程序后、模型绑定后和页面处理程序方法执行后。您可以将页面过滤器用于与作过滤器类似的目的。页面筛选器仅对 Razor Pages 执行;它们不为 MVC 控制器运行。

Razor Page PageModels implement IPageFilter and IAsyncPageFilter, so they can be used to implement page-specific page filters. These are rarely used, as you can typically achieve similar results with simple private methods.
Razor Page PageModel 实现 IPageFilter 和 IAsyncPageFilter,因此它们可用于实现特定于页面的页面筛选器。这些方法很少使用,因为通常可以使用简单的私有方法获得类似的结果。

Exception filters execute after action and page filters, when an action method or page handler has thrown an exception. They can be used to provide custom error handling specific to the action executed.
当作方法或页面处理程序引发异常时,异常筛选器在作和页面筛选器之后执行。它们可用于提供特定于所执行作的自定义错误处理。

Generally, you should handle exceptions at the middleware level, but you can use exception filters to customize how you handle exceptions for specific actions, controllers, or Razor Pages.
通常,您应该在中间件级别处理异常,但您可以使用异常筛选器来自定义处理特定作、控制器或 Razor Pages 的异常的方式。

Result filters run before and after an IActionResult is executed. You can use them to control how the action result is executed or to completely change the action result that will be executed.
结果筛选器在执行 IActionResult 之前和之后运行。您可以使用它们来控制作结果的执行方式,或完全更改将要执行的作结果。

All filters can short-circuit the pipeline by setting a response. This generally prevents the request progressing further in the filter pipeline, but the exact behavior varies with the type of filter that is short-circuited.
所有过滤器都可以通过设置响应来短路管道。这通常可以防止请求在筛选条件管道中进一步进行,但具体行为会因短路的筛选条件类型而异。

Result filters aren’t executed when you short-circuit the pipeline using authorization, resource, or exception filters. You can ensure that result filters also run for these short-circuit cases by implementing a result filter as IAlwaysRunResultFilter or IAsyncAlwaysRunResultFilter.
当您使用授权、资源或异常筛选条件对管道进行短路时,不会执行结果筛选条件。您可以通过将结果筛选器实现为 IAlwaysRunResultFilter 或 IAsyncAlwaysRunResultFilter 来确保结果筛选器也针对这些短路情况运行。

You can use ServiceFilterAttribute and TypeFilterAttribute to allow dependency injection in your custom filters. ServiceFilterAttribute requires that you register your filter and all its dependencies with the DI container, whereas TypeFilterAttribute requires only that the filter’s dependencies have been registered.
您可以使用 ServiceFilterAttribute 和 TypeFilterAttribute 在自定义筛选条件中允许依赖项注入。ServiceFilterAttribute 要求您向 DI 容器注册过滤器及其所有依赖项,而 TypeFilterAttribute 仅要求已注册过滤器的依赖项。