Integrowanie aplikacji OWIN z Microsoft. Identity.Web

Dodaj nowoczesne uwierzytelnianie do aplikacji ASP.NET MVC i Web API w .NET Framework 4.7.2 lub nowszym, korzystając z pakietu Microsoft.Identity.Web.OWIN.

Omówienie integracji OWIN

Microsoft.Identity.Web.OWIN zapewnia możliwości Microsoft.Identity.Web dla aplikacji ASP.NET MVC i aplikacji Web API, które korzystają z oprogramowania pośredniczącego OWIN oraz Microsoft Entra ID.

Zapoznaj się z kluczowymi korzyściami

Poniższa tabela zawiera podsumowanie najważniejszych funkcji i korzyści pakietu.

Funkcja Korzyść
TokenAcquirerFactory Automatyczne pozyskiwanie tokenów za pomocą buforowania
Rozszerzenia kontrolera Łatwy dostęp do GraphServiceClient i IDownstreamApi
Rozproszona pamięć podręczna tokenów Wbudowana obsługa SQL Server, Redis, Cosmos DB, PostgreSQL
Automatyczne odświeżanie tokenu Obsługuje przezroczyste odświeżanie tokenu
Zgoda przyrostowa Bezproblemowa integracja przepływu zgody

Przegląd obsługiwanych scenariuszy

Microsoft.Identity.Web.OWIN obsługuje następujące typy aplikacji i scenariusze.

  • ASP.NET MVC Web Applications (.NET Framework 4.7.2+)
  • ASP.NET Web API (.NET Framework 4.7.2+)
  • Aplikacje hybrydowe (MVC i internetowy interfejs API)
  • Calling Microsoft Graph z kontrolerów
  • Wywoływanie podrzędnych interfejsów API z automatycznym uwierzytelnianiem

Instalowanie pakietu

Zainstaluj pakiet Microsoft.Identity.Web.OWIN NuGet przy użyciu preferowanej przez Ciebie metody.

Uruchom następujące polecenie w konsoli Menedżer pakietów:

Install-Package Microsoft.Identity.Web.OWIN

Alternatywnie uruchom następujące polecenie używając .NET CLI:

dotnet add package Microsoft.Identity.Web.OWIN

Zależności są automatycznie uwzględniane:

  • Microsoft.Identity.Web.TokenAcquisition
  • Microsoft. Identity.Web.TokenCache
  • Microsoft.Owin
  • System.web

Konfigurowanie aplikacji

Skonfiguruj ustawienia aplikacji, aby łączyć się z interfejsami API Microsoft Entra i podrzędnymi.

Konfigurowanie Web.config

Dodaj do pliku Web.config następujące ustawienia Microsoft Entra i ustawienia interfejsu API downstream.

<configuration>
  <appSettings>
    <!-- Microsoft Entra ID Configuration -->
    <add key="AzureAd:Instance" value="https://login.microsoftonline.com/" />
    <add key="AzureAd:TenantId" value="your-tenant-id" />
    <add key="AzureAd:ClientId" value="your-client-id" />
    <add key="AzureAd:ClientSecret" value="your-client-secret" />
    <add key="AzureAd:RedirectUri" value="https://localhost:44368/" />
    <add key="AzureAd:PostLogoutRedirectUri" value="https://localhost:44368/" />

    <!-- Microsoft Graph Configuration -->
    <add key="DownstreamApi:MicrosoftGraph:BaseUrl" value="https://graph.microsoft.com/v1.0" />
    <add key="DownstreamApi:MicrosoftGraph:Scopes" value="user.read" />

    <!-- Custom Downstream API Configuration -->
    <add key="DownstreamApi:TodoListService:BaseUrl" value="https://localhost:44351" />
    <add key="DownstreamApi:TodoListService:Scopes" value="api://todo-api-client-id/.default" />
  </appSettings>

  <connectionStrings>
    <!-- Optional: SQL Server Token Cache -->
    <add name="TokenCache"
         connectionString="Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=TokenCache;Integrated Security=True;" />
  </connectionStrings>
</configuration>

Konfigurowanie appsettings.json (alternatywa)

Ustawienia można również przechowywać w appsettings.json pliku, jak pokazano w poniższym przykładzie.

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "TenantId": "your-tenant-id",
    "ClientId": "your-client-id",
    "ClientSecret": "your-client-secret",
    "RedirectUri": "https://localhost:44368/",
    "PostLogoutRedirectUri": "https://localhost:44368/"
  },
  "DownstreamApi": {
    "MicrosoftGraph": {
      "BaseUrl": "https://graph.microsoft.com/v1.0",
      "Scopes": "user.read"
    },
    "TodoListService": {
      "BaseUrl": "https://localhost:44351",
      "Scopes": "api://todo-api-client-id/.default"
    }
  }
}

Konfigurowanie klasy uruchamiania

Zarejestruj oprogramowanie pośredniczące uwierzytelniania, pozyskiwanie tokenów i usługi podrzędnego interfejsu API w klasie startowej.

Konfigurowanie App_Start/Startup.Auth.cs

Poniższy kod przedstawia pełną konfigurację z Microsoft. Identity.Web.OWIN:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Microsoft.Identity.Web.TokenCacheProviders.Distributed;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Configuration;
using System.Web;

namespace MyMvcApp
{
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            // Set default authentication type
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            // Configure cookie authentication
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                CookieName = "MyApp.Auth",
                ExpireTimeSpan = TimeSpan.FromHours(1),
                SlidingExpiration = true
            });

            // Configure OpenID Connect authentication
            app.UseOpenIdConnectAuthentication(
                new OpenIdConnectAuthenticationOptions
                {
                    ClientId = ConfigurationManager.AppSettings["AzureAd:ClientId"],
                    Authority = $"https://login.microsoftonline.com/{ConfigurationManager.AppSettings["AzureAd:TenantId"]}",
                    RedirectUri = ConfigurationManager.AppSettings["AzureAd:RedirectUri"],
                    PostLogoutRedirectUri = ConfigurationManager.AppSettings["AzureAd:PostLogoutRedirectUri"],

                    Scope = "openid profile email offline_access",
                    ResponseType = "code id_token",

                    TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        NameClaimType = "preferred_username"
                    },

                    Notifications = new OpenIdConnectAuthenticationNotifications
                    {
                        AuthenticationFailed = context =>
                        {
                            context.HandleResponse();
                            context.Response.Redirect("/Error?message=" + context.Exception.Message);
                            return Task.FromResult(0);
                        }
                    }
                });

            // Configure Microsoft Identity Web services
            var services = CreateOwinServiceCollection();

            // Add token acquisition
            services.AddTokenAcquisition();

            // Add Microsoft Graph support
            services.AddMicrosoftGraph();

            // Add downstream API support
            services.AddDownstreamApi("MicrosoftGraph", services.BuildServiceProvider()
                .GetRequiredService<IConfiguration>().GetSection("DownstreamApi:MicrosoftGraph"));

            services.AddDownstreamApi("TodoListService", services.BuildServiceProvider()
                .GetRequiredService<IConfiguration>().GetSection("DownstreamApi:TodoListService"));

            // Configure token cache (choose one option)
            ConfigureTokenCache(services);

            // Build service provider
            var serviceProvider = services.BuildServiceProvider();

            // Create and register token acquirer factory
            var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
            tokenAcquirerFactory.Build(serviceProvider);

            // Add OWIN token acquisition middleware
            app.Use<OwinTokenAcquisitionMiddleware>(tokenAcquirerFactory);
        }

        private IServiceCollection CreateOwinServiceCollection()
        {
            var services = new ServiceCollection();

            // Add configuration from appsettings.json and/or Web.config
            IConfiguration configuration = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", optional: true)
                .AddInMemoryCollection(new Dictionary<string, string>
                {
                    ["AzureAd:Instance"] = ConfigurationManager.AppSettings["AzureAd:Instance"],
                    ["AzureAd:TenantId"] = ConfigurationManager.AppSettings["AzureAd:TenantId"],
                    ["AzureAd:ClientId"] = ConfigurationManager.AppSettings["AzureAd:ClientId"],
                    ["AzureAd:ClientSecret"] = ConfigurationManager.AppSettings["AzureAd:ClientSecret"],
                    ["DownstreamApi:MicrosoftGraph:BaseUrl"] = ConfigurationManager.AppSettings["DownstreamApi:MicrosoftGraph:BaseUrl"],
                    ["DownstreamApi:MicrosoftGraph:Scopes"] = ConfigurationManager.AppSettings["DownstreamApi:MicrosoftGraph:Scopes"],
                })
                .Build();

            services.AddSingleton(configuration);

            return services;
        }

        private void ConfigureTokenCache(IServiceCollection services)
        {
            // Option 1: In-memory cache (development)
            services.AddDistributedTokenCaches(cacheServices =>
            {
                cacheServices.AddDistributedMemoryCache();
            });

            // Option 2: SQL Server cache (production)
            /*
            services.AddDistributedTokenCaches(cacheServices =>
            {
                cacheServices.AddDistributedSqlServerCache(options =>
                {
                    options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
                    options.SchemaName = "dbo";
                    options.TableName = "TokenCache";
                    options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
                });
            });
            */

            // Option 3: Redis cache (production, high-scale)
            /*
            services.AddDistributedTokenCaches(cacheServices =>
            {
                cacheServices.AddStackExchangeRedisCache(options =>
                {
                    options.Configuration = ConfigurationManager.AppSettings["Redis:ConnectionString"];
                    options.InstanceName = "MyMvcApp_";
                });
            });
            */
        }
    }
}

Integrowanie kontrolerów

Uzyskaj dostęp do Microsoft Graph i podrzędnych interfejsów API z kontrolerów przy użyciu metod rozszerzeń udostępnianych przez pakiet.

Integrowanie kontrolerów MVC

W poniższym przykładzie pokazano, jak używać metod rozszerzenia kontrolera w celu uzyskania dostępu do Microsoft Graph:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Microsoft.Graph;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace MyMvcApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {
        // GET: Home/Index
        public async Task<ActionResult> Index()
        {
            try
            {
                // Access Microsoft Graph using extension method
                var graphClient = this.GetGraphServiceClient();
                var user = await graphClient.Me.GetAsync();

                ViewBag.UserName = user.DisplayName;
                ViewBag.Email = user.Mail ?? user.UserPrincipalName;
                ViewBag.JobTitle = user.JobTitle;

                return View();
            }
            catch (MsalUiRequiredException)
            {
                // Incremental consent required
                return new ChallengeResult();
            }
            catch (Exception ex)
            {
                return View("Error", new ErrorViewModel { Message = ex.Message });
            }
        }

        // GET: Home/Profile
        public async Task<ActionResult> Profile()
        {
            var graphClient = this.GetGraphServiceClient();

            // Get user profile
            var user = await graphClient.Me
                .GetAsync(requestConfig => requestConfig.QueryParameters.Select = new[] { "displayName", "mail", "jobTitle", "department" });

            return View(user);
        }

        // GET: Home/Photo
        public async Task<ActionResult> Photo()
        {
            var graphClient = this.GetGraphServiceClient();

            try
            {
                // Get user photo
                var photoStream = await graphClient.Me.Photo.Content.GetAsync();
                return File(photoStream, "image/jpeg");
            }
            catch (ServiceException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
            {
                return File(Server.MapPath("~/Content/images/default-user.png"), "image/png");
            }
        }
    }
}

Integrowanie kontrolerów internetowego interfejsu API

W poniższym przykładzie pokazano, jak używać metod rozszerzenia ApiController do wywoływania podrzędnego interfejsu API:

using Microsoft.Identity.Web;
using Microsoft.Identity.Web.OWIN;
using Microsoft.Identity.Abstractions;
using System.Threading.Tasks;
using System.Web.Http;

namespace MyWebApi.Controllers
{
    [Authorize]
    [RoutePrefix("api/todos")]
    public class TodoController : ApiController
    {
        // GET: api/todos
        [HttpGet]
        [Route("")]
        public async Task<IHttpActionResult> GetTodos()
        {
            try
            {
                // Call downstream API using extension method
                var downstreamApi = this.GetDownstreamApi();

                var todos = await downstreamApi.GetForUserAsync<List<TodoItem>>(
                    "TodoListService",
                    options =>
                    {
                        options.RelativePath = "api/todolist";
                    });

                return Ok(todos);
            }
            catch (MsalUiRequiredException)
            {
                return Unauthorized();
            }
            catch (HttpRequestException ex)
            {
                return InternalServerError(ex);
            }
        }

        // POST: api/todos
        [HttpPost]
        [Route("")]
        public async Task<IHttpActionResult> CreateTodo([FromBody] TodoItem todo)
        {
            var downstreamApi = this.GetDownstreamApi();

            var createdTodo = await downstreamApi.PostForUserAsync<TodoItem, TodoItem>(
                "TodoListService",
                todo,
                options =>
                {
                    options.RelativePath = "api/todolist";
                });

            return Created($"api/todos/{createdTodo.Id}", createdTodo);
        }
    }
}

Wywoływanie programu Microsoft Graph

Użyj GraphServiceClient do interakcji z danymi Microsoft Graph z kontrolerów.

Konfigurowanie klienta Microsoft Graph

Klient Microsoft Graph jest już skonfigurowany w Startup.Auth.cs z następującym wywołaniem:

services.AddMicrosoftGraph();

Używanie elementu GraphServiceClient w kontrolerach

W poniższym przykładzie pokazano typowe operacje Microsoft Graph w kontrolerze MVC.

[Authorize]
public class GraphController : Controller
{
    public async Task<ActionResult> MyProfile()
    {
        var graphClient = this.GetGraphServiceClient();
        var user = await graphClient.Me.GetAsync();

        return View(user);
    }

    public async Task<ActionResult> MyManager()
    {
        var graphClient = this.GetGraphServiceClient();
        var manager = await graphClient.Me.Manager.GetAsync();

        return View(manager);
    }

    public async Task<ActionResult> MyDirectReports()
    {
        var graphClient = this.GetGraphServiceClient();
        var directReports = await graphClient.Me.DirectReports.GetAsync();

        return View(directReports.Value);
    }

    public async Task<ActionResult> SendEmail([FromBody] EmailMessage message)
    {
        var graphClient = this.GetGraphServiceClient();

        var email = new Message
        {
            Subject = message.Subject,
            Body = new ItemBody
            {
                ContentType = BodyType.Text,
                Content = message.Body
            },
            ToRecipients = new[]
            {
                new Recipient
                {
                    EmailAddress = new EmailAddress
                    {
                        Address = message.To
                    }
                }
            }
        };

        await graphClient.Me.SendMail.PostAsync(new SendMailPostRequestBody
        {
            Message = email
        });

        return RedirectToAction("Index");
    }
}

Wywoływanie interfejsów API niższego poziomu

Zarejestruj i wywołaj podrzędne interfejsy API przy użyciu automatycznego uzyskiwania tokenu z kontrolerów.

Konfigurowanie podrzędnego interfejsu API

Użyj następującego kodu, aby zarejestrować podrzędny interfejs API w Startup.Auth.cs.

services.AddDownstreamApi("TodoListService", configuration.GetSection("DownstreamApi:TodoListService"));

Dodaj odpowiednie ustawienia w pliku Web.config:

<add key="DownstreamApi:TodoListService:BaseUrl" value="https://localhost:44351" />
<add key="DownstreamApi:TodoListService:Scopes" value="api://todo-api-client-id/.default" />

Używanie interfejsu IDownstreamApi w kontrolerach

W poniższym przykładzie pokazano, jak wykonywać operacje CRUD względem podrzędnego interfejsu API z kontrolera MVC.

[Authorize]
public class TodoController : Controller
{
    // GET all todos
    public async Task<ActionResult> Index()
    {
        var downstreamApi = this.GetDownstreamApi();

        var todos = await downstreamApi.GetForUserAsync<List<TodoItem>>(
            "TodoListService",
            options =>
            {
                options.RelativePath = "api/todolist";
            });

        return View(todos);
    }

    // GET specific todo
    public async Task<ActionResult> Details(int id)
    {
        var downstreamApi = this.GetDownstreamApi();

        var todo = await downstreamApi.GetForUserAsync<TodoItem>(
            "TodoListService",
            options =>
            {
                options.RelativePath = $"api/todolist/{id}";
            });

        return View(todo);
    }

    // POST new todo
    [HttpPost]
    public async Task<ActionResult> Create(TodoItem todo)
    {
        var downstreamApi = this.GetDownstreamApi();

        var createdTodo = await downstreamApi.PostForUserAsync<TodoItem, TodoItem>(
            "TodoListService",
            todo,
            options =>
            {
                options.RelativePath = "api/todolist";
            });

        return RedirectToAction("Index");
    }

    // PUT update todo
    [HttpPost]
    public async Task<ActionResult> Edit(int id, TodoItem todo)
    {
        var downstreamApi = this.GetDownstreamApi();

        await downstreamApi.CallApiForUserAsync(
            "TodoListService",
            options =>
            {
                options.HttpMethod = HttpMethod.Put;
                options.RelativePath = $"api/todolist/{id}";
                options.RequestBody = todo;
            });

        return RedirectToAction("Index");
    }

    // DELETE todo
    [HttpPost]
    public async Task<ActionResult> Delete(int id)
    {
        var downstreamApi = this.GetDownstreamApi();

        await downstreamApi.CallApiForUserAsync(
            "TodoListService",
            options =>
            {
                options.HttpMethod = HttpMethod.Delete;
                options.RelativePath = $"api/todolist/{id}";
            });

        return RedirectToAction("Index");
    }
}

Eksplorowanie przykładowych aplikacji

Skorzystaj z poniższych przykładów, aby zobaczyć Microsoft Identity.Web.OWIN w działaniu.

Przejrzyj oficjalne przykłady Microsoft

W poniższej tabeli wymieniono oficjalne przykłady, które pokazują integrację Microsoft.Identity.Web.OWIN.

Przykład Opis
ms-identity-aspnet-webapp-openidconnect Aplikacja ASP.NET MVC z Microsoft.Identity.Web.OWIN
Pliki kluczowe App_Start/Startup.Auth.cs, Controllers/HomeController.cs

Sklonuj i uruchom przykład za pomocą następujących poleceń:

git clone https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect
cd ms-identity-aspnet-webapp-openidconnect
# Update Web.config with your Microsoft Entra app registration
# Run in Visual Studio

Postępuj zgodnie z najlepszymi rozwiązaniami

Zastosuj te zalecane wzorce i unikaj typowych błędów podczas tworzenia przy użyciu Microsoft. Identity.Web.OWIN.

1. Użyj rozproszonej pamięci podręcznej w środowisku produkcyjnym:

//  Production
services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
        options.SchemaName = "dbo";
        options.TableName = "TokenCache";
        options.DefaultSlidingExpiration = TimeSpan.FromMinutes(90);
    });
});

2. Obsługa przyrostowej zgody w sposób bezproblemowy:

try
{
    var graphClient = this.GetGraphServiceClient();
    var user = await graphClient.Me.GetAsync();
}
catch (MsalUiRequiredException)
{
    // User needs to consent to additional scopes
    return new ChallengeResult();
}

3. Użyj identyfikatorów korelacji do rozwiązywania problemów:

var downstreamApi = this.GetDownstreamApi();
var correlationId = Guid.NewGuid();

var result = await downstreamApi.GetForUserAsync<Todo>(
    "TodoListService",
    options =>
    {
        options.RelativePath = $"api/todolist/{id}";
        options.TokenAcquisitionOptions = new TokenAcquisitionOptions
        {
            CorrelationId = correlationId
        };
    });

4. Zaimplementuj właściwą obsługę błędów:

try
{
    // Call API
}
catch (MsalUiRequiredException)
{
    return new ChallengeResult();
}
catch (HttpRequestException ex)
{
    logger.Error($"API call failed: {ex.Message}");
    return View("Error");
}

Unikaj typowych błędów

1. Nie używaj pamięci podręcznej w pamięci RAM dla farm serwerów:

//  Wrong for load-balanced scenarios
services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedMemoryCache();
});

//  Correct
services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedSqlServerCache(/* ... */);
});

2. Nie wprowadzaj konfiguracji na stałe w kodzie:

//  Wrong
ClientId = "your-client-id-here"

//  Correct
ClientId = ConfigurationManager.AppSettings["AzureAd:ClientId"]

3. Nie ignoruj wygaśnięcia tokenu:

//  Microsoft.Identity.Web.OWIN handles this automatically
// No manual token refresh needed!

Rozwiązywanie typowych problemów

Zapoznaj się z poniższymi rozwiązaniami typowych problemów występujących podczas instalacji i środowiska uruchomieniowego.

Rozwiązywanie typowych problemów

Problem 1: "Nie można odnaleźć elementu IAuthorizationHeaderProvider"

Rozwiązanie: Upewnij się, że OwinTokenAcquirerFactory jest zarejestrowany w programie Startup.Auth.cs:

var tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
tokenAcquirerFactory.Build(serviceProvider);
app.Use<OwinTokenAcquisitionMiddleware>(tokenAcquirerFactory);

Problem 2: "Nie można odnaleźć elementu GraphServiceClient"

Rozwiązanie: Dodaj AddMicrosoftGraph() w Startup.Auth.cs:

services.AddMicrosoftGraph();

Problem 3. Pamięć podręczna tokenu nie jest utrwalana

Rozwiązanie: Sprawdź konfigurację rozproszonej pamięci podręcznej:

services.AddDistributedTokenCaches(cacheServices =>
{
    cacheServices.AddDistributedSqlServerCache(options =>
    {
        // Ensure connection string is correct
        options.ConnectionString = ConfigurationManager.ConnectionStrings["TokenCache"].ConnectionString;
    });
});

Dowiedz się więcej o powiązanych funkcjach i scenariuszach.