Linked Server در SQL Server
تعریف Linked Server
Linked Server در SQL Server یعنی:
ایجاد یک اتصال دائمی (persistent connection) بین سرور فعلی و یک سرور دیگر (که میتونه SQL Server یا هر دیتابیس دیگری مثل Oracle، MySQL، یا حتی Excel باشه) تا بتونی مستقیماً از طریق T-SQL به اون سرور query بزنی.
کاربرد اصلی
وقتی یک Linked Server تعریف کردی، میتونی مثل یک جدول لوکال، از جداول سرور مقصد Select، Insert، Update، Delete انجام بدی.
یعنی بدون اینکه نیاز به export/import یا SSIS داشته باشی، مستقیماً query میزنی.
🧩 مثال کاربردی
فرض کن دو سرور داری:
- سرور اصلی:
SQLServer_Main - سرور دوم:
SQLServer_Branch
میخوای از سرور اصلی، اطلاعات جدول Customers در سرور دوم رو بخونی.
اول Linked Server رو میسازی:
EXEC sp_addlinkedserver
@server = N'SQLServer_Branch',
@srvproduct = N'SQL Server';
بعد اطلاعات لاگین برای اتصال:
EXEC sp_addlinkedsrvlogin
@rmtsrvname = N'SQLServer_Branch',
@useself = 'false',
@locallogin = NULL,
@rmtuser = N'sa',
@rmtpassword = N'YourPassword';
حالا میتونی به راحتی کوئری بزنی:
SELECT *
FROM [SQLServer_Branch].[DatabaseName].[dbo].[Customers];
⚙️ ساختار چهار قسمتی نامها
در Linked Server همیشه از فرمت زیر برای دسترسی استفاده میکنیم:
[LinkedServerName].[DatabaseName].[SchemaName].[ObjectName]
مثلاً:
SELECT * FROM [MyLinkedServer].[SalesDB].[dbo].[Orders];
🛠 کاربردهای واقعی
- ترکیب دادهها از چند سرور
برای مثال گزارشگیری از چند دیتابیس فروش در شعب مختلف. - ETL یا مهاجرت دادهها
انتقال داده از یک سیستم به سیستم دیگر با query ساده. - اجرای distributed query
اجرای کوئریهایی که دادههاش از چند منبع مختلف میان.
⚠️ نکات مهم (Best Practices)
- عملکردش نسبت به query لوکال کندتره، چون شبکه درگیر میشه.
- برای performance بهتر، از OPENQUERY استفاده کن (query سمت سرور مقصد اجرا میشه).
- سطح دسترسیها رو محدود کن تا امنیت حفظ بشه.
- وقتی فقط گاهی نیاز داری، بهتره از Linked Server موقت استفاده نکنی، بلکه Connection string در اپلیکیشن بسازی.
🧠 مثال با OPENQUERY (روش بهینهتر)
SELECT *
FROM OPENQUERY([SQLServer_Branch], 'SELECT TOP 10 * FROM SalesDB.dbo.Orders');
اینجا کل کوئری در سمت سرور مقصد اجرا میشه و فقط نتیجه به سرور اصلی برمیگرده — بسیار سریعتر 🚀
چرا باید سطح دسترسی Linked Server رو محدود کنیم؟
وقتی یه Linked Server تعریف میکنی، در واقع داری به SQL Server میگی:
“هر وقت از این سرور (A) به اون سرور (B) وصل شدم، از فلان کاربر و رمز عبور استفاده کن.”
اگر این تنظیم رو درست انجام ندی، ممکنه:
- همهی کاربران سرور A بتونن به دادههای حساس در سرور B دسترسی پیدا کنن 😱
- یا حتی کاربران بدون پسورد خاصی بتونن query اجرا کنن روی سرور دوم.
🧩 نحوه کنترل دسترسی با sp_addlinkedsrvlogin
دستور زیر مشخص میکنه چه کاربری از سرور محلی، با چه کاربری در سرور مقصد وصل بشه:
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'LinkedServerName',
@useself = 'false',
@locallogin = 'LocalUserName',
@rmtuser = 'RemoteUserName',
@rmtpassword = 'Password123';
پارامترها:
| پارامتر | توضیح |
|---|---|
@rmtsrvname | اسم Linked Server |
@useself | اگر 'true' باشه، همون لاگین فعلی روی سرور محلی برای اتصال استفاده میشه |
@locallogin | نام کاربری در سرور محلی |
@rmtuser | نام کاربری در سرور مقصد |
@rmtpassword | رمز عبور کاربر در سرور مقصد |
🧠 سه سناریوی مهم امنیتی
1. ✅ امنترین حالت: دسترسی اختصاصی برای هر کاربر
هر کاربر لوکال فقط با یک یوزر خاص به سرور مقصد وصل بشه.
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = 'report_user',
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
🔹 مزیت: دقیقاً میدونی چه کسی داره به چی دسترسی داره.
🔹 عیب: باید برای هر کاربر جدا تنظیم کنی.
2. ⚠️ حالت عمومی (برای همه کاربران لوکال)
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = NULL,
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
یعنی همه کاربران لوکال با همون کاربر branch_reader به سرور مقصد وصل میشن.
🔸 این روش رایجه ولی خطرناکه، چون کنترل جزئی نداری.
3. ❌ بدترین حالت: استفاده از sa یا sysadmin
@rmtuser = 'sa'
🚫 هرگز نکن.
چون اگه کسی از سرور A بتونه query اجرا کنه، عملاً دسترسی کامل به کل سرور B پیدا میکنه.
🔐 چند توصیه امنیتی حرفهای
- برای Linked Server یک یوزر اختصاصی بساز (مثلاً
linked_reader) با حداقل سطح دسترسی (فقط SELECT یا EXEC روی Stored Procedure مجاز). - از Windows Authentication استفاده کن تا کنترل توسط Active Directory انجام بشه.
- در فایروال فقط پورت مورد نیاز (پیشفرض 1433) رو بین دو سرور باز کن.
- از SSL/TLS برای ارتباط امن بین سرورها استفاده کن.
- دسترسی Public یا sysadmin رو به Linked Server نده.
- در تنظیمات Linked Server، گزینهی
RPC OutوData Accessرو فقط در صورت نیاز فعال کن.
بررسی تنظیمات امنیتی موجود
برای دیدن لاگینهای فعلی مرتبط با Linked Server:
EXEC sp_helplinkedsrvlogin 'LinkedServerName';
برای حذف تنظیم ناامن:
EXEC sp_droplinkedsrvlogin 'LinkedServerName', NULL;
میخوای در ادامه برات توضیح بدم گزینههای امنیتی Linked Server در GUI (از SSMS) مثل:
- “Be made using the login’s current security context”
- “Be made using this security context”
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = 'report_user',
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
توضیح خط به خط
EXEC sp_addlinkedsrvlogin
فراخوانی پروسیجر سیستمیای هست که یک نقشه (mapping) بین لاگینهای محلی (سروری که کوئری اجرا میکنه) و یک حساب روی سرور مقصد برای یک Linked Server تعریف میکنه. این پروسیجر مشخص میکنه وقتی یک لاگین محلی از طریق Linked Server وصل میشه، با چه credentials ای به سرور remote وصل بشه.@rmtsrvname = 'BranchServer'
اسم Linked Server (همون نامی که باsp_addlinkedserverتعریف کردی) رو مشخص میکنه. در واقع این mapping برای آنِ Linked Server به نامBranchServerساخته میشه.@useself = 'false'
تعیین میکنه که اتصال به سرور مقصد با همان security context کاربر فعلی (current login) انجام شود یا با یک credential مشخص.'true'→ از لاگین فعلی (impersonate) استفاده میشه (pass-through). این حالت معمولاً برای Windows Authentication و در صورت پیکربندی Kerberos/Delegation کاربرد داره.'false'→ از@rmtuser/@rmtpasswordاستفاده میشه (یعنی شما صریحاً یک نام کاربری و رمز برای سرور مقصد مشخص کردی).
در این مثال چون'false'هست، هر بار که این mapping اعمال بشه اتصال باbranch_readerخواهد بود — نه با لاگین محلی.
@locallogin = 'report_user'
مشخص میکنه این mapping فقط برای چه لاگینی در سرور محلی اعمال بشه.- اگر یک کاربر با لاگین سرور
report_userاز Linked Server استفاده کنه، اتصال بهBranchServerبا credential مشخص شده در@rmtuserانجام میشه. - اگر این پارامتر
NULLبود، یعنی یک mapping عمومی برای همه لاگینهای محلی ساخته شده (همه با همان@rmtuserوصل میشن). - اگر لاگین دیگری (غیر از
report_user) کوئری بزنه و هیچ mapping دیگری نداشته باشیم، اتصال با این mapping انجام نمیشه و یا بسته به تنظیمات، خطا میاره یا از mapping عمومی (اگر وجود داشته باشه) استفاده میشه.
localloginاشاره به Server Login داره (نه صرفاً Database User). یعنی اسم لاگینی که در Instance تعریف شده.- اگر یک کاربر با لاگین سرور
@rmtuser = 'branch_reader'
نام کاربریای روی سرور مقصد هست که قرار است هنگام اتصال استفاده شود.- معمولاً این یک SQL Login در سرور مقصد است (یعنی در آن سرور
CREATE LOGIN branch_readerباید وجود داشته باشد). - سطح دسترسی این حساب روی دیتابیسهای مقصد تعیینکننده عملیاتی است که remote session میتواند انجام دهد (مثلاً فقط SELECT یا اجازه EXEC روی Stored Procedure خاص).
- معمولاً این یک SQL Login در سرور مقصد است (یعنی در آن سرور
@rmtpassword = 'StrongP@ss1'
رمز عبور@rmtuserرا مشخص میکند (وقتی@rmtuserیک SQL Login است).- این مقدار هنگام ذخیره mapping استفاده میشود تا SQL Server هنگام اتصال به سرور مقصد این credentials را ارسال کند.
- هشدار امنیتی: نگهداری password بهصورت متن قابل خواندن در اسکریپتها و مکانهای ناامن میتواند مشکلساز باشد — اگر امکانش هست از Windows Auth یا روشهای امنتر استفاده کن.
نتیجهٔ عملی (وقتی این mapping وجود داشته باشه)
- اگر کاربری با Login سرور
report_userبخواد کوئری روی Linked Server اجرا کنه:SELECT * FROM [BranchServer].[SalesDB].[dbo].[Customers];این اتصال با credentialsbranch_readerو رمزStrongP@ss1به سرورBranchServerبرقرار میشه و عملیات با مجوزهایbranch_readerانجام میشه. - اگر لاگین دیگری (مثلاً
other_user) اقدام کنه، این mapping اعمال نمیشه (مگر اینکه mapping عمومی با@locallogin = NULLوجود داشته باشه یا mapping جداگانهای برای آن لاگین ساخته شده باشد).
دستورات کمکی (برای بررسی / حذف / تست)
- دیدن mappingهای تعریفشده برای یک Linked Server:
EXEC sp_helplinkedsrvlogin 'BranchServer'; - حذف همین mapping:
EXEC sp_droplinkedsrvlogin 'BranchServer', 'report_user'; - تست اینکه کوئری با کدام لاگین remote اجرا میشه:
EXECUTE AS LOGIN = 'report_user'; SELECT * FROM [BranchServer].[SalesDB].[dbo].[Customers]; REVERT;(این باعث میشه session فعلی impersonate شه و mapping شبیه رفتار واقعی تست بشه.)
نکات امنیتی و عملی (خلاصه و مهم)
- در سرور مقصد مطمئن شو
branch_readerوجود داره و فقط مجوزهای لازم (مثلاً فقط SELECT روی جداول مشخص) بهش داده شده. - هرگز برای mapping از حسابهای با امتیاز بالا مثل
saیا sysadmin استفاده نکن. - اگر میتونی از Windows Authentication + Kerberos delegation استفاده کن — امنتره چون رمز در اسکریپت ذخیره نمیشه. اما راهاندازی Kerberos نیاز به پیکربندی AD و delegation داره.
- برای اعمال granular security بهتر است به جای دادن
db_datareaderکلی، فقط مجوز SELECT روی جداول/گزارههای مورد نیاز بده. - اجرای
sp_addlinkedsrvloginمعمولاً نیاز به دسترسی مدیریتی (مثلاً sysadmin یا مالک linked server) داره — پس این کار رو با حسابی که حق مناسب داره انجام بده.
فرض کن دو تا سرور داری:
- سرور اصلی:
MainServer - سرور شعبه:
BranchServer
و روی سرور شعبه یک دیتابیس به اسمSalesDBداریم که جدولCustomersداخلشه.
میخوای از سرور اصلی بتونی مستقیماً به اطلاعات SalesDB روی سرور شعبه دسترسی پیدا کنی.
🧩 مرحله ۱: ساخت Linked Server
اول باید به SQL Server بگی که “یه سرور دیگه به اسم BranchServer وجود داره”.
EXEC sp_addlinkedserver
@server = N'BranchServer',
@srvproduct = N'SQL Server';
🟢 با این دستور، یک Linked Server به اسم BranchServer ساخته شد.
اما هنوز SQL Server نمیدونه با چه یوزری باید به اون وصل بشه.
🔐 مرحله ۲: ساخت یوزر امن روی سرور مقصد (BranchServer)
میریم روی سرور شعبه (BranchServer) و یه یوزر با دسترسی محدود میسازیم:
-- در سطح سرور
CREATE LOGIN branch_reader WITH PASSWORD = 'StrongP@ss1';
GO
-- در دیتابیس SalesDB
USE SalesDB;
CREATE USER branch_reader FOR LOGIN branch_reader;
GO
-- فقط اجازهی SELECT روی تمام جدولها
EXEC sp_addrolemember 'db_datareader', 'branch_reader';
GO
✅ نتیجه:
branch_readerفقط میتونه دیتاها رو بخونه- نمیتونه چیزی حذف یا تغییر بده
- نمیتونه به دیتابیسهای دیگه دسترسی داشته باشه
⚙️ مرحله ۳: تعریف نحوه اتصال در سرور اصلی (MainServer)
حالا باید بگی:
وقتی از سرور اصلی (MainServer) وصل شدم به BranchServer، با چه کاربری لاگین کنم؟
اینجا از sp_addlinkedsrvlogin استفاده میکنیم:
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = NULL, -- یعنی همه کاربران محلی از همین credential استفاده کنن
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
🟢 حالا هر کسی از سرور اصلی بخواد کوئری بزنه روی BranchServer،
SQL Server با یوزر branch_reader به اون سرور وصل میشه.
📊 مرحله ۴: تست ارتباط
الان میتونی از سرور اصلی بنویسی:
SELECT * FROM [BranchServer].[SalesDB].[dbo].[Customers];
✅ و نتیجه جدول از سرور شعبه نمایش داده میشه،
بدون اینکه نیاز باشه دستی فایل بفرستی یا import/export کنی.
🧠 مرحله ۵: امنیت و کنترل
- فقط یوزر
branch_readerبه سرور مقصد وصل میشه. - اون یوزر فقط
SELECTداره (امن). - هیچکس پسورد اصلی سرور شعبه (
sa) رو نداره. - اگر بخوای، میتونی به جای
@locallogin = NULLمشخص کنی فقط یک کاربر خاص در سرور اصلی حق استفاده از Linked Server رو داشته باشه.
🔁 خلاصه کل داستان:
| مرحله | کار | دستور اصلی | توضیح ساده |
|---|---|---|---|
| 1 | معرفی سرور مقصد | sp_addlinkedserver | SQL Server بفهمه یه سرور دیگه وجود داره |
| 2 | ساخت یوزر امن در سرور مقصد | CREATE LOGIN / CREATE USER | یه حساب بساز که فقط بتونه دیتا بخونه |
| 3 | تنظیم ارتباط بین دو سرور | sp_addlinkedsrvlogin | بگو با چه یوزر و رمزی به سرور مقصد وصل بشه |
| 4 | استفاده از اون | SELECT ... FROM [LinkedServer].[DB].[Schema].[Table] | مثل دیتابیس خودت ازش استفاده کن |
هدف
میخوای مثلاً فقط یه لاگین خاص (مثلاً report_user) بتونه از Linked Server استفاده کنه.
نه اینکه هر کاربری در سرور اصلی بتونه از همون branch_reader استفاده کنه.
⚙️ نحوه کار Linked Server از نظر امنیت
وقتی کاربری در سرور اصلی (MainServer) میاد کوئری میزنه:
SELECT * FROM [BranchServer].[SalesDB].[dbo].[Customers];
SQL Server این مسیر رو طی میکنه:
1️⃣ نگاه میکنه ببینه کاربر فعلی (مثلاً report_user) چه mapping ای برای BranchServer داره.
2️⃣ اگه پیدا کرد → با همون credential (rmtuser, rmtpassword) به سرور مقصد وصل میشه.
3️⃣ اگه پیدا نکرد → میره ببینه آیا mapping عمومی وجود داره (@locallogin = NULL) یا نه.
4️⃣ اگه اونم نبود → بسته به تنظیمات Linked Server ممکنه خطا بده یا از credential فعلی استفاده کنه.
🔐 سناریو ۱ — حالت عمومی (همه اجازه دارن)
وقتی اینطوری بنویسی:
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = NULL, -- یعنی برای همه کاربران
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
✅ یعنی هر کاربری روی سرور اصلی (admin، developer، یا هر کس دیگه)
وقتی کوئری بزنه روی [BranchServer],
با یوزر branch_reader به اون سرور وصل میشه.
🔴 این راحت ولی ناامن هست.
چون کنترل جزئی نداری که کی وصل بشه.
🧠 سناریو ۲ — محدود به کاربر خاص (حالت حرفهای و امنتر)
فرض کن فقط report_user باید بتونه از Linked Server استفاده کنه.
در این صورت @locallogin رو مقداردهی میکنی:
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = 'report_user',
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
✅ حالا فقط کسی که با لاگین report_user به سرور اصلی وصل شده
میتونه از Linked Server استفاده کنه.
بقیه کاربرا وقتی کوئری بزنن مثلاً:
SELECT * FROM [BranchServer].[SalesDB].[dbo].[Customers];
🔴 با خطا روبهرو میشن مثل:
Login failed for user ''. The user is not associated with a trusted SQL Server connection.
⚖️ ترکیب هر دو حالت (برای چند یوزر خاص)
میتونی چند mapping جدا بسازی برای چند تا کاربر خاص:
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = 'report_user',
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = 'admin_user',
@rmtuser = 'branch_admin',
@rmtpassword = 'AdminP@ss2';
✅ نتیجه:
report_user→ باbranch_readerوصل میشه (فقط read)admin_user→ باbranch_adminوصل میشه (مثلاً full access)- سایر کاربران → اصلاً اجازه اتصال ندارن
🔍 نکته امنیتی کلیدی
SQL Server همیشه ترتیب رو رعایت میکنه:
- اگر mapping خاص برای لاگین فعلی پیدا کنه → از اون استفاده میکنه.
- اگر نباشه ولی mapping عمومی (
@locallogin = NULL) وجود داشته باشه → از اون استفاده میکنه. - اگر هیچکدوم نباشه → اتصال رد میشه یا با context فعلی تلاش میکنه (که معمولاً شکست میخوره مگر delegation تنظیم شده باشه).
🧩 بررسی و حذف mappingها
میخوای ببینی الان چه mappingهایی برای یه Linked Server داری:
EXEC sp_helplinkedsrvlogin 'BranchServer';
میخوای یکی از mappingها رو حذف کنی:
EXEC sp_droplinkedsrvlogin 'BranchServer', 'report_user';
میخوای mapping عمومی رو حذف کنی (یعنی همه دسترسی عمومی رو از بین ببری):
EXEC sp_droplinkedsrvlogin 'BranchServer', NULL;
🧠 جمعبندی ساده:
| حالت | پارامتر @locallogin | نتیجه |
|---|---|---|
| برای همه کاربران | NULL | همه میتونن وصل شن با یوزر مشترک |
| برای یک کاربر خاص | ‘report_user’ | فقط اون کاربر وصل میشه |
| برای چند کاربر خاص | چند بار اجرای دستور با نامهای مختلف | هرکدوم credential مخصوص خودشون دارن |
| برای هیچکس | هیچ mapping نده | هیچکس اجازه نداره |
در مثال زیر:
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'BranchServer',
@useself = 'false',
@locallogin = 'report_user',
@rmtuser = 'branch_reader',
@rmtpassword = 'StrongP@ss1';
🔹 جواب کوتاه:
هیچکدوم از کاربران معمولی (مثل report_user) نیازی ندارند پسورد 'StrongP@ss1' را بدانند یا وارد کنند.
چون این پسورد داخل تنظیمات Linked Server ذخیره میشود و SQL Server بهصورت خودکار هنگام برقراری اتصال از آن استفاده میکند.
🧠 توضیح کامل:
وقتی شما با این دستور login mapping تعریف میکنی، داری به SQL Server میگی:
«هر وقت کاربر
report_userاز سرور اصلی (مثلاً MainServer) خواست از طریق Linked Server بهBranchServerوصل شود،
SQL Server خودش بهصورت خودکار با یوزرbranch_readerو پسورد'StrongP@ss1'برود به سرور مقصد.»
یعنی:
- کاربر
report_userدر سرور اصلی فقط به Linked Server دسترسی دارد. - خودش مستقیماً با
branch_readerلاگین نمیکند. - SQL Server پشت صحنه، authentication را انجام میدهد.
✅ نتیجه:
| نقش | توضیح |
|---|---|
| report_user | کاربر در سرور اصلی است، کوئری میزند. مثلاً: SELECT * FROM BranchServer.DBName.dbo.Table1 |
| branch_reader | کاربر در سرور مقصد (Linked Server) است، فقط برای خواندن دادهها تعریف شده. |
| StrongP@ss1 | پسوردی که فقط مدیر سیستم میداند، در تنظیمات Linked Server ذخیره میشود، کاربران آن را هرگز وارد نمیکنند. |
🛡️ امنیت:
چون StrongP@ss1 در metadata (و رمزنگاریشده) نگهداری میشود، کاربران نباید هیچوقت به آن دسترسی داشته باشند.
مدیر با دستور زیر میتونه ببیند چه mapping هایی وجود دارد:
EXEC sp_helplinkedsrvlogin 'BranchServer';
ولی پسورد را نمیتواند بخواند (فقط سیستم میداند).