首页 > 解决方案 > .Net Core 2.1 与 IdentityServer4 Cookie 和基于 API JWT 的同一应用程序的身份验证

问题描述

我有一个带有 API 的 IdentityServer 设置和一个用 React 编写的 SPA。SPA 使用 javascript oidc 客户端库向 IdentityServer 进行身份验证,然后从 API 获取数据。SPA 和 API 在同一个项目中,SPA 使用 services.AddSpaStaticFiles 和 app.UseSpa 提供服务,所以我认为我应该能够互换使用这两种身份验证方案。

问题是我在 API 端存储了图像,我希望 SPA 客户端能够获取并放置在 <img> 标记中,可以选择单击它并在新窗口中打开全尺寸图像。图像必须要求用户通过身份验证才能访问它们。

我尝试将基于 Cookie 的身份验证添加到 API 的 ConfigureServices,希望让用户在 SPA 上进行身份验证,然后访问 API 上的图像 URL 会起作用。

services.AddAuthentication(options => 
{ 
    options.DefaultScheme = "Cookies";
    options.DefaultChallengeScheme = "oidc";
})
.AddIdentityServerAuthentication("apiAuth", options =>
{
    options.Authority = "http://localhost:5000";
    options.RequireHttpsMetadata = false;
    options.ApiName = "api";
})
.AddCookie("cookieAuth")
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "http://localhost:5000";
    options.RequireHttpsMetadata = false;
    options.ClientId = "afx_api";
    options.SaveTokens = true;
 });

然后在将返回图像的控制器上添加 [Authorize(AuthenticationSchemes = "cookieAuth")],在所有其他 API 控制器上添加 [Authorize(AuthenticationSchemes = "apiAuth")]。

但是,当我尝试访问图像时,比如说http://localhost:6000/api/file/1我在这里被重定向http://localhost:6001/Account/Login?ReturnUrl=%2Fapi%2Fdocuments%2Ffile%2F90404即使我已经通过身份验证并且正常的 API 调用正常工作。

我该怎么做呢?谢谢

编辑:我的设置中的更多代码

IdentityServer/Config.cs 客户端配置

new Client
{
    ClientId = "client",
    ClientName = "React Client",
    AllowedGrantTypes = GrantTypes.Implicit,
    AllowAccessTokensViaBrowser = true,
    RequireConsent = false,
    AccessTokenLifetime = 3600,

    RedirectUris = {
        "http://localhost:6001/callback",
        "http://localhost:6001/silent_renew.html",
    },
    PostLogoutRedirectUris =
    {
        "http://localhost:6001/",
    },
    AllowedCorsOrigins =
    {
        "http://localhost:6001",
    },

    AllowedScopes = new List<string>
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "api"
    },
    AlwaysIncludeUserClaimsInIdToken = true
}

ClientApp/src/userManager.js

import { createUserManager } from 'redux-oidc';

const settings = {
    client_id: 'client',
    redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/callback`,
    response_type: 'id_token token',
    scope:"openid profile api",
    authority: 'http://localhost:5000',
    silent_redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/silent_renew.html`,
    automaticSilentRenew: true,
    loadUserInfo: true,
    monitorSession: true
};

const userManager = createUserManager(settings);

export default userManager;

基于 Elrashid 的新创业公司的回答:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
    {
        options.DefaultScheme = "cookies";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("cookies",
                    options => options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")
    .AddJwtBearer("jwt", options =>
    {
        options.Authority = "http://localhost:5000";
        options.Audience = "api";
        options.RequireHttpsMetadata = false;
    })
    .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "cookies";
        options.Authority = "http://localhost:5000";
        options.RequireHttpsMetadata = false;
        options.ClientId = "client";
        options.SaveTokens = true;

        options.ResponseType = "id_token token";
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("api");
        options.Scope.Add("offline_access");
        options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc";
    });

当我尝试从控制器获取图像时,仍然得到相同的结果,即重定向到 /Account/Login?ReturnUrl=%2Fimages%2Ffile%2F90404。API 仍然有效。控制器方法返回一个 PNG,无需授权即可工作。

[Authorize(AuthenticationSchemes = "cookies")]
[Route("[controller]")]
public class ImagesController : Controller
{
    ...
}

标签: .netreactjsauthenticationcookiesidentityserver4

解决方案


使用 ForwardDefaultSelector

  • 所有路线都将使用 cookie

  • 但是以/api开头的路由将使用 jwt

    ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")
    
    ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc")
    

如果你有这 2 条路线

- localhost/Secure/Index

- localhost/api/secure/Get

安全控制器

public class SecureController : Controller
{
    [Authorize]
    public IActionResult Index()
    {
        return View();
    }
}

安全API

[Route("api/secure")]
[Authorize]
public class SecureApi : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
    }
}

清除 Jwt 安全令牌处理程序

JwtSecurityTokenHandler
   .DefaultInboundClaimTypeMap.Clear();

使用 AddAuthentication 将 cookie 设置为 DefaultScheme

services.AddAuthentication(options =>
        {
            // Notice the schema name is case sensitive [ cookies != Cookies ]
            options.DefaultScheme = "cookies";
            options.DefaultChallengeScheme = "oidc";
        })

添加 Cookie 选项

    .AddCookie("cookies", options => 
     options.ForwardDefaultSelector = ctx => 
      ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")

添加 Jwt 承载

   .AddJwtBearer("jwt", options =>
    {
        options.Authority = "http://localhost:5010";
        options.Audience = "app2api";
        options.RequireHttpsMetadata = false;
    })

添加OpenIdConnect

        .AddOpenIdConnect("oidc", options =>
    {
        options.SignInScheme = "cookies";
        options.Authority = "http://localhost:5010";
        options.RequireHttpsMetadata = false;
        options.ClientId = "mvc";
        options.SaveTokens = true;

        options.ClientSecret = "secret";
        options.ResponseType = "code id_token";
        options.GetClaimsFromUserInfoEndpoint = true;
        options.Scope.Add("app2api");
        options.Scope.Add("offline_access");
        //https://github.com/leastprivilege/AspNetCoreSecuritySamples/blob/aspnetcore21/OidcAndApi/src/AspNetCoreSecurity/Startup.cs
        options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc";
    });

完整的 Startup.cs

 public class Startup
{


    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            // This lambda determines whether user consent for non-essential cookies is needed for a given request.
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });

        ////////////////////////////////
        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        services.AddAuthentication(options =>
        {
            // Notice the schema name is case sensitive [ cookies != Cookies ]
            options.DefaultScheme = "cookies";
            options.DefaultChallengeScheme = "oidc";
        })

        .AddCookie("cookies", options => options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "cookies")
        .AddJwtBearer("jwt", options =>
        {
            options.Authority = "http://localhost:5010";
            options.Audience = "app2api";
            options.RequireHttpsMetadata = false;
        })
        .AddOpenIdConnect("oidc", options =>
        {
            options.SignInScheme = "cookies";
            options.Authority = "http://localhost:5010";
            options.RequireHttpsMetadata = false;
            options.ClientId = "mvc";
            options.SaveTokens = true;

            options.ClientSecret = "secret";
            options.ResponseType = "code id_token";
            options.GetClaimsFromUserInfoEndpoint = true;
            options.Scope.Add("app2api");
            options.Scope.Add("offline_access");

            options.ForwardDefaultSelector = ctx => ctx.Request.Path.StartsWithSegments("/api") ? "jwt" : "oidc";
        });
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            app.UseHsts();
        }

        app.UseAuthentication();
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();

        app.UseMvcWithDefaultRoute();
    }
}

在身份服务器中使用 HybridAndClientCredentials

    new Client
    {
        ClientId = "mvc",
        ClientName = "MVC Client",
        AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
        ClientSecrets =
        {
            new Secret("secret".Sha256())
        },
        // where to redirect to after login
        RedirectUris = { "http://localhost:5011/signin-oidc" },
        // where to redirect to after logout
        PostLogoutRedirectUris = { "http://localhost:5011/signout-callback-oidc" },
        AllowedScopes = new List<string>
        {
            IdentityServerConstants.StandardScopes.OpenId,
            IdentityServerConstants.StandardScopes.Profile,
            "app2api"
        },
        AllowOfflineAccess = true
    }

摘要:我的 Github 上的示例应用程序


推荐阅读