【问题标题】:ASP.NET Core WebAPI Cookie + JWT AuthenticationASP.NET Core WebAPI Cookie + JWT 身份验证
【发布时间】:2018-09-02 12:01:57
【问题描述】:

我们有一个带有 API 后端(ASP.NET Core WebAPI)的 SPA(Angular):

SPA 监听 app.mydomain.com,API 监听 app.mydomain.com/API

我们使用内置Microsoft.AspNetCore.Authentication.JwtBearer的JWT进行身份验证;我有一个控制器 app.mydomain.com/API/auth/jwt/login 来创建令牌。 SPA 将它们保存到本地存储中。一切都很完美。安全审核后,我们被告知要为 cookie 切换本地存储。

问题是,app.mydomain.com/API 上的 API 被 SPA 使用,但也被移动应用程序和一些客户的 server-2-server 解决方案使用。

所以,我们必须保持 JWT 不变,但要添加 Cookie。我发现了几篇文章,它们在不同的控制器上结合了 Cookie 和 JWT,但我需要它们在每个控制器上并排工作。

如果客户端发送 cookie,则通过 cookie 进行身份验证。如果客户端发送 JWT 承载,则通过 JWT 进行身份验证。

这是否可以通过内置的 ASP.NET 身份验证或 DIY 中间件来实现?

谢谢!

【问题讨论】:

  • 为您的 MVC 控制器使用 cookie 很好,但我建议不要将 cookie 用于 WebAPI,因为您的 api 容易受到Cross Site Request Forgery / XSRF 的攻击,并且保护它是一个更大的麻烦(Anti Request forgery on WebAPI 比 MVC 应用更让人头疼)。
  • 可能还是比 cookie 和 webapi 好。如果攻击者可以将他引诱到任何其他方面或您无法控制的隐藏表单,它可能会使攻击者使用登录用户权限执行操作。最重要的是,Antiforgery 请求需要一个状态(cookie 和服务器上的正确令牌以供稍后比较),这违反了 REST 服务的“无状态”性质。此外,在 SPA 中发布新的 AntiRequest 伪造令牌并不直观,您需要在发送请求之前每次都请求服务器以获取对下一个请求有效的新令牌
  • 恕我直言,您最好的选择是使用不透明(或 IdentityServer 4 术语中的参考令牌)令牌。它仍然需要在每个请求上发送令牌,但是您可以启用令牌验证,因此您可以相当快速地撤销令牌,以防它被泄露和使用。您还可以在发布时将用户 IP 放入令牌中,如果 IP 更改,它就会失效。对用户来说更烦人,但它可以防止攻击者自己使用访问令牌或刷新令牌(除非再次,用户可以将 javascript 代码注入应用程序)。
  • 但是两种方法(Cookie 和 JWT)在注入代码时都可能受到攻击。 Http Cookie 不允许攻击者窃取 cookie,但他仍然可以代表登录用户执行操作。存储在本地存储中的 JWT cookie 相同,但它们也可以自己窃取令牌,但这可以通过将 IP 作为声明放入令牌并在服务器上验证它或至少更难(IP 可以被欺骗)来防止,但攻击者无法得到任何响应)。复杂的话题
  • 感谢您的努力。我们将重新打开安全审计建议,并进行头脑风暴@work。

标签: c# authentication cookies asp.net-core jwt


【解决方案1】:

ASP.NET Core 2.0 Web API

请按照这篇文章实现基于JWT Token的身份验证

https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login

如果您使用的是 Visual Studio,请确保将 Bearer 类型的身份验证类型与过滤器一起应用

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

用于控制器或操作。

【讨论】:

  • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。
  • 这将允许仅使用 JWT 调用方法,而不是使用 cookie。
  • @user369450 没问题。因为我不能在这里粘贴所有东西.. 我们单独发布了它fullstackmark.com/post/13/…
【解决方案2】:

我也遇到了同样的问题,我刚刚在 stackoverflow 的另一个问题中找到了解决方案。

请看this

我将自己尝试该解决方案并使用结果更新此答案。

编辑:似乎不可能以相同的方法实现双重身份验证类型,但我提到的链接中提供的解决方案说:

不可能用两个 Schemes Or-Like 来授权一个方法,但是你可以使用两个公共方法来调用一个私有方法

//private method
private IActionResult GetThingPrivate()
{
   //your Code here
}
//Jwt-Method
[Authorize(AuthenticationSchemes = $"{JwtBearerDefaults.AuthenticationScheme}")]
[HttpGet("bearer")]
public IActionResult GetByBearer()
{
   return GetThingsPrivate();
}
 //Cookie-Method
[Authorize(AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme}")]
[HttpGet("cookie")]
public IActionResult GetByCookie()
{
   return GetThingsPrivate();
}    

不管怎样,你应该看看这个链接,它确实对我有帮助。 感谢 Nikolaus 的解答。

【讨论】:

    【解决方案3】:

    我还没有找到很多关于执行此操作的好方法的信息 - 不得不复制 API 只是为了支持 2 种授权方案是一件痛苦的事情。

    我一直在研究使用反向代理的想法,在我看来这是一个很好的解决方案。

    1. 用户登录网站(使用 cookie httpOnly 进行会话)
    2. 网站使用防伪令牌
    3. SPA 向网站服务器发送请求,并在标头中包含防伪令牌:https://app.mydomain.com/api/secureResource
    4. 网站服务器验证防伪令牌 (CSRF)
    5. 网站服务器确定请求是针对 API 的,应将其发送到反向代理
    6. 网站服务器获取 API 的用户访问令牌
    7. 反向代理将请求转发到 API:https://api.mydomain.com/api/secureResource

    请注意,防伪令牌(#2、#4)至关重要,否则您可能会将您的 API 暴露给 CSRF 攻击。


    示例(带有 IdentityServer4 的 .NET Core 2.1 MVC):

    为了获得一个工作示例,我从 IdentityServer4 快速入门 Switching to Hybrid Flow and adding API Access back 开始。这设置了 MVC 应用程序使用 cookie 并可以从身份服务器请求 access_token 以调用 API 的场景。

    我用Microsoft.AspNetCore.Proxy做反向代理,修改了快速入门。

    MVC 启动.ConfigureServices:

    services.AddAntiforgery();
    services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    

    MVC 启动.配置:

    app.MapWhen(IsApiRequest, builder =>
    {
        builder.UseAntiforgeryTokens();
    
        var messageHandler = new BearerTokenRequestHandler(builder.ApplicationServices);
        var proxyOptions = new ProxyOptions
        {
            Scheme = "https",
            Host = "api.mydomain.com",
            Port = "443",
            BackChannelMessageHandler = messageHandler
        };
        builder.RunProxy(proxyOptions);
    });
    
    private static bool IsApiRequest(HttpContext httpContext)
    {
        return httpContext.Request.Path.Value.StartsWith(@"/api/", StringComparison.OrdinalIgnoreCase);
    }
    

    ValidateAntiForgeryToken(马吕斯·舒尔茨):

    public class ValidateAntiForgeryTokenMiddleware
    {
        private readonly RequestDelegate next;
        private readonly IAntiforgery antiforgery;
    
        public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
        {
            this.next = next;
            this.antiforgery = antiforgery;
        }
    
        public async Task Invoke(HttpContext context)
        {
            await antiforgery.ValidateRequestAsync(context);
            await next(context);
        }
    }
    
    public static class ApplicationBuilderExtensions
    {
        public static IApplicationBuilder UseAntiforgeryTokens(this IApplicationBuilder app)
        {
            return app.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
        }
    }
    

    BearerTokenRequestHandler:

    public class BearerTokenRequestHandler : DelegatingHandler
    {
        private readonly IServiceProvider serviceProvider;
    
        public BearerTokenRequestHandler(IServiceProvider serviceProvider, HttpMessageHandler innerHandler = null)
        {
            this.serviceProvider = serviceProvider;
            InnerHandler = innerHandler ?? new HttpClientHandler();
        }
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
            var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
            request.Headers.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
            var result = await base.SendAsync(request, cancellationToken);
            return result;
        }
    }
    

    _Layout.cshtml:

    @Html.AntiForgeryToken()
    

    然后,您可以使用您的 SPA 框架提出请求。为了验证我刚刚做了一个简单的 AJAX 请求:

    <a onclick="sendSecureAjaxRequest()">Do Secure AJAX Request</a>
    <div id="ajax-content"></div>
    
    <script language="javascript">
    function sendSecureAjaxRequest(path) {
        var myRequest = new XMLHttpRequest();
        myRequest.open('GET', '/api/secureResource');
        myRequest.setRequestHeader("RequestVerificationToken",
            document.getElementsByName('__RequestVerificationToken')[0].value);
        myRequest.onreadystatechange = function () {
            if (myRequest.readyState === XMLHttpRequest.DONE) {
                if (myRequest.status === 200) {
                    document.getElementById('ajax-content').innerHTML = myRequest.responseText;
                } else {
                    alert('There was an error processing the AJAX request: ' + myRequest.status);
                }
            }  
        };
        myRequest.send();
    };
    </script>
    

    这是一个概念验证测试,所以你的里程可能非常好,而且我对 .NET Core 和中间件配置还很陌生,所以它可能看起来更漂亮。我对此进行了有限的测试,只向 API 发出了 GET 请求,没有使用 SSL (https)。

    正如预期的那样,如果从 AJAX 请求中删除了防伪令牌,则会失败。如果用户尚未登录(经过身份验证),则请求失败。

    与往常一样,每个项目都是独一无二的,因此请务必验证您的安全要求是否得到满足。请查看此答案中留下的任何 cmets,以了解有人可能提出的任何潜在安全问题。

    另一方面,我认为一旦子资源完整性 (SRI) 和内容安全策略 (CSP) 在所有常用浏览器上可用(即旧版浏览器被逐步淘汰),就应该重新评估本地存储以存储 API令牌存储的复杂性。 现在应该使用 SRI 和 CSP 来帮助减少支持浏览器的攻击面。

    【讨论】:

      【解决方案4】:

      我认为最简单的解决方案是David Kirkland提出的一个:

      创建组合授权策略(在ConfigureServices(IServiceCollection services)):

      services.AddAuthorization(options =>
      {
          var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
              CookieAuthenticationDefaults.AuthenticationScheme,
              JwtBearerDefaults.AuthenticationScheme);
          defaultAuthorizationPolicyBuilder =
              defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
          options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
      });
      

      并添加中间件,在 401 的情况下将重定向到登录(Configure(IApplicationBuilder app)):

      app.UseAuthentication();
      app.Use(async (context, next) =>
      {
          await next();
          var bearerAuth = context.Request.Headers["Authorization"]
              .FirstOrDefault()?.StartsWith("Bearer ") ?? false;
          if (context.Response.StatusCode == 401
              && !context.User.Identity.IsAuthenticated
              && !bearerAuth)
          {
              await context.ChallengeAsync("oidc");
          }
      });
      

      【讨论】:

      • 如何修改中间件以支持 XMLHttpRequest() ?在使用 XMLHttpRequest 进行 ajax 调用的情况下,当点击 await context.ChallangeAsync() 行时,它返回 status = 200 并登录 html 页面?
      【解决方案5】:

      好的,我已经尝试了一段时间,并且通过以下代码解决了使用 jwt 身份验证令牌和 Cookie 身份验证的相同问题。

      API 服务提供者UserController.cs

      这通过(Cookie 和 JWT Bearer)身份验证方案为用户提供不同的服务

      [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
      [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)] 
      [Route("[controller]")]
      [ApiController]
      public class UsersController : ControllerBase
      { 
          private readonly IUserServices_Api _services;
          public UsersController(IUserServices_Api services)
          {
              this._services = services;
          }
           
          [HttpGet]
          public IEnumerable<User> Getall()
          {
              return _services.GetAll();
          }
      }
      

      我的Startup.cs

      public void ConfigureServices(IServiceCollection services)
          {
                
              services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
               
              services.AddAuthentication(options => {
                  options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                  options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                  options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                  options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
              })
                  .AddCookie(options =>
                  {
                      options.LoginPath = "/Account/Login";
                      options.AccessDeniedPath = "/Home/Error";
                  })
                  .AddJwtBearer(options =>
                  {
                      options.SaveToken = true;
                      options.RequireHttpsMetadata = false;
                      options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                      {
                          ValidateIssuer = true,
                          ValidateAudience = true,
                          ValidAudience = " you site link blah blah",
                          ValidIssuer = "You Site link Blah  blah",
                          IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(sysController.GetSecurityKey()))
                          ,
                          ValidateLifetime = true,
                          ClockSkew = TimeSpan.Zero
                      };
                  });
      
          }
      

      如果您想为特定控制器自定义身份验证,则进一步 那么您必须为授权指定身份验证类型 喜欢:

      [Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme)]
      public IActionResult Index()
      {
          return View();    // This can only be Access when Cookie Authentication is Authorized.
      }
      
      [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
      public IActionResult Index()
      {
          return View();    // And this one will be Access when JWT Bearer is Valid
      }
      

      【讨论】:

      • 你有没有注意到你在这样实施后有重复的声明?我还为 webapi 和 MVC 设置了 Jwt & Cookie,并且我有两组相同的声明。这有点烦人。我搜查了一个问题stackoverflow.com/questions/68558515/… 并没有正面回应。
      【解决方案6】:

      在寻找与 net core web api(网站 cookie 和移动应用程序授权标头)组合的 firebase 授权时,以以下解决方案结束。

      public void ConfigureServices(IServiceCollection services)
              {
                  services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                     .AddJwtBearer(options =>
                     {
                         options.Authority = "https://securetoken.google.com/xxxxx";
                         options.TokenValidationParameters = new TokenValidationParameters
                         {
                             ValidateIssuer = true,
                             ValidIssuer = options.Authority,
                             ValidateAudience = true,
                             ValidAudience = "xxxxx",
                             ValidateLifetime = true
                         };
                         options.Events = new JwtBearerEvents
                         {
                             OnMessageReceived = context =>
                             {
                                 if (context.Request.Cookies.ContainsKey(GlobalConst.JwtBearer))
                                 {
                                     context.Token = context.Request.Cookies[GlobalConst.JwtBearer];
                                 }
                                 else if (context.Request.Headers.ContainsKey("Authorization"))
                                      {
                                          var authhdr = context.Request.Headers["Authorization"].FirstOrDefault(k=>k.StartsWith(GlobalConst.JwtBearer));
                                          if (!string.IsNullOrEmpty(authhdr))
                                          {
                                              var keyval = authhdr.Split(" ");
                                              if (keyval != null && keyval.Length > 1) context.Token = keyval[1];
                                          }
                                      }
                                 return Task.CompletedTask;
                             }
                         };
                     });
      
      

      在哪里

       public static readonly string JwtBearer = "Bearer";
      

      似乎工作正常。 从移动和邮递员那里检查过(用于 cookie)

      【讨论】:

        【解决方案7】:

        使用此代码,您可以同时使用 cookie 和标头。 如果 cookie 为空,则自动检查标头。

        在 AddJwtBearer 选项中添加此代码。

        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                context.Token = context.Request.Cookies["Authorization"];
                return Task.CompletedTask;
            }
        };
        

        完整用法是:

                services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
                    {
                        options.RequireHttpsMetadata = false;
                        options.SaveToken = false;
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidAudience = Configuration["JwtToken:Audience"],
                            ValidIssuer = Configuration["JwtToken:Issuer"],
                            IssuerSigningKey =
                                new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtToken:Key"]))
                        };
                        options.Events = new JwtBearerEvents
                        {
                            OnMessageReceived = context =>
                            {
                                context.Token = context.Request.Cookies["Authorization"];
                                return Task.CompletedTask;
                            }
                        };
                    });
        

        Header => 授权:Bearer Your-Token

        Cookie => 授权=您的令牌 //不要在Cookie中添加Bearer

        【讨论】:

          猜你喜欢
          • 2017-03-30
          • 2019-09-06
          • 2021-04-20
          • 2020-05-01
          • 2019-07-17
          • 2018-05-30
          • 1970-01-01
          • 2018-01-28
          • 2018-01-28
          相关资源
          最近更新 更多