前言
在 SwaggerUI 中加入登录验证,是我很早前就做过的,不过之前的做法总感觉有点硬编码,最近 .Net8 增加了一个新特性:调用 MapSwagger().RequireAuthorization
来保护 Swagger UI ,但官方的这个功能又像半成品一样,只能使用 postman curl 之类的工具带上 Authorization
header 来请求,在浏览器里打开就直接401了 ......
刚好有个项目需要用到这个功能,于是我把之前做过的 SwaggerUI 登录认证中间件拿出来重构了一下。
这次我依然使用 Basic Auth 的方式来登录,写了一个自定义的 SwaggerAuthenticationHandler
,通过 Microsoft.AspNetCore.Authentication
提供的扩展方法来实现登录。
PS:本文以我最近在开发的单点认证项目(IdentityServerLite)为例
配置Swagger
这次我试着不按照写代码的顺序,而是站在使用者的角度来介绍,也许会更直观一些。
编辑 src/IdsLite.Api/Extensions/CfgSwagger.cs
文件 (顾名思义,这是用来配置Swagger的相关扩展方法)
c#
public static class CfgSwagger {
public static IServiceCollection AddSwagger(this IServiceCollection services) {
services.AddSwaggerGen();
return services;
}
public static IApplicationBuilder UseSwaggerWithAuthorize(this IApplicationBuilder app) {
app.UseMiddleware<SwaggerBasicAuthMiddleware>();
app.UseSwagger();
app.UseSwaggerUI();
return app;
}
}
其他的都是常规的配置,重点在于 app.UseMiddleware<SwaggerBasicAuthMiddleware>();
添加了一个中间件
SwaggerBasicAuth 中间件
来编写这个中间件,代码路径 src/IdsLite.Api/Middlewares/SwaggerBasicAuthMiddleware.cs
c#
public class SwaggerBasicAuthMiddleware {
private readonly RequestDelegate _next;
public SwaggerBasicAuthMiddleware(RequestDelegate next) {
_next = next;
}
public async Task InvokeAsync(HttpContext context) {
if (context.Request.Path.StartsWithSegments("/swagger")) {
var result = await context.AuthenticateAsync(AuthSchemes.Swagger);
if (!result.Succeeded) {
context.Response.Headers["WWW-Authenticate"] = "Basic";
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return;
}
}
await _next(context);
}
}
主要逻辑在 InvokeAsync
方法里
判断当前地址以 /swagger
开头的话,就进入身份认证流程,如果配置了其他 SwaggerUI 地址,记得同步修改这个中间件的配置,或者做成通用的配置,避免硬编码。
这里使用了 Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions
提供的扩展方法 context.AuthenticateAsync("Scheme Name")
来验证身份 (具体的 scheme 我们后面会实现)
如果验证失败的话,返回 401 ,同时添加响应头 WWW-Authenticate:Basic
,这样就能在浏览器里弹出输入用户名和密码的提示框了。
AuthenticationScheme
在注册 Authentication
服务的时候,可以添加一些其他的 scheme
PS: AspNetCore 的这套 Identity 确实有点复杂,用了这么久感觉还是没有系统的认识这个 Identity 框架
注册服务
注册服务的代码大概是这样
c#
services
.AddAuthentication(options => {
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultSignInScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(...)
.AddScheme<AuthenticationSchemeOptions, SwaggerAuthenticationHandler>(AuthSchemes.Swagger, null);
AddScheme
方法可以添加各种类型的认证方案,这里添加了一个自定义的认证方案 SwaggerAuthenticationHandler
,后面的参数是方案的名称和选项。
为了避免硬编码,我写了个静态类
c#
public static class AuthSchemes {
public const string Swagger = "SwaggerAuthentication";
}
SwaggerAuthenticationHandler
接下来实现这个自定义的认证方案
其实就是把 Basic Authenticate 和固定用户名和密码结合在一起
不过为了不在代码里硬编码,我把用户名和密码放在配置里了,通过注入 IOption<T>
的方式获取。也可以放在数据库里,通过 EFCore 之类的去读取。
c#
public class SwaggerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> {
public SwaggerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) {}
public SwaggerAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) {}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync() {
if (!Request.Headers.TryGetValue("Authorization", out var value)) {
return AuthenticateResult.Fail("Missing Authorization Header");
}
var config = Context.RequestServices.GetRequiredService<IOptions<IdsLiteConfig>>().Value;
try {
var authHeader = AuthenticationHeaderValue.Parse(value);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(":", 2);
var username = credentials[0];
var password = credentials[1];
if (username != config.Swagger.UserName || password != config.Swagger.Password) {
return AuthenticateResult.Fail("Invalid Username or Password");
}
var claims = new[] { new Claim(ClaimTypes.Name, username) };
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
catch {
return AuthenticateResult.Fail("Invalid Authorization Header");
}
}
}
try
里面的代码,就是从 request header 里读取 basic auth 的用户名和密码(通常是 Base64 编码过的),解码之后判断是否正确,然后返回认证结果。
扩展
还可以集成 OpenIDConnect 和 OAuth ,我还没有实践,详情见参考资料。
小结
既要在项目发布后访问 SwaggerUI ,又要保证一定的安全性,本文提供的思路或许是一种比较简单又有效的解决方案。