.Net 7 Swagger API JWT 身份验证:使用有效令牌访问端点后出现 HTTP 401(未经授权)

问题描述 投票:0回答:1

我已经尝试过了

使用有效的 JWT 令牌获取 401 Unauthorized

无济于事,因为这不是我的情况。

我刚刚在我的 dotnet 7 API 中实现了 jwt 令牌身份验证,并且令牌已成功生成,但是当尝试访问任何端点时,我总是收到 401 未经授权的 http 错误。

程序.cs

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();

#region swagger
//builder.Services.AddSwaggerGen();
builder.Services.AddSwaggerGen(option =>
{
    option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" });
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter a valid token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            new string[]{}
        }
    });
});
#endregion

#region jwt data
var validIssuer = builder.Configuration["JWT:ValidIssuer"];
var validAudience = builder.Configuration["JWT:ValidAudience"];
var issuerSigningKey = builder.Configuration["JWT:IssuerSigningKey"];
#endregion

if (validIssuer != null && validAudience != null && issuerSigningKey != null) 
{
    #region jwt
    builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ClockSkew = TimeSpan.Zero,
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = validIssuer,
            ValidAudience = validAudience,
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(issuerSigningKey)
            ),
        };
    });
    #endregion

    #region DbContext
    //builder.Services.AddDbContext<SurveysDbContext>(options =>
    //{
    //    options.UseSqlServer(builder.Configuration.GetConnectionString("SurveyConnection"));
    //});
    //builder.Services.AddDbContext<UsersDbContext>(options =>
    //{
    //    options.UseSqlServer(builder.Configuration.GetConnectionString("SurveyConnection"));
    //});
    builder.Services.AddDbContext<UsersDbContext>();
    #endregion

    #region neo4j data
    var neo4jServer = builder.Configuration["Neo4J:server"];
    var neo4jUser = builder.Configuration["Neo4J:user"];
    var neo4jPwd = builder.Configuration["Neo4J:pwd"];
    #endregion

    if (neo4jServer != null && neo4jUser != null && neo4jPwd != null)
    {
        #region neo4j
        var client = new BoltGraphClient(new Uri(neo4jServer), neo4jUser, neo4jPwd);
        client.ConnectAsync();
        #endregion

        #region services
        #region domain
        builder.Services.AddSingleton<SurveysDomain>();
        builder.Services.AddSingleton<BlocksDomain>();
        builder.Services.AddSingleton<QuestionsDomain>();
        builder.Services.AddSingleton<OptionsDomain>();
        builder.Services.AddSingleton<AnswersDomain>();
        #endregion

        #region repository
        //builder.Services.AddTransient<IQuestionsRepository, QuestionsRepository>();
        builder.Services.AddSingleton<SurveysRepository>();
        builder.Services.AddSingleton<BlocksRepository>();
        builder.Services.AddSingleton<QuestionsRepository>();
        builder.Services.AddSingleton<OptionsRepository>();
        builder.Services.AddSingleton<AnswersRepository>();
        #endregion

        builder.Services.AddSingleton<IGraphClient>(client);
        builder.Services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
        builder.Services.AddScoped<TokenService, TokenService>();
        #endregion

        #region users
        builder.Services
            .AddIdentityCore<IdentityUser>(options =>
            {
                options.SignIn.RequireConfirmedAccount = false;
                options.User.RequireUniqueEmail = true;
                options.Password.RequireDigit = false;
                options.Password.RequiredLength = 6;
                options.Password.RequireNonAlphanumeric = false;
                options.Password.RequireUppercase = false;
                options.Password.RequireLowercase = false;
            }).AddRoles<IdentityRole>().AddEntityFrameworkStores<UsersDbContext>();
        #endregion

        var app = builder.Build();

        // Configure the HTTP request pipeline.
        //if (app.Environment.IsDevelopment())
        //{
        app.UseSwagger();
        app.UseSwaggerUI();
        //}

        #region swagger default page in release mode
        if (!app.Environment.IsDevelopment())
        {
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "Surveys Web API");
                options.RoutePrefix = string.Empty;
            });
            app.UseHsts();
        }
        #endregion

        app.UseHttpsRedirection();
        app.UseAuthorization();
        app.UseAuthentication();
        app.MapControllers();

        app.Run();
    }
    else
    {
        throw new Exception("Neo4J is not configured!");
    }
}
else
{
    throw new Exception("JWT is not configured!");
}

应用程序设置.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SurveyConnection": "xxx"
  },
  "Neo4J": {
    "server": "bolt://localhost:",
    "user": "yyy",
    "pwd": "yyy"
  },
  "JWT": {
    "ValidIssuer": "aaa",
    "ValidAudience": "aaa",
    "IssuerSigningKey": "bbb"
  }
}

令牌服务:

public class TokenService
{
    private const int ExpirationMinutes = 30;
    public string? CreateToken(IdentityUser user)
    {
        var signingCredentials = CreateSigningCredentials();
        if (signingCredentials == null) return null;

        var claims = CreateClaims(user);
        if (claims==null) return null;

        var expiration = DateTime.UtcNow.AddMinutes(ExpirationMinutes);
        var token = CreateJwtToken(
            claims,
            signingCredentials,
            expiration
        );
        var tokenHandler = new JwtSecurityTokenHandler();
        return tokenHandler.WriteToken(token);
    }

    private JwtSecurityToken CreateJwtToken(List<Claim> claims, SigningCredentials credentials,
        DateTime expiration)
    {
        var validIssuer = Util.Util.getAppSettingsValue("JWT:ValidIssuer");
        var validAudience = Util.Util.getAppSettingsValue("JWT:ValidAudience");

        return new(
            validIssuer,
            validAudience,
            claims,
            expires: expiration,
            signingCredentials: credentials
        );
    }

    private List<Claim>? CreateClaims(IdentityUser user)
    {
        if (user.UserName == null || user.Email == null) return null;

        try
        {
            var claims = new List<Claim>
                {
                    new Claim(JwtRegisteredClaimNames.Sub, "TokenForTheApiWithAuth"),
                    new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                    new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
                    new Claim(ClaimTypes.NameIdentifier, user.Id),
                    new Claim(ClaimTypes.Name, user.UserName),
                    new Claim(ClaimTypes.Email, user.Email)
                };
            return claims;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }
    private SigningCredentials? CreateSigningCredentials()
    {
        var secret = Util.Util.getAppSettingsValue("JWT:IssuerSigningKey");
        if (secret == null) return null;

        return new SigningCredentials(
            new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(secret)
            ),
            SecurityAlgorithms.HmacSha256
        );
    }
}

授权请求:

public class AuthRequest
{
    public string Email { get; set; } = null!;
    public string Password { get; set; } = null!;
}

验证响应:

public class AuthResponse
{
    public string Username { get; set; } = null!;
    public string Email { get; set; } = null!;
    public string Token { get; set; } = null!;
}

验证控制器:

[ApiController]
[Route("[controller]")]
public class AuthController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly UsersDbContext _context;
    private readonly TokenService _tokenService;

    public AuthController(UserManager<IdentityUser> userManager, UsersDbContext context, TokenService tokenService)
    {
        _userManager = userManager;
        _context = context;
        _tokenService = tokenService;
    }

    [HttpPost]
    [Route("register")]
    public async Task<IActionResult> Register(User user)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        var result = await _userManager.CreateAsync(
            new IdentityUser { UserName = user.Username, Email = user.Email },
            user.Passwd
        );
        if (result.Succeeded)
        {
            user.Passwd = "";
            return CreatedAtAction(nameof(Register), new { email = user.Email }, user);
        }
        foreach (var error in result.Errors)
        {
            ModelState.AddModelError(error.Code, error.Description);
        }
        return BadRequest(ModelState);
    }

    [HttpPost]
    [Route("login")]
    public async Task<ActionResult<AuthResponse>> Authenticate([FromBody] AuthRequest request)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var managedUser = await _userManager.FindByEmailAsync(request.Email);
        if (managedUser == null)
        {
            return BadRequest("Bad credentials");
        }
        var isPasswordValid = await _userManager.CheckPasswordAsync(managedUser, request.Password);
        if (!isPasswordValid)
        {
            return BadRequest("Bad credentials");
        }
        var userInDb = _context.Users.FirstOrDefault(u => u.Email == request.Email);
        if (userInDb is null)
            return Unauthorized();
        var accessToken = _tokenService.CreateToken(userInDb);
        await _context.SaveChangesAsync();
        return Ok(new AuthResponse
        {
            Username = userInDb.UserName,
            Email = userInDb.Email,
            Token = accessToken,
        });
    }
}

问题控制器:

[ApiController]
[Route("surveys")]
public class QuestionsController : ControllerBase
{
    private readonly QuestionsDomain _questionsDomain;
    public QuestionsController(QuestionsDomain questionsDomain)
    {
        _questionsDomain = questionsDomain;
    }

    /// <summary>
    /// Get all questions in a block
    /// </summary>
    /// <param name="surveyId"></param>
    /// <returns>json</returns>
    [HttpGet("{surveyId}/blocks/{blockId}/questions"), Authorize]
    public async Task<IActionResult> GetQuestionsAsync(int surveyId)
    {
        //var questions = await _questionDomain.GetQuestionsAsync(surveyId);
        //return Ok(questions);

        var question1 = new Question
        {
            Id = 1,
            IdSurvey = 1,
            Text = "¿Te gusta el deporte?"
        };
        var question2 = new Question
        {
            Id = 2,
            IdSurvey = 1,
            Text = "¿Te gusta el fútbol?"
        };

        var questions = new List<Question> { question1, question2, question3, question4 };

        return Ok(questions);
    }

    ...   
}

.net jwt swagger
1个回答
2
投票

最终我没有在端点的 [Authorize] 装饰器中指定 AuthenticationScheme。不知道是强制性的,但添加此后它开始正确验证令牌。

[授权(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

© www.soinside.com 2019 - 2024. All rights reserved.