ASP.NET Core的Authorize属性与JWT无法正常工作

38
我想在ASP.Net Core中实现基于JWT的安全性。目前,我只希望它能够读取Authorization标头中的承载令牌并根据我的标准对其进行验证。我不需要(也不想)包含ASP.Net Identity。事实上,我尽量避免使用MVC添加的许多功能,除非我真正需要它们。
我创建了一个最小化项目来演示问题。要查看原始代码,请查看编辑历史记录。我期望这个示例将拒绝所有未提供相应承载令牌的/api/icons请求。但是,这个示例实际上允许所有请求
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Routing;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System;
using Newtonsoft.Json.Serialization;

namespace JWTSecurity
{
    public class Startup
    {
        public IConfigurationRoot Configuration { get; set; }

        public Startup(IHostingEnvironment env)
        {
            IConfigurationBuilder builder = new ConfigurationBuilder().SetBasePath(env.ContentRootPath);
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions();
            services.AddAuthentication();
            services.AddMvcCore().AddJsonFormatters(options => options.ContractResolver = new CamelCasePropertyNamesContractResolver());
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();
            app.UseJwtBearerAuthentication(new JwtBearerOptions
            {
                AutomaticAuthenticate = true,
                AutomaticChallenge = true,
                TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("supersecretkey")),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.Zero
                }
            });
            app.UseMvc(routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"));
        }
    }
}

控制器/IconsController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace JWTSecurity.Controllers
{
    [Route("api/[controller]")]
    public class IconsController : Controller
    {
        [Authorize]
        public IActionResult Get()
        {
            return Ok("Some content");
        }
    }
}

你能展示一下你在哪些方法和类中使用了授权属性吗? - jegtugado
你的JWT令牌中间件在哪里?你的服务是否通过/token端点或类似方式正确生成JWT? - SledgeHammer
你现在拥有的与我几乎相同,除了我有ValidateIssuer = true和ValidateAudience = true,但我认为这些不是必需的。我也没有services.AddAuthentication();。 - SledgeHammer
是的,我刚刚尝试添加它们,但没有任何区别。肯定有什么我漏掉了... - Andrew Williamson
我注意到的一个不同之处是你没有在Get()方法上使用[HttpGet]进行装饰。这可能与此有关,因为它会在没有该属性的情况下以不同的方式处理该方法...值得一试 :)。 - SledgeHammer
显示剩余5条评论
6个回答

60

找到了!

主要问题在这一行:

services.AddMvcCore().AddJsonFormatters(options => options.ContractResolver = new CamelCasePropertyNamesContractResolver());

我注意到将 AddMvcCore() 改为 AddMvc() 后,授权功能突然开始起作用了!在深入研究ASP.NET源代码,查看 AddMvc() 所做的工作后,我意识到需要再调用 IMvcBuilder.AddAuthorization()

services.AddMvcCore()
    .AddAuthorization() // Note - this is on the IMvcBuilder, not the service collection
    .AddJsonFormatters(options => options.ContractResolver = new CamelCasePropertyNamesContractResolver());

6
这也让我感到困惑(因为我正在使用 AddMvcCore)。调用 AddAuthorization() 会添加 AuthorizationApplicationModelProvider,该提供程序查找控制器上的 Authorize/AllowAnonymous 并添加相应的策略。 - Ben Foster
1
如果您使用数据注释进行请求验证,请不要忘记添加 AddDataAnnotations - Peter Morris
在使用了 services.AddMvcCore().AddAuthorization() 后,Authorize 属性开始生效。 - user2167322
我认为你可以查看这个答案 https://dev59.com/llIG5IYBdhLWcg3w-nBU#63446357 - Hoque MD Zahidul
@HoqueMDZahidul 的目标是使用 MvcCore 制作一个最小化的示例。你提供的答案展示了一个工作的 asp net 配置,但它并没有展示出 _获取令牌认证工作所需的最少配置_。如果我只想让认证工作正常运行,我本可以继续使用 .AddMvc() 而不是 .AddMvcCore()。你的回答并没有解决原始问题。 - Andrew Williamson

42

您还在使用身份验证,其中隐含了Cookie身份验证。可能您使用身份方案登录并导致成功的身份验证。

如果不需要身份验证(如果只需要JWT身份验证),请删除身份验证。否则,请像下面这样为Authorize属性指定Bearer方案:

[Authorize(AuthenticationSchemes = "Bearer")]

完成。我也尝试过在非异步方法上进行这个操作,结果也是一样的。 - Andrew Williamson
奇怪!你能在操作方法中检查当前用户吗? - adem caglin
1
您不需要ActiveAuthenticationSchemes来使用JWT。只要正确发送令牌,就可以正常工作。 - SledgeHammer
1
@SledgeHammer 当然,通常情况下你不需要它。但是我假设问题是由身份验证引起的,并且假设 OP 只想要 JWT 身份验证。 - adem caglin
1
你是我的英雄。 - Linda Lawton - DaImTo
显示剩余6条评论

25

对于那些尝试了之前的答案但未能解决问题的人,以下是我个人情况下如何解决该问题的方法。

[Authorize(AuthenticationSchemes="Bearer")]

我认为你可以查看这个答案 https://dev59.com/llIG5IYBdhLWcg3w-nBU#63446357 - Hoque MD Zahidul
没问题,老兄。 愉快地编写代码。 - André Mendonça

4

我之前遇到了类似的问题,结果发现控制器级别的 [AllowAnonymous] 属性会覆盖在该控制器中任何动作上应用的 [Authorize] 属性。这是我之前不知道的情况。


3
我找到了解决这个问题的完美方法。 您的配置服务类应该如下所示。
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;
        });

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));

        services.AddIdentity<ApplicationUser, IdentityRole>
        (options => options.Stores.MaxLengthForKeys = 128)
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultUI()
        .AddDefaultTokenProviders();

        JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();


        services.AddAuthentication(options =>
        {
            //options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            //options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            //options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            //options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

        })
        .AddCookie(cfg => cfg.SlidingExpiration = true)
        .AddJwtBearer(cfg =>
        {
            cfg.RequireHttpsMetadata = false;
            cfg.SaveToken = true;
            cfg.TokenValidationParameters = new TokenValidationParameters
            {
                ValidIssuer = Configuration["JwtIssuer"],
                ValidAudience = Configuration["JwtIssuer"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JwtKey"])),
                ClockSkew = TimeSpan.Zero // remove delay of token when expire
            };
        });


        services.Configure<IdentityOptions>(options =>
        {
            // Password settings  
            options.Password.RequireDigit = true;
            options.Password.RequiredLength = 8;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequireUppercase = true;
            options.Password.RequireLowercase = false;
            options.Password.RequiredUniqueChars = 6;

            // Lockout settings  
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
            options.Lockout.MaxFailedAccessAttempts = 10;
            options.Lockout.AllowedForNewUsers = true;

            // User settings  
            options.User.RequireUniqueEmail = true;
        });

        services.AddAuthentication().AddFacebook(facebookOptions =>
        {
            facebookOptions.AppId = Configuration["Authentication:Facebook:AppId"];
            facebookOptions.AppSecret =  Configuration["Authentication:Facebook:AppSecret"];
        });
        //Seting the Account Login page  
        services.ConfigureApplicationCookie(options =>
        {
            // Cookie settings  
            options.Cookie.HttpOnly = true;
            options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
            options.LoginPath = "/Account/Login"; // If the LoginPath is not set here, ASP.NET Core will default to /Account/Login  
            options.LogoutPath = "/Account/Logout"; // If the LogoutPath is not set here, ASP.NET Core will default to /Account/Logout  
            options.AccessDeniedPath = "/Account/AccessDenied"; // If the AccessDeniedPath is not set here, ASP.NET Core will default to /Account/AccessDenied  
            options.SlidingExpiration = true;
        });



        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

您可以像下面这样对Web API控制器进行身份验证。
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[ApiController]
public class TaskerController : ControllerBase
{
    [HttpGet("[action]")]
    //[AllowAnonymous]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
}

您可以像下面这样使用基于身份验证的授权属性来为MVC控制器进行授权。
public class TaskController : Controller
{

    [Authorize]
    public IActionResult Create()
    {
    }
}

关键解决方案是在JWT身份验证之前添加.AddCookie(cfg => cfg.SlidingExpiration = true),默认设置基于Cookie的授权方式,因此 [Authorize] 的功能仍然正常。每当需要JWT时,您必须显式调用[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

希望这将有助于那些想要将网站作为前端,并将移动就绪的Web API作为后端的人们。


问题中的关键点之一是“我不需要(也不想)包含ASP.Net Identity”。使用标准配置设置Asp.Net很容易,但目标是创建一个使用MvcCore和JwtAuthentication的最小项目。 - Andrew Williamson
答案涵盖了两种身份验证机制,您可以根据需要跳过其中任何一个。您只需要注释掉不需要的部分即可。@AndrewWilliamson - Tushar Kshirsagar
1
我很感激你想要与他人分享你的经验 - 不要停止,只需记得专注于用最简单的例子回答问题。 - Andrew Williamson
2
感谢[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]属性。在我的情况下,对于WebApi我想使用JWTAuth,对于ViewController我想使用CookieAuth。然后我创建了双重AddAuthentication()。但不幸的是,由于我在未添加参数的情况下使用了[Authorize],未授权页面总是会命中JWTAuth设置。但是,在[Authorize]上添加参数后,这个问题得到了解决。 - Fadhly Permata
我认为你可以查看这个答案 https://dev59.com/llIG5IYBdhLWcg3w-nBU#63446357 - Hoque MD Zahidul
显示剩余2条评论

1
如果您正在使用自定义方案,则必须使用。
[Authorize(AuthenticationSchemes="your custom scheme")]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接