394 lines
16 KiB
HTML
394 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fa" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>مستندات فنی پیادهسازی احراز هویت XApiKey در xIds</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap');
|
||
:root {
|
||
--primary: #1a365d;
|
||
--primary-light: #2c5282;
|
||
--accent: #c9a961;
|
||
--accent-dark: #a8893f;
|
||
--bg: #faf9f6;
|
||
--card-bg: #ffffff;
|
||
--text: #1a202c;
|
||
--text-muted: #4a5568;
|
||
--border: #e2e8f0;
|
||
--success: #38a169;
|
||
--danger: #e53e3e;
|
||
--warning: #dd6b20;
|
||
--shadow: 0 4px 6px rgba(0,0,0,0.05), 0 10px 30px rgba(26,54,93,0.08);
|
||
--code-bg: #f7fafc;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: 'Vazirmatn', Tahoma, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.8;
|
||
font-size: 15px;
|
||
padding: 30px 15px;
|
||
}
|
||
.document-container {
|
||
max-width: 1000px;
|
||
margin: 0 auto;
|
||
background: var(--card-bg);
|
||
box-shadow: var(--shadow);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
.document-header {
|
||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
|
||
color: white;
|
||
padding: 40px;
|
||
position: relative;
|
||
border-bottom: 4px solid var(--accent);
|
||
}
|
||
.header-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
.doc-meta {
|
||
text-align: left;
|
||
font-size: 13px;
|
||
opacity: 0.9;
|
||
}
|
||
.doc-meta div { margin-bottom: 4px; }
|
||
.document-title h1 {
|
||
font-size: 26px;
|
||
font-weight: 800;
|
||
margin-bottom: 8px;
|
||
}
|
||
.document-title h2 {
|
||
font-size: 16px;
|
||
font-weight: 400;
|
||
opacity: 0.9;
|
||
}
|
||
.document-body {
|
||
padding: 40px;
|
||
}
|
||
.section {
|
||
margin-bottom: 40px;
|
||
page-break-inside: avoid;
|
||
}
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
padding-bottom: 12px;
|
||
border-bottom: 2px solid var(--border);
|
||
margin-bottom: 20px;
|
||
}
|
||
.section-number {
|
||
width: 40px; height: 40px;
|
||
background: linear-gradient(135deg, var(--primary), var(--primary-light));
|
||
color: white;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 800;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
.section-title h3 {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: var(--primary);
|
||
}
|
||
.section-content p {
|
||
margin-bottom: 15px;
|
||
text-align: justify;
|
||
}
|
||
.code-block {
|
||
background: var(--code-bg);
|
||
border: 1px solid var(--border);
|
||
border-right: 4px solid var(--accent);
|
||
border-radius: 6px;
|
||
padding: 15px;
|
||
margin: 15px 0;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 13px;
|
||
overflow-x: auto;
|
||
direction: ltr;
|
||
text-align: left;
|
||
}
|
||
.highlight-box {
|
||
background: #fffaf0;
|
||
border: 1px solid #f6e05e;
|
||
border-right: 4px solid var(--accent);
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
margin: 20px 0;
|
||
}
|
||
.success-box {
|
||
background: #f0fff4;
|
||
border: 1px solid #9ae6b4;
|
||
border-right: 4px solid var(--success);
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
margin: 20px 0;
|
||
}
|
||
.warning-box {
|
||
background: #fff5f5;
|
||
border: 1px solid #feb2b2;
|
||
border-right: 4px solid var(--danger);
|
||
padding: 15px;
|
||
border-radius: 6px;
|
||
margin: 20px 0;
|
||
}
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 20px 0;
|
||
}
|
||
th, td {
|
||
padding: 12px;
|
||
border: 1px solid var(--border);
|
||
text-align: right;
|
||
}
|
||
th {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
tr:nth-child(even) { background: #f8fafc; }
|
||
.document-footer {
|
||
background: var(--primary);
|
||
color: white;
|
||
padding: 20px;
|
||
text-align: center;
|
||
font-size: 12px;
|
||
}
|
||
.footer-brand {
|
||
font-weight: 700;
|
||
color: var(--accent);
|
||
margin-bottom: 5px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="document-container">
|
||
<header class="document-header">
|
||
<div class="header-top">
|
||
<div class="doc-meta">
|
||
<div>شماره مستند: TECH-XIDS-APIKEY-001</div>
|
||
<div>نسخه: ۱.۰</div>
|
||
<div>تاریخ تهیه: ۲۰ مهر ۱۴۰۵</div>
|
||
<div>طبقهبندی: داخلی - فنی</div>
|
||
</div>
|
||
</div>
|
||
<div class="document-title">
|
||
<h1>راهنمای جامع پیادهسازی و عیبیابی احراز هویت XApiKey</h1>
|
||
<h2>پروژه xIds (Identity Server) | ماژول مدیریت Application و API Key</h2>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="document-body">
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<div class="section-number">۱</div>
|
||
<div class="section-title">
|
||
<h3>مقدمه و بررسی وضعیت فعلی</h3>
|
||
</div>
|
||
</div>
|
||
<div class="section-content">
|
||
<p>
|
||
در پروژه <strong>xIds</strong>، زیرساخت کاملی برای مدیریت <code>XApplication</code> و <code>XApiKey</code> پیادهسازی شده است.
|
||
کلاسهای <code>XApplicationProvider</code> و <code>XApiKeyHelper</code> وظیفه تولید، هش کردن و اعتبارسنجی کلیدها را بر عهده دارند.
|
||
با این حال، چالش اصلی در لایه <strong>Authentication Middleware</strong> و نحوه اتصال آن به Policyهای موجود بود.
|
||
</p>
|
||
<div class="highlight-box">
|
||
<strong>مشکل شناسایی شده:</strong> درخواستهای ارسالی با هدر <code>X-Api-Key</code> توسط ASP.NET Core نادیده گرفته میشدند، زیرا Scheme پیشفرض روی JWT Bearer تنظیم شده بود و Handler مخصوص ApiKey فراخوانی نمیشد.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<div class="section-number">۲</div>
|
||
<div class="section-title">
|
||
<h3>راهحل فنی: اصلاح زنجیره احراز هویت</h3>
|
||
</div>
|
||
</div>
|
||
<div class="section-content">
|
||
<p>برای رفع مشکل، دو تغییر اساسی در فایل <code>xIds/DI/XDIHelperExtension.cs</code> اعمال شد:</p>
|
||
|
||
<h4>الف) ثبت Scheme اختصاصی برای XApiKey</h4>
|
||
<p>قبل از ثبت JwtBearer، Scheme مربوط به ApiKey ثبت میشود تا در لیست Authentication Schemes وجود داشته باشد.</p>
|
||
|
||
<h4>ب) استفاده از ForwardDefaultSelector هوشمند</h4>
|
||
<p>به جای استفاده ثابت از <code>ForwardDefault</code>، از یک Delegate استفاده میکنیم که Header درخواست را بررسی میکند.</p>
|
||
|
||
<div class="code-block">
|
||
// xIds/DI/XDIHelperExtension.cs
|
||
|
||
public static void AddXIdentityServerAuthentication(
|
||
this IServiceCollection services,
|
||
IConfiguration configuration,
|
||
bool addApiKeyAuthentication = false)
|
||
{
|
||
services.AddXIdentityResourceConfiguration(configuration);
|
||
var authBuilder = services.AddAuthentication();
|
||
|
||
// 1. ثبت Scheme برای XApiKey (اولویت اول)
|
||
if (addApiKeyAuthentication)
|
||
{
|
||
authBuilder.AddScheme<AuthenticationSchemeOptions, XApiKeyAuthenticationHandler>(
|
||
XAuthenticationScheme.XApiKey.GetStringValue(), // "XApiKey"
|
||
options => { });
|
||
}
|
||
|
||
// 2. ثبت JwtBearer با Selector هوشمند
|
||
authBuilder.AddJwtBearer(options =>
|
||
{
|
||
options.SaveToken = true;
|
||
options.RequireHttpsMetadata = false;
|
||
|
||
// انتخاب دینامیک Scheme بر اساس هدر درخواست
|
||
options.ForwardDefaultSelector = context =>
|
||
{
|
||
var apiKeyHeader = XHeader.ApiKey.GetStringValue(); // "X-Api-Key"
|
||
if (context.Request.Headers.ContainsKey(apiKeyHeader))
|
||
{
|
||
return XAuthenticationScheme.XApiKey.GetStringValue();
|
||
}
|
||
// در غیر این صورت، از احراز هویت استاندارد IdentityServer استفاده کن
|
||
return XAuthentication.IDENTITY_SERVER_LOCAL_API;
|
||
};
|
||
})
|
||
.AddLocalApi();
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<div class="section-number">۳</div>
|
||
<div class="section-title">
|
||
<h3>اصلاح Claims تولید شده در Handler</h3>
|
||
</div>
|
||
</div>
|
||
<div class="section-content">
|
||
<p>
|
||
در فایل <code>xIds/Providers/XApiKeyAuthenticationHandler.cs</code>، متد <code>HandleAuthenticateAsync</code> باید Claimهای صحیح را تولید کند تا با Policyهای تعریف شده در <code>XAuthorizationHelper</code> همخوانی داشته باشد.
|
||
</p>
|
||
<div class="warning-box">
|
||
<strong>نکته مهم:</strong> Policyهای ApiKey بر اساس Scope (مانند read, write, admin) کار میکنند، نه Role. بنابراین Claim از نوع <code>JwtClaimTypes.Scope</code> ضروری است.
|
||
</div>
|
||
<div class="code-block">
|
||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||
{
|
||
// ... استخراج و اعتبارسنجی ApiKey ...
|
||
|
||
var validationResult = await applicationProvider.ValidateApiKey(apiKey, clientIP);
|
||
|
||
if (validationResult.Errors.HasChild())
|
||
return AuthenticateResult.Fail(string.Join(", ", validationResult.Errors));
|
||
|
||
var claims = new List<Claim>
|
||
{
|
||
new Claim(ClaimTypes.Name, validationResult.OwnerId),
|
||
new Claim(JwtClaimTypes.Subject, validationResult.OwnerId),
|
||
new Claim("application_id", validationResult.ApplicationId.ToString()),
|
||
new Claim("auth_type", "apikey"),
|
||
};
|
||
|
||
// افزودن Scopeهای مجاز به Claims
|
||
if (validationResult.Scopes != null)
|
||
{
|
||
foreach (var scope in validationResult.Scopes)
|
||
{
|
||
claims.Add(new Claim(JwtClaimTypes.Scope, scope));
|
||
}
|
||
}
|
||
|
||
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
||
var principal = new ClaimsPrincipal(identity);
|
||
var ticket = new AuthenticationTicket(principal, Scheme.Name);
|
||
|
||
return AuthenticateResult.Success(ticket);
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<div class="section-number">۴</div>
|
||
<div class="section-title">
|
||
<h3>نحوه استفاده در Controllerها</h3>
|
||
</div>
|
||
</div>
|
||
<div class="section-content">
|
||
<p>برای محافظت از Endpointها با ApiKey، از Attributeهای زیر استفاده کنید:</p>
|
||
<div class="code-block">
|
||
[RequireXPowered]
|
||
[Authorize(Policy = XPolicies.ApiKeyAccess)] // دسترسی کلی
|
||
public async Task<ActionResult> MySecureAction() { ... }
|
||
|
||
[Authorize(Policy = XPolicies.ApiKeyReadAccess)] // فقط خواندن
|
||
public async Task<ActionResult> GetItems() { ... }
|
||
</div>
|
||
<div class="success-box">
|
||
<strong>تست موفقیتآمیز:</strong> اکنون با ارسال هدر <code>X-Api-Key: xapp_...</code> به Endpointهای فوق، احراز هویت انجام شده و دسترسی اعطا میشود.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<div class="section-header">
|
||
<div class="section-number">۵</div>
|
||
<div class="section-title">
|
||
<h3>چکلیست نهایی استقرار</h3>
|
||
</div>
|
||
</div>
|
||
<div class="section-content">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ردیف</th>
|
||
<th>اقدام</th>
|
||
<th>وضعیت</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>۱</td>
|
||
<td>بررسی ثبت <code>AddXIdentityServerAuthentication</code> در Startup با پارامتر <code>true</code></td>
|
||
<td>✅ انجام شد</td>
|
||
</tr>
|
||
<tr>
|
||
<td>۲</td>
|
||
<td>اطمینان از وجود Policyهای <code>ApiKeyAccess</code> در <code>XAuthorizationHelper</code></td>
|
||
<td>✅ موجود است</td>
|
||
</tr>
|
||
<tr>
|
||
<td>۳</td>
|
||
<td>تست ایجاد ApiKey جدید از طریق <code>/Applications/{id}/ApiKeys/Create</code></td>
|
||
<td>✅ تست شد</td>
|
||
</tr>
|
||
<tr>
|
||
<td>۴</td>
|
||
<td>تست دسترسی به <code>/Account/Test/HiApiKeyAccess</code> با کلید معتبر</td>
|
||
<td>✅ پاس شد</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<footer class="document-footer">
|
||
<div class="footer-brand">تهیه شده توسط: هادی خزاعی اصل | شرکت فن آوران ساحر علم</div>
|
||
<div>تاریخ انتشار: ۲۰ مهر ۱۴۰۵</div>
|
||
</footer>
|
||
</div>
|
||
</body>
|
||
</html> |