给scalar/swagger 文档页面加个登录
效果:

极简使用:
builder.Services.AddApiDocAuth();//启用session
scalar:
app.UseApiDocAuth("scalar");//放在启用scalar之前。
app.MapOpenApi();
app.MapScalarApiReference();
swagger:
app.UseApiDocAuth("swagger");//放在启用swagger之前。
app.UseSwagger();
app.UseSwaggerUI();
就一个ApiDocAuthExtensions.cs
public static class ApiDocAuthExtensions
{
public static IServiceCollection AddApiDocAuth(this IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession();
return services;
}
/// <summary>
/// 在启用 scalar/swagger 中间件之前
/// </summary>
/// <param name="app"></param>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="apiDocUI">scalar/swagger</param>
/// <returns></returns>
public static IApplicationBuilder UseApiDocAuth(this WebApplication app, string apiDocUI, string username = "scalar", string password = "scalar")
{
app.UseSession();
if (string.IsNullOrWhiteSpace(apiDocUI)) { apiDocUI = "scalar"; }
apiDocUI = apiDocUI.Replace("/", string.Empty);
var sessionKeyLoggedIn = $"{apiDocUI}LoggedIn";
var sessionKeyUser = $"{apiDocUI}User";
var authUrl = "/apidoc/auth";
var loginUrl = "/apidoc/login";
var loginPage = "/apidoc/login.html";
var logoutUrl = "/apidoc/logout";
#region 登录验证
app.MapPost(authUrl, async (HttpContext context) =>
{
if (string.IsNullOrWhiteSpace(username))
{
await ApiPage(username, context);
return;
}
using var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
var loginData = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(body);
var uname = loginData?.GetValueOrDefault("username");
var pwd = loginData?.GetValueOrDefault("password");
if (uname == username && pwd == password)
{
await ApiPage(username, context);
}
else
{
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{\"success\": false, \"message\": \"用户名或密码错误\"}");
}
async Task ApiPage(string username, HttpContext context)
{
context.Session.SetString(sessionKeyLoggedIn, "true");
context.Session.SetString(sessionKeyUser, username);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("{\"success\": true, \"redirectUrl\": \"/" + apiDocUI + "\"}");
}
});
#endregion
#region 登录页
app.MapGet(loginUrl, async (HttpContext context) =>
{
var isLoggedIn = context.Session.GetString(sessionKeyLoggedIn);
if (!string.IsNullOrWhiteSpace(isLoggedIn))
{
context.Response.Redirect($"/{apiDocUI}");
return;
}
await ToLogin(context);
});
#endregion
#region 退出登录
app.MapGet(logoutUrl, async (HttpContext context) =>
{
context.Session.Remove(sessionKeyLoggedIn);
context.Session.Remove(sessionKeyUser);
await ToLogin(context);
});
#endregion
#region 只对需要保护的路径启用检查拦截
app.UseWhen(
context =>
(context.Request.Path.StartsWithSegments($"/{apiDocUI}") || context.Request.Path.StartsWithSegments("/openapi"))
&& !context.Request.Path.StartsWithSegments(loginPage)
&& !context.Request.Path.StartsWithSegments(loginUrl)
&& !context.Request.Path.StartsWithSegments(authUrl),
appBuilder =>
{
appBuilder.Use(async (context, next) =>
{
if (string.IsNullOrWhiteSpace(context.Session.GetString(sessionKeyLoggedIn)))
{
await ToLogin(context);
return;
}
await next();
});
});
#endregion
#region 将嵌入式资源文件,以静态文件的形式暴露给 HTTP 客户端访问。 ToLogin()逻辑则为context.Response.Redirect/SendFileAsync("/apidoc/login.html");
//app.UseStaticFiles(new StaticFileOptions
//{
// FileProvider = new Microsoft.Extensions.FileProviders.EmbeddedFileProvider(
// System.Reflection.Assembly.GetExecutingAssembly(),
// //baseNamespace: $"{typeof(ApiDocAuthExtensions).Namespace}.Resources"
// baseNamespace: $"{System.Diagnostics.Process.GetCurrentProcess().ProcessName}.Resources"
// ),
// RequestPath = $"/{apiDocUI}"
//});
#endregion
return app;
}
#region 去登录
private static async Task ToLogin(HttpContext context)
{
context.Response.ContentType = "text/html; charset=utf-8";
await context.Response.WriteAsync(@"
<!DOCTYPE html>
<html>
<head>
<title>API 文档 UI</title>
<style>
body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.login-box { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); width: 350px; }
h2 { text-align: center; color: #333; margin-bottom: 30px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; color: #555; font-size: 14px; }
input { width: 100%; padding: 12px; border: 2px solid #e1e1e1; border-radius: 6px; font-size: 14px; transition: border-color 0.3s; box-sizing: border-box; }
input:focus { outline: none; border-color: #667eea; }
button { width: 100%; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: bold; transition: transform 0.1s; }
button:hover { transform: translateY(-1px); }
button:active { transform: translateY(0); }
.error { color: #dc3545; text-align: center; margin-top: 15px; font-size: 14px; display: none; }
</style>
</head>
<body>
<div class='login-box'>
<h2>🔒 API 文档 UI 登录</h2>
<form id='loginForm' method='post' action='/apidoc/auth'>
<div class='form-group'>
<label for='username'>用户名</label>
<input type='text' id='username' name='username' placeholder='请输入用户名' required />
</div>
<div class='form-group'>
<label for='password'>密码</label>
<input type='password' id='password' name='password' placeholder='请输入密码' required />
</div>
<button type='submit'>登 录</button>
<div class='error' id='errorMsg'></div>
</form>
</div>
</body>
<script>
document.querySelector('#loginForm').addEventListener('submit',async function(e){
try {
e.preventDefault(); // 阻止原生form整页提交(禁止跳转)
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorDiv = document.getElementById('errorMsg');
const response = await fetch(this.action, {
method: this.method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await response.json();
console.log('result',result)
if (result.success) {
window.location.href = result.redirectUrl;
} else {
errorDiv.textContent = result.message || '登录失败';
errorDiv.style.display = 'block';
}
} catch (err) {
errorDiv.textContent = err+'网络错误,请稍后重试';
errorDiv.style.display = 'block';
}
})
</script>
</html>");
}
#endregion
}
浙公网安备 33010602011771号