打造 .NET Core 链接转发服务

我最近使用 .NET Core 2.2 造了个名为"Link Forwarder" (链接转发器)的 URL 转发服务,并已开源。目前预览版已部署到我的子域"go.edi.wang"。本文将分享我如何构建这个项目,以及我学到的东西。

为了帮助大家了解系统并浏览代码,请查看我的 GitHub 存储库:https://github.com/EdiWang/LinkForwarder

640?wx_fmt=gif

面向的问题

互联网上的资源有时会更改其 URL。例如,当我 10 年前创建网站时,一个典型的博客文章 URL 就像"https://myolddomain.net/viewarticle.aspx?id=123"。我朋友在其他网站的帖子上引用了这个URL,或讲它发给其他人。几年后,我拥有了一个新域名,并推出了一个新的博客系统,完全改变了该文章的URL,例如"https://edi.wang/post/2009/1/1/an-old-article",这使得任何旧的URL引用都失效。还好我的博客不盈利,所以没太大关系。

但是,这个问题可能发生在企业的产品上。尤其是对于客户端系统和应用程序。比如将产品的支持链接写入安装在客户端的产品中,结果有一天该链接更改了,那么您就必须将所有客户端推送更新。

为了解决这个问题,我想以微软为榜样。微软创建了"go.microsoft.com",它使用不会更改的静态 ID,以重定向到可能随时间变化的实际 URL。例如,https://go.microsoft.com/fwlink/?linkid=2049807  指向的是基于Chromium 的 Edge 浏览器的帮助文档,该文档目前 URL 是 https://microsoftedgesupport.microsoft.com/hc/en-us  。如果文档的 URL 随时间而变化,Edge 浏览器不必更改其内置帮助链接。微软只需要更新其数据库以更改链接 ID 2049807 的目标 URL。这种"go.microsoft.com"服务在微软产品中随处可见。

这是链接转发器的基本思想。

640?wx_fmt=gif

基本流程

管理员为有效的 URL (例如https://www.some-website.com/1234/abcd/1.html) 创建Token URL(例如https://go.edi.wang/fw/e66fad1e)。然后,用户可以使用生成的Token URL 重定向到原始 URL。每次成功重定向都将偷偷记录用户的浏览器 UA 和 IP 地址,以便管理员可以查看报表并暗中观察一切(得加个隐私协议)。

640?wx_fmt=png

报表页面

640?wx_fmt=png

创建/编辑链接

640?wx_fmt=png

分享链接

并非短链接服务

链接转发器非常像,但并不是短链接。关键差异在于:

  • 短链接的目标是创建尽可能短的 URL,通常部署到非常短的域名。链接转发器并不关心是否将其部署到长域名。

  • 大多数短链接服务不允许在创建链接后再修改。但是链接转发器的目标是面向更改。

并不简单

链接转发器不只是将Token映射到 URL。需要考虑以下问题。

它需要足够快,并能处理一定量的流量

我当前的设计会缓存有效的 URL 重定向,因此对于对同一令牌的请求,系统不会每次都查询数据库。

如何处理无效的令牌或有效但不存在的 URL?

对于无效令牌,停止请求。对于该有效的令牌,但它指向不存在的 URL(数据库中没有记录),将用户重定向到预先设置的默认 URL。

系统需要保护用户免受潜在有害链接的侵害

例如,链接转发器的数据库遭到破坏,并且 URL 指向"https://127.0.0.1/some-virus",可以触发一个事先安装在本地的病毒。用户就可能会受到攻击。其他 URL (如"/abc"、"123") 也被视为无效 URL,不会执行重定向。

对于可能包含恶意代码的互联网 URL,目前不在设计范围中。但是,也许将来我们可以集成第三方服务来识别链接。

系统需要自我保护

指向系统本身的链接可能会导致重定向死循环并把服务器爆上天。

例如:

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

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

如果将链接转发器或其他类似的系统部署到另一个域,也会发生类似的情况。甚至可以有多个节点参与在循环中:

640?wx_fmt=png

尽管现代浏览器会停止这种重定向循环,但攻击者可以通过不使用现代浏览器或根本不使用浏览器来绕过此限制。

对于指向服务器域本身的链接,我们可以轻松地识别和阻止它。但对于有多放参与的重定向环,我找不到识别和阻止请求的可靠方法。因此,我只能绕弯解决,将特定时间段内同一 IP 地址的同一令牌的请求数做限制,本文稍后将对此进行说明。

重定向流程

下图说明了URL重定向流程。(手机上看不清可以稍后查看原文)

640?wx_fmt=png

数据库设计

我们只需要两张表就能进行重定向和跟踪用户事件。我选择的数据库引擎是用于开发的 LocalDB 和用于生产的 Microsoft Azure SQL Database

640?wx_fmt=png

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 应用程序设计

为了避免篇幅又臭又长,本文不列出代码的每处细节。完整参考请查看项目 GitHub 仓库:https://github.com/EdiWang/LinkForwarder

LinkForwarder.Web

ASP.NET Core MVC 应用程序作为入口点。它控制 URL 重定向、链接验证、本地帐户或 Azure AD 的身份验证、创建或编辑链接以及查看报告。

LinkForwarder.Services

定义对数据库的 CRUD 操作,并通过 ILinkForwarderService 接口和实现 LinkForwarderService 获取报告数据。稍后解释的 ITokenGenerator 也在此项目中。

LinkForwarder.Setup

用于运行 SQL 脚本以为新服务器设置数据库。这仅在系统的第一次运行中使用。

640?wx_fmt=gif

关键点

Token生成

"/fw"后面的参数是一个 Token。它用于在数据库中查找源 URL。我不使用 Link.Id 的原因是,当执行数据库迁移或从多个服务器合并数据库时,Id 可能会更改。但Token将保持不变。

640?wx_fmt=png

系统使用 ITokenGenerator 接口生成Token。

public interface ITokenGenerator

{

    string GenerateToken();

    bool TryParseToken(string input, out string token);

}

GenerateToken() 用于在提交新 URL 时创建新Token。

TryParseToken() 用于验证客户端请求的Token格式。

目前,ITokenGenerator 接口的唯一实现是ShortGuidTokenGenerator。它将以 GUID 的前 8 个字符作为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() 并不总是可靠的,因为无法判断 8 个字符的字符串是否属于 GUID。您当然可以根据自己的规则创建另一个Token生成器,这些规则可以进行准确的Token验证。

创建新链接

首先,我们需要防止为已经存在的 URL 创建新Token。对于现有 URL,我们可以查找旧记录并返回旧Token,而不是生成新Token。在此之前,我们还需要再次验证现有URL的Token,以确保数据良好。例如,黑客可以将数据库中的Token更改为某个恶意字符串,我不希望它最终追加到 URL 上。

所以,TryParseToken() 必须比我目前的设计更可靠

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

基于这两个因素,创建新链接的代码将是:

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);

验证重定向 URL

系统使用 ILinkVerifier 接口在将其发送到链接到客户端之前验证 URL。有 3 种无效状态:

  • 无效格式: 例如"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 接口执行前两个无效情况的验证。

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 请求速率限制

对于单个 IP,重定向入口 (/fw/{token} ) 在一分钟内最多包含 30 个请求。

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

public async Task<IActionResult> Forward(string token)

appsettings.json中的配置控制 IP 限制规则:

"IpRateLimiting": {

  "EnableEndpointRateLimiting": true,

  "StackBlockedRequests": false,

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

  "ClientIdHeader": "X-ClientId",

  "HttpStatusCode": 429,

  "GeneralRules": [

    {

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

      "Period": "1m",

      "Limit": 30

    }

  ]

}

有关如何进行 IP 速率限制的更完整介绍,请查看我之前的博客文章《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

为了最方便地从中获取信息,我使用一个名为 UAParser 的库。(有了轮子就别自己造,.NET程序员不需要福报)

var uaParser = Parser.GetDefault();


string GetClientTypeName(string userAgent)

{

    ClientInfo c = uaParser.Parse(userAgent);

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

}

此代码允许我按 操作系统-浏览器 对数据进行分组。例如,Windows 7 + Chrome 60 的用户和 Windows 10 + Chrome 62 的用户都将分组为 Windows-Chrome。因此,最终的饼图不会显示太多碎片序列。

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)

        };

还没完事

链接转发器项目处于早期阶段。我能想到很多改进和新功能。例如为第三方提供 REST API、为管理链接添加Tag、甚至在ASP.NET Core 3.0 发布后使用 Blazor。技术上也存在可以优化的地方,比如是否需要引入HASH查找、LinkTracking表到底用不用GUID主键、索引怎么加等等,类似这些需要经过一段时间的线上实践才能做决定。这是一个开源项目,所以我欢迎大家一起帮它变得更牛逼!

640?wx_fmt=jpeg

640?wx_fmt=gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/315361.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

P3293 [SCOI2016]美味 主席树 + 伪01trie

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 思路&#xff1a; 看到异或的话&#xff0c;很容易想到用01trie来贪心的搞&#xff0c;但是这个题涉及区间问题&#xff0c;直接搞的话需要将[l,r][l,r][l,r]的数都插入trie里面&#xff0c;这样的复杂度显然…

Abp 0.18.0 正式发布! -ABP CLI,新模板和其他功能

ABP CLI, v0.18版本新模板和其他功能ABP v0.18已发布, 包含解决的80个issue, 550次提交.网站更改abp.io网站完全更新以突出ABP框架的目标和重要功能.文档和博客网址也会更改&#xff1a;abp.io/documents移至docs.abp.io.abp.io/blog转移到blog.abp.io.ABP CLIABP CLI(命令行界…

牛客练习赛74 E CCA的期望(算概率的技巧+floyd处理)

牛客地址 题目描述 是否经常有艺术创作的冲动&#xff0c;但却限于水平无法描绘&#xff1f;那就交给随机吧&#xff01; 给定一张 n 个点 m 条边的无向带边权连通图&#xff0c;点有颜色&#xff0c;为黑或白&#xff0c;保证无自环和重边。 定义一次操作为&#xff1a;随机选…

Codeforces Round #712 (Div. 2) D. 3-Coloring 交互 构造

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给一个n∗nn*nn∗n的格子染色&#xff0c;一共可以染三种颜色&#xff0c;每次都会给一种颜色&#xff0c;代表当前这种颜色不可以使用&#xff0c;染色要求相邻的颜色不能相同&#xff0c;让你给出一种染色…

短信验证码“最佳实践”

1、背景年初&#xff0c;从外地转移阵地到西安&#xff0c;转眼已两个多月。很久不写业务代码了&#xff0c;到了新公司&#xff0c;条件恶劣到前所未有&#xff0c;从需求&#xff0c;设计&#xff0c;架构&#xff0c;实现&#xff0c;实施&#xff0c;测试&#xff0c;bug修…

Codeforces Round #712 (Div. 2) E. Travelling Salesman Problem 思维转换

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给你nnn个点&#xff0c;从iii到jjj的花费是max(ci,aj−ai)max(c_i,a_j-a_i)max(ci​,aj​−ai​)&#xff0c;求从111开始经过每个点再回到111的最小花费。 思路&#xff1a; 首先可以发现我们的起点在哪…

HDU - 5877 Weak Pair (dfs序+树状数组+离散化)

VJ地址 题意&#xff1a;给一个有根树给你&#xff0c;计算一下满足下列条件的序列对的数目 &#xff08;1&#xff09;u是v的祖先&#xff08;不能是它自己&#xff09; &#xff08;2&#xff09;a[v]*a[u]<k 思路&#xff1a;用DFS序分裂每一条链&#xff0c;使链上的点…

Codeforces Round #614 (Div. 2) D. Aroma‘s Search 暴力 + 思维

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给你x0,y0,ax,ay,bx,byx_0,y_0,a_x,a_y,b_x,b_yx0​,y0​,ax​,ay​,bx​,by​&#xff0c;让后根据[ax∗xi−1bx,ay∗yi−1by][a_x*x_{i-1}b_x,a_y*y_{i-1}b_y][ax​∗xi−1​bx​,ay​∗yi−1​by​]构造出…

Insider Dev Tour 2019巡演中国站

Insider Dev Tour 2019全球巡演中国站落下帷幕&#xff0c;在线直播加六大城市会场&#xff0c;作为四十余位讲师的一份子&#xff0c;我在线分享了Microsoft Graph及Microsoft Teams开发平台的内容&#xff0c;很高兴与几千位开发者共同度过了一个特别的星期天&#xff0c;很不…

HDU - 6267 (概论/找规律/递推)

VJ地址 题目大意&#xff1a; 有n个节点 从0-&#xff08;n-1&#xff09;&#xff0c;连边的规律为 即i点的父亲只能是比i小的数&#xff0c;而且是随机的&#xff0c;现在随机选择应该一个节点作为根&#xff0c;求这子树的和的期望是多少。 思路&#xff1a;可以知道总共有…

Educational Codeforces Round 81 (Rated for Div. 2) D. Same GCDs 欧拉函数\莫比乌斯

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给定a,ma,ma,m&#xff0c;求满足gcd(a,m)gcd(ax,m)gcd(a,m)gcd(ax,m)gcd(a,m)gcd(ax,m)的xxx的个数&#xff0c;且0<x<m0<x<m0<x<m。 思路&#xff1a; 由辗转相除法得&#xff1a;gcd…

Microsoft Graph Toolkit 初探

在今年的Build大会上面, Microsoft Graph 产品组公开宣布了一套新的Web组件&#xff0c;Microsoft Graph Toolkit&#xff08;简称mgt&#xff09;&#xff0c;这套组件可以与任何前端开发平台无缝整合&#xff0c;通过几行代码就能实现基于Microsoft Graph的应用&#xff0c;而…

CodeCraft-21 and Codeforces Round #711 (Div. 2) 题解

先上链接CodeCraft-21 and Codeforces Round #711 (Div. 2) A&#xff1a; 从n开始往后找&#xff0c;不出几十个 一定能找到的&#xff0c;所以暴力就好了 void sovle(){cin>>n;while(1){ll kn;ll sum0;while(k) sumk%10,k/10;if(gcd(n,sum)>1) {cout<<n<&…

Educational Codeforces Round 81 (Rated for Div. 2) B. Infinite Prefixes 数学

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给你个串sss&#xff0c;让后把它重复无限次得到ttt&#xff0c;定义前缀的价值为cnt0−cnt1cnt_0-cnt_1cnt0​−cnt1​&#xff0c;求ttt的前缀价值为xxx的前缀个数&#xff0c;若有无限多输出−1-1−1。 …

终于等到你!微软正式上线 Windows Terminal 预览版

前一段时间&#xff0c;一直在知乎、技术社区收到技术小伙伴们的终极拷问&#xff1a;微软Build 大会上提到的「6月中旬」要上Windows store 的 Windows Terminal 到底啥时候可以用到呀&#xff1f;有一次&#xff0c;我跑去我给 Windows Terminal 提的某个 issue 里问 Windows…

牛客练习赛76 E 牛牛数数(线性基加二分)

牛客地址 思路&#xff1a;全部组合异或&#xff0c;很容易想到使用线性基&#xff0c;正好线性基中有一个求第k小的用法&#xff0c;那我们可以二分来找 K是第几小的数&#xff0c;然后用总数减去。 #include <iostream> #include <cstdio> #include <fstream…

Educational Codeforces Round 81 (Rated for Div. 2) E. Permutation Separation 线段树 + dp

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给你一个打乱的排列&#xff0c;每个位置都各有一个价值&#xff0c;让你选择一个分界点&#xff0c;分成p1,p2,...,prp_1,p_2,...,p_rp1​,p2​,...,pr​和pr1,...,pn−1,pnp_{r1},...,p_{n-1},p_{n}pr1​,…

几种设计良好结构以提高.NET性能的方法

写在前面设计良好的系统&#xff0c;除了架构层面的优良设计外&#xff0c;剩下的大部分就在于如何设计良好的代码&#xff0c;.NET提供了很多的类型&#xff0c;这些类型非常灵活&#xff0c;也非常好用&#xff0c;比如List&#xff0c;Dictionary、HashSet、StringBuilder、…

牛客挑战赛47 D Lots of Edges(最短路+递归枚举子集)

牛客挑战赛47 D Lots of Edges 思路&#xff1a;点的权值最多只有&#xff08;1<<17&#xff09;-1(131071) ,那我们可以枚举终点的值来算最短路&#xff0c;每个点能连边的值都是固定的&#xff0c;可以通过递归枚举子集&#xff08;技巧&#xff09;来找&#xff0c;每…

Codeforces Round #715 (Div. 2) C. The Sports Festival 区间dp

传送门 文章目录题意&#xff1a;思路&#xff1a;题意&#xff1a; 给定一个序列aaa&#xff0c;每次拿出来任意一个数(注意每次选的数不同)&#xff0c;让后定义maxmax(a1,a2,...,ai)maxmax(a_1,a_2,...,a_i)maxmax(a1​,a2​,...,ai​)&#xff0c;minmin(a1,a2,...,ai)min…