【问题标题】:Adding JWT Bearer Auth to OpenAPI3 NSwag generated Client将 JWT Bearer Auth 添加到 OpenAPI3 NSwag 生成的客户端
【发布时间】:2021-03-20 00:36:10
【问题描述】:

我有一个 Blazor WebAssm 应用程序,我想使用基本的 JWT 不记名令牌来保护它,它是一个内部使用的应用程序,因此实际的身份验证将通过对内部 AD 的检查。我的 WebAPI 层使用内置的 Swagger 生成一个 OpenAPI 3 文档,然后我的客户端使用它来创建一个 C# 类来使用它。

所以我有 OpenAPI 客户端,可以从我的 blazor 应用程序调用它,但我不知道如何为 API 端或 Blazor 端添加安全性。

我在 WebAPI 层和 Blazor 客户端上都使用 .NET 5。

【问题讨论】:

    标签: swagger authorization openapi blazor-webassembly nswag


    【解决方案1】:

    这需要很长时间才能弄清楚,我相信还有更好的方法。但这里是我添加这个的步骤:

    首先在WebAPI Startup中,需要正确配置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 替换为空字符串数组即可。

    最后,我们在 Startup 中进行了一些更标准的授权设置:

            //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
            });
        }
    

    此时您可以使用[Authorize] 属性来装饰您的api 方法,并且后续调用应该在请求中没有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 时,你的不记名令牌将被添加。

    【讨论】:

      猜你喜欢
      • 2021-01-15
      • 1970-01-01
      • 1970-01-01
      • 2023-02-20
      • 2020-09-04
      • 1970-01-01
      • 2021-11-18
      • 1970-01-01
      • 2020-04-11
      相关资源
      最近更新 更多