日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > asp.net >内容正文

asp.net

打造 .NET Core 链接转发服务

發(fā)布時(shí)間:2023/12/4 asp.net 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 打造 .NET Core 链接转发服务 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

我最近使用 .NET Core 2.2 造了個(gè)名為"Link Forwarder" (鏈接轉(zhuǎn)發(fā)器)的 URL 轉(zhuǎn)發(fā)服務(wù),并已開源。目前預(yù)覽版已部署到我的子域"go.edi.wang"。本文將分享我如何構(gòu)建這個(gè)項(xiàng)目,以及我學(xué)到的東西。

為了幫助大家了解系統(tǒng)并瀏覽代碼,請(qǐng)查看我的 GitHub 存儲(chǔ)庫:https://github.com/EdiWang/LinkForwarder

面向的問題

互聯(lián)網(wǎng)上的資源有時(shí)會(huì)更改其 URL。例如,當(dāng)我 10 年前創(chuàng)建網(wǎng)站時(shí),一個(gè)典型的博客文章 URL 就像"https://myolddomain.net/viewarticle.aspx?id=123"。我朋友在其他網(wǎng)站的帖子上引用了這個(gè)URL,或講它發(fā)給其他人。幾年后,我擁有了一個(gè)新域名,并推出了一個(gè)新的博客系統(tǒng),完全改變了該文章的URL,例如"https://edi.wang/post/2009/1/1/an-old-article",這使得任何舊的URL引用都失效。還好我的博客不盈利,所以沒太大關(guān)系。

但是,這個(gè)問題可能發(fā)生在企業(yè)的產(chǎn)品上。尤其是對(duì)于客戶端系統(tǒng)和應(yīng)用程序。比如將產(chǎn)品的支持鏈接寫入安裝在客戶端的產(chǎn)品中,結(jié)果有一天該鏈接更改了,那么您就必須將所有客戶端推送更新。

為了解決這個(gè)問題,我想以微軟為榜樣。微軟創(chuàng)建了"go.microsoft.com",它使用不會(huì)更改的靜態(tài) ID,以重定向到可能隨時(shí)間變化的實(shí)際 URL。例如,https://go.microsoft.com/fwlink/?linkid=2049807 ?指向的是基于Chromium 的 Edge 瀏覽器的幫助文檔,該文檔目前 URL 是 https://microsoftedgesupport.microsoft.com/hc/en-us ?。如果文檔的 URL 隨時(shí)間而變化,Edge 瀏覽器不必更改其內(nèi)置幫助鏈接。微軟只需要更新其數(shù)據(jù)庫以更改鏈接 ID 2049807 的目標(biāo) URL。這種"go.microsoft.com"服務(wù)在微軟產(chǎn)品中隨處可見。

這是鏈接轉(zhuǎn)發(fā)器的基本思想。

基本流程

管理員為有效的 URL (例如https://www.some-website.com/1234/abcd/1.html) 創(chuàng)建Token URL(例如https://go.edi.wang/fw/e66fad1e)。然后,用戶可以使用生成的Token URL 重定向到原始 URL。每次成功重定向都將偷偷記錄用戶的瀏覽器 UA 和 IP 地址,以便管理員可以查看報(bào)表并暗中觀察一切(得加個(gè)隱私協(xié)議)。

報(bào)表頁面

創(chuàng)建/編輯鏈接

分享鏈接

并非短鏈接服務(wù)

鏈接轉(zhuǎn)發(fā)器非常像,但并不是短鏈接。關(guān)鍵差異在于:

  • 短鏈接的目標(biāo)是創(chuàng)建盡可能短的 URL,通常部署到非常短的域名。鏈接轉(zhuǎn)發(fā)器并不關(guān)心是否將其部署到長域名。

  • 大多數(shù)短鏈接服務(wù)不允許在創(chuàng)建鏈接后再修改。但是鏈接轉(zhuǎn)發(fā)器的目標(biāo)是面向更改。

并不簡單

鏈接轉(zhuǎn)發(fā)器不只是將Token映射到 URL。需要考慮以下問題。

它需要足夠快,并能處理一定量的流量

我當(dāng)前的設(shè)計(jì)會(huì)緩存有效的 URL 重定向,因此對(duì)于對(duì)同一令牌的請(qǐng)求,系統(tǒng)不會(huì)每次都查詢數(shù)據(jù)庫。

如何處理無效的令牌或有效但不存在的 URL?

對(duì)于無效令牌,停止請(qǐng)求。對(duì)于該有效的令牌,但它指向不存在的 URL(數(shù)據(jù)庫中沒有記錄),將用戶重定向到預(yù)先設(shè)置的默認(rèn) URL。

系統(tǒng)需要保護(hù)用戶免受潛在有害鏈接的侵害

例如,鏈接轉(zhuǎn)發(fā)器的數(shù)據(jù)庫遭到破壞,并且 URL 指向"https://127.0.0.1/some-virus",可以觸發(fā)一個(gè)事先安裝在本地的病毒。用戶就可能會(huì)受到攻擊。其他 URL (如"/abc"、"123") 也被視為無效 URL,不會(huì)執(zhí)行重定向。

對(duì)于可能包含惡意代碼的互聯(lián)網(wǎng) URL,目前不在設(shè)計(jì)范圍中。但是,也許將來我們可以集成第三方服務(wù)來識(shí)別鏈接。

系統(tǒng)需要自我保護(hù)

指向系統(tǒng)本身的鏈接可能會(huì)導(dǎo)致重定向死循環(huán)并把服務(wù)器爆上天。

例如:

https://go.edi.wang/fw/a? 指向 https://go.edi.wang/fw/b?

https://go.edi.wang/fw/b?又指向 https://go.edi.wang/fw/a?

如果將鏈接轉(zhuǎn)發(fā)器或其他類似的系統(tǒng)部署到另一個(gè)域,也會(huì)發(fā)生類似的情況。甚至可以有多個(gè)節(jié)點(diǎn)參與在循環(huán)中:

盡管現(xiàn)代瀏覽器會(huì)停止這種重定向循環(huán),但攻擊者可以通過不使用現(xiàn)代瀏覽器或根本不使用瀏覽器來繞過此限制。

對(duì)于指向服務(wù)器域本身的鏈接,我們可以輕松地識(shí)別和阻止它。但對(duì)于有多放參與的重定向環(huán),我找不到識(shí)別和阻止請(qǐng)求的可靠方法。因此,我只能繞彎解決,將特定時(shí)間段內(nèi)同一 IP 地址的同一令牌的請(qǐng)求數(shù)做限制,本文稍后將對(duì)此進(jìn)行說明。

重定向流程

下圖說明了URL重定向流程。(手機(jī)上看不清可以稍后查看原文)

數(shù)據(jù)庫設(shè)計(jì)

我們只需要兩張表就能進(jìn)行重定向和跟蹤用戶事件。我選擇的數(shù)據(jù)庫引擎是用于開發(fā)的 LocalDB 和用于生產(chǎn)的 Microsoft Azure SQL Database

SQL腳本:

IF NOT EXISTS(SELECT TOP 1 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'Link')

CREATE TABLE [Link](

[Id] [int] IDENTITY(1,1) PRIMARY KEY NOT NULL,

[OriginUrl] [nvarchar](256) NULL,

[FwToken] [varchar](32) NULL,

[Note] [nvarchar](max) NULL,

[IsEnabled] [bit] NOT NULL,

[UpdateTimeUtc] [datetime] NOT NULL)


IF NOT EXISTS(SELECT TOP 1 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'LinkTracking')

CREATE TABLE [LinkTracking](

[Id] UNIQUEIDENTIFIER PRIMARY KEY NOT NULL,

[LinkId] [int] NOT NULL,

[UserAgent] [nvarchar](256) NULL,

[IpAddress] [varchar](64) NULL,

[RequestTimeUtc] [datetime] NOT NULL)


IF NOT EXISTS(SELECT TOP 1 1 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = N'FK_LinkTracking_Link')

ALTER TABLE [LinkTracking]? WITH CHECK ADD? CONSTRAINT [FK_LinkTracking_Link] FOREIGN KEY([LinkId])

REFERENCES [Link] ([Id])

ON UPDATE CASCADE

ON DELETE CASCADE

ALTER TABLE [LinkTracking] CHECK CONSTRAINT [FK_LinkTracking_Link]

ASP.NET Core 應(yīng)用程序設(shè)計(jì)

為了避免篇幅又臭又長,本文不列出代碼的每處細(xì)節(jié)。完整參考請(qǐng)查看項(xiàng)目 GitHub 倉庫:https://github.com/EdiWang/LinkForwarder

LinkForwarder.Web

ASP.NET Core MVC 應(yīng)用程序作為入口點(diǎn)。它控制 URL 重定向、鏈接驗(yàn)證、本地帳戶或 Azure AD 的身份驗(yàn)證、創(chuàng)建或編輯鏈接以及查看報(bào)告。

LinkForwarder.Services

定義對(duì)數(shù)據(jù)庫的 CRUD 操作,并通過 ILinkForwarderService?接口和實(shí)現(xiàn) LinkForwarderService 獲取報(bào)告數(shù)據(jù)。稍后解釋的 ITokenGenerator?也在此項(xiàng)目中。

LinkForwarder.Setup

用于運(yùn)行 SQL 腳本以為新服務(wù)器設(shè)置數(shù)據(jù)庫。這僅在系統(tǒng)的第一次運(yùn)行中使用。

關(guān)鍵點(diǎn)

Token生成

"/fw"后面的參數(shù)是一個(gè) Token。它用于在數(shù)據(jù)庫中查找源 URL。我不使用 Link.Id 的原因是,當(dāng)執(zhí)行數(shù)據(jù)庫遷移或從多個(gè)服務(wù)器合并數(shù)據(jù)庫時(shí),Id 可能會(huì)更改。但Token將保持不變。

系統(tǒng)使用 ITokenGenerator?接口生成Token。

public interface ITokenGenerator

{

? ? string GenerateToken();

? ? bool TryParseToken(string input, out string token);

}

GenerateToken() 用于在提交新 URL 時(shí)創(chuàng)建新Token。

TryParseToken() 用于驗(yàn)證客戶端請(qǐng)求的Token格式。

目前,ITokenGenerator?接口的唯一實(shí)現(xiàn)是ShortGuidTokenGenerator。它將以 GUID 的前 8 個(gè)字符作為Token。

public class ShortGuidTokenGenerator : ITokenGenerator

{

? ? private const int Length = 8;


? ? public string GenerateToken()

? ? {

? ? ? ? return Guid.NewGuid().ToString().Substring(0, Length).ToLower();

? ? }


? ? public bool TryParseToken(string input, out string token)

? ? {

? ? ? ? token = null;

? ? ? ? if (input.Length != Length)

? ? ? ? {

? ? ? ? ? ? return false;

? ? ? ? }


? ? ? ? token = input;

? ? ? ? return true;

? ? }

}

注意:在此示例中,TryParseToken() 并不總是可靠的,因?yàn)闊o法判斷 8 個(gè)字符的字符串是否屬于 GUID。您當(dāng)然可以根據(jù)自己的規(guī)則創(chuàng)建另一個(gè)Token生成器,這些規(guī)則可以進(jìn)行準(zhǔn)確的Token驗(yàn)證。

創(chuàng)建新鏈接

首先,我們需要防止為已經(jīng)存在的 URL 創(chuàng)建新Token。對(duì)于現(xiàn)有 URL,我們可以查找舊記錄并返回舊Token,而不是生成新Token。在此之前,我們還需要再次驗(yàn)證現(xiàn)有URL的Token,以確保數(shù)據(jù)良好。例如,黑客可以將數(shù)據(jù)庫中的Token更改為某個(gè)惡意字符串,我不希望它最終追加到 URL 上。

所以,TryParseToken() 必須比我目前的設(shè)計(jì)更可靠。

其次,我們需要防止生成已存在的令牌。完整 GUID 是可靠的,但部分 GUID 不是。

基于這兩個(gè)因素,創(chuàng)建新鏈接的代碼將是:

const string sqlLinkExist = "SELECT TOP 1 FwToken FROM Link l WHERE l.OriginUrl = @originUrl";

var tempToken = await conn.ExecuteScalarAsync<string>(sqlLinkExist, new { originUrl });

if (null != tempToken)

{

? ? if (_tokenGenerator.TryParseToken(tempToken, out var tk))

? ? {

? ? ? ? _logger.LogInformation($"Link already exists for token '{tk}'");

? ? ? ? return new SuccessResponse<string>(tk);

? ? }


? ? string message = $"Invalid token '{tempToken}' found for existing url '{originUrl}'";

? ? _logger.LogError(message);

}


const string sqlTokenExist = "SELECT TOP 1 1 FROM Link l WHERE l.FwToken = @token";

string token;

do

{

? ? token = _tokenGenerator.GenerateToken();

} while (await conn.ExecuteScalarAsync<int>(sqlTokenExist, new { token }) == 1);


_logger.LogInformation($"Generated Token '{token}' for url '{originUrl}'");


var link = new Link

{

? ? FwToken = token,

? ? IsEnabled = isEnabled,

? ? Note = note,

? ? OriginUrl = originUrl,

? ? UpdateTimeUtc = DateTime.UtcNow

};

const string sqlInsertLk = @"INSERT INTO Link (OriginUrl, FwToken, Note, IsEnabled, UpdateTimeUtc)

? ? ? ? ? ? ? ? ? ? ? ? ? ? ?VALUES (@OriginUrl, @FwToken, @Note, @IsEnabled, @UpdateTimeUtc)";

await conn.ExecuteAsync(sqlInsertLk, link);

return new SuccessResponse<string>(link.FwToken);

驗(yàn)證重定向 URL

系統(tǒng)使用 ILinkVerifier 接口在將其發(fā)送到鏈接到客戶端之前驗(yàn)證 URL。有 3 種無效狀態(tài):

  • 無效格式: 例如"865c8gyiB"

  • 本地 URL: 例如"/some-path"

  • 自引用 URL: 例如"https://go.edi.wang/some-path"

public enum LinkVerifyResult

{

? ? Valid,

? ? InvalidFormat,

? ? InvalidLocal,

? ? InvalidSelfReference

}


public interface ILinkVerifier

{

? ? LinkVerifyResult Verify(string url, IUrlHelper urlHelper, HttpRequest currentRequest);

}

我們可以利用ASP.NET MVC 的 IUrlHelper 接口執(zhí)行前兩個(gè)無效情況的驗(yàn)證。

public LinkVerifyResult Verify(string url, IUrlHelper urlHelper, HttpRequest currentRequest)

{

? ? if (!url.IsValidUrl())

? ? {

? ? ? ? return LinkVerifyResult.InvalidFormat;

? ? }


? ? if (urlHelper.IsLocalUrl(url))

? ? {

? ? ? ? return LinkVerifyResult.InvalidLocal;

? ? }


? ? if (Uri.TryCreate(url, UriKind.Absolute, out var testUri))

? ? {

? ? ? ? if (string.Compare(testUri.Authority, currentRequest.Host.ToString(), StringComparison.OrdinalIgnoreCase) == 0

? ? ? ? ? ? && string.Compare(testUri.Scheme, currentRequest.Scheme, StringComparison.OrdinalIgnoreCase) == 0

? ? ? ? ? ? && testUri.AbsolutePath != "/")

? ? ? ? {

? ? ? ? ? ? return LinkVerifyResult.InvalidSelfReference;

? ? ? ? }

? ? }


? ? return LinkVerifyResult.Valid;

}

要檢查 URL 是否采用有效格式:

public enum UrlScheme

{

? ? Http,

? ? Https,

? ? All

}


public static bool IsValidUrl(this string url, UrlScheme urlScheme = UrlScheme.All)

{

? ? bool isValidUrl = Uri.TryCreate(url, UriKind.Absolute, out var uriResult);

? ? if (!isValidUrl)

? ? {

? ? ? ? return false;

? ? }


? ? switch (urlScheme)

? ? {

? ? ? ? case UrlScheme.All:

? ? ? ? ? ? isValidUrl &= uriResult.Scheme == Uri.UriSchemeHttps || uriResult.Scheme == Uri.UriSchemeHttp;

? ? ? ? ? ? break;

? ? ? ? case UrlScheme.Https:

? ? ? ? ? ? isValidUrl &= uriResult.Scheme == Uri.UriSchemeHttps;

? ? ? ? ? ? break;

? ? ? ? case UrlScheme.Http:

? ? ? ? ? ? isValidUrl &= uriResult.Scheme == Uri.UriSchemeHttp;

? ? ? ? ? ? break;

? ? }


? ? return isValidUrl;

}

IP 請(qǐng)求速率限制

對(duì)于單個(gè) IP,重定向入口 (/fw/{token} ) 在一分鐘內(nèi)最多包含 30 個(gè)請(qǐng)求。

[Route("/fw/{token}")]

public async Task<IActionResult> Forward(string token)

appsettings.json中的配置控制 IP 限制規(guī)則:

"IpRateLimiting": {

? "EnableEndpointRateLimiting": true,

? "StackBlockedRequests": false,

? "RealIpHeader": "X-Real-IP",

? "ClientIdHeader": "X-ClientId",

? "HttpStatusCode": 429,

? "GeneralRules": [

? ? {

? ? ? "Endpoint": "*:/fw/*",

? ? ? "Period": "1m",

? ? ? "Limit": 30

? ? }

? ]

}

有關(guān)如何進(jìn)行 IP 速率限制的更完整介紹,請(qǐng)查看我之前的博客文章《IP Rate Limit for ASP.NET Core》 https://edi.wang/post/2019/6/16/ip-rate-limit-for-aspnet-core

從User Agent里暗中觀察

典型的 User Agent 字符串如下:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.12 Safari/537.36 Edg/76.0.182.6

為了最方便地從中獲取信息,我使用一個(gè)名為 UAParser 的庫。(有了輪子就別自己造,.NET程序員不需要福報(bào))

var uaParser = Parser.GetDefault();


string GetClientTypeName(string userAgent)

{

? ? ClientInfo c = uaParser.Parse(userAgent);

? ? return $"{c.OS.Family}-{c.UA.Family}";

}

此代碼允許我按 操作系統(tǒng)-瀏覽器 對(duì)數(shù)據(jù)進(jìn)行分組。例如,Windows 7 + Chrome 60 的用戶和 Windows 10 + Chrome 62 的用戶都將分組為 Windows-Chrome。因此,最終的餅圖不會(huì)顯示太多碎片序列。

var q = from d in userAgentCounts

? ? ? ? group d by GetClientTypeName(d.UserAgent)

? ? ? ? into g

? ? ? ? select new ClientTypeCount

? ? ? ? {

? ? ? ? ? ? ClientTypeName = g.Key,

? ? ? ? ? ? Count = g.Sum(gp => gp.RequestCount)

? ? ? ? };

還沒完事

鏈接轉(zhuǎn)發(fā)器項(xiàng)目處于早期階段。我能想到很多改進(jìn)和新功能。例如為第三方提供 REST API、為管理鏈接添加Tag、甚至在ASP.NET Core 3.0 發(fā)布后使用 Blazor。技術(shù)上也存在可以優(yōu)化的地方,比如是否需要引入HASH查找、LinkTracking表到底用不用GUID主鍵、索引怎么加等等,類似這些需要經(jīng)過一段時(shí)間的線上實(shí)踐才能做決定。這是一個(gè)開源項(xiàng)目,所以我歡迎大家一起幫它變得更牛逼!

總結(jié)

以上是生活随笔為你收集整理的打造 .NET Core 链接转发服务的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。