#swagger #authorization #openapi #blazor-webassembly #nswag
#развязность #авторизация #openapi #blazor-веб-сборка #нсваг
Вопрос:
У меня есть приложение Blazor WebAssm, которое я хочу защитить с помощью базовых токенов на предъявителя JWT, это внутренне используемое приложение, поэтому фактическая аутентификация будет осуществляться с помощью проверки на соответствие внутреннему AD. Мой уровень WebAPI использует встроенный Swagger для создания документа OpenAPI 3, который затем использует мой клиент для создания класса C # для его использования.
Итак, у меня есть клиент OpenAPI, и я могу вызвать его из своего приложения blazor, но я не могу понять, как добавить безопасность ни на стороне API, ни на стороне Blazor.
Я использую .NET 5 как на уровне WebAPI, так и на клиенте Blazor.
Ответ №1:
Это заняло много времени, чтобы разобраться, и я уверен, что есть лучшие способы. Но вот шаги, которые я сделал, чтобы добавить это:
Во-первых, при запуске WebAPI вам необходимо правильно настроить Swagger:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My Cool Tool",
Version = "v1",
Description = "My Cool API",
});
//add jwt authentication definition to the OpenAPI doc.
var securityScheme = new OpenApiSecurityScheme
{
Name = "JWT Authentication",
Description = "Enter JWT Bearer Token Only",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Reference = new OpenApiReference
{
Id = JwtBearerDefaults.AuthenticationScheme,
Type = ReferenceType.SecurityScheme
}
};
c.AddSecurityDefinition(securityScheme.Reference.Id, securityScheme);
//to indicate the entire API is secured, add this. NOTE it does NOT secure it, just indicates it is.
//c.AddSecurityRequirement(new OpenApiSecurityRequirement
//{
// {securityScheme, new string[]{ } }
//});
//this filter does add if an API is secured based upon the Authorize attribute
c.OperationFilter<SecurityRequirementsOperationFilter>();
});
Обратите внимание, что последние несколько строк являются ключевыми, в AddSecurityDefinition
конце помещается фрагмент с подробным описанием схемы, которую вы используете. Если вы раскомментируете эти строки, то схема безопасности добавляется к каждой операции (маловероятно, что вы хотите!), Поэтому последняя строка просто добавляет ее к этим операциям / методам с [Authorize]
тегом.
А это SecurityRequirementsOperationFilter
заключается в следующем
public class SecurityRequirementsOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
//if the controller method has the authorize attribute, get the roles
var requiredRoles = context.MethodInfo
.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Select(attr => attr.Roles)
.Distinct();
if (requiredRoles.Any())
{
//add the fact that this operation could return the 401/403 HTTP status codes
operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });
operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" });
//add the fact that this operation is secured by bearer auth
var bearerScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = JwtBearerDefaults.AuthenticationScheme,
Type = ReferenceType.SecurityScheme
}
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{ bearerScheme, requiredRoles.ToList() }
}
};
}
}
}
Вы можете пропустить часть ролей и просто заменить ToList
на пустой массив строк.
Наконец, у нас есть еще несколько стандартных настроек авторизации, которые нужно выполнить при запуске:
//get token management settings
var token = Configuration.GetSection("tokenManagement").Get<TokenManagement>();
services.AddSingleton(token);
//add authentication
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = token.Issuer,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
ValidAudience = token.Audience,
ValidateAudience = false
};
});
//add the authentication user service
services.AddScoped<IUserService, UserService>();
TokenManagement
это poco, в котором хранятся некоторые элементы конфигурации для токенов.
UserService
это класс IsValidUser
, в котором у вас есть, куда вы добавляете свой фактический чек.
Наконец, вам нужен какой-то метод входа в систему:
[AllowAnonymous]
[HttpPost("login")]
[ProducesResponseType(typeof(LoginResult), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult Login([FromBody] LoginRequest request)
{
if (!ModelState.IsValid)
{
return BadRequest("Invalid Request");
}
if (!_userService.IsValidUser(request.UserName, request.Password))
{
return BadRequest("Invalid Request");
}
var claims = new[]
{
new Claim(ClaimTypes.Name, request.UserName),
new Claim(ClaimTypes.Role, _userService.GetUserRole(request.UserName))
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenManagement.Secret));
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var jwtToken = new JwtSecurityToken(
_tokenManagement.Issuer,
_tokenManagement.Audience,
claims,
expires: DateTime.Now.AddMinutes(_tokenManagement.AccessExpiration),
signingCredentials: credentials);
var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
_logger.LogInformation($"User [{request.UserName}] logged in the system.");
return Ok(new LoginResult
{
UserName = request.UserName,
JwtToken = token
});
}
На этом этапе вы можете украсить свои методы api [Authorize]
атрибутом, и последующие вызовы должны быть заблокированы без заголовка Bearer в запросе.
На стороне клиента NSwag сгенерирует partial
класс для клиента, который я расширил и предоставил тело одному из предоставленных PrepareRequest
методов, поскольку это вызывается непосредственно перед HttpClient
выполнением Send
.
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
{
_logger.LogInformation($"Prepare Request: {request.RequestUri.AbsoluteUri}");
var user = _storage.GetItem<LoginResult>("user");
if (user != null)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", user.JwtToken);
_logger.LogDebug($"Headers: {request.Headers.ToString()}");
}
}
Ключевым моментом здесь является то, что этот метод должен быть синхронным, поскольку сгенерированный клиент этого не await
делает.
Поэтому в моем случае я использовал Blazored.LocalStorage
пакет, чтобы получить эти методы синхронизации. Обратите внимание, что, поскольку мне был нужен LocalStorageService
, мне также пришлось добавить свой собственный конструктор для клиента в файл частичного класса, который вызывал тот, который генерирует NSwag.
Теперь, когда это сделано, вы можете добавить страницу входа в систему для вызова этого более раннего метода входа, сохранить токен, и теперь, когда вы вызываете API, будет добавлен ваш токен-носитель.