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

效果:
apodoclogin

极简使用:

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
    }

posted on 2026-10-19 23:05  鱼歌  阅读(3)  评论(0)    收藏  举报

导航