打造 .NET Core 链接转发服务
我最近使用 .NET Core 2.2 造了个名为"Link Forwarder" (链接转发器)的 URL 转发服务,并已开源。目前预览版已部署到我的子域"go.edi.wang"。本文将分享我如何构建这个项目,以及我学到的东西。
为了帮助大家了解系统并浏览代码,请查看我的 GitHub 存储库:https://github.com/EdiWang/LinkForwarder
面向的问题
互联网上的资源有时会更改其 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"服务在微软产品中随处可见。
这是链接转发器的基本思想。
基本流程
管理员为有效的 URL (例如https://www.some-website.com/1234/abcd/1.html) 创建Token URL(例如https://go.edi.wang/fw/e66fad1e)。然后,用户可以使用生成的Token URL 重定向到原始 URL。每次成功重定向都将偷偷记录用户的浏览器 UA 和 IP 地址,以便管理员可以查看报表并暗中观察一切(得加个隐私协议)。
报表页面
创建/编辑链接
分享链接
并非短链接服务
链接转发器非常像,但并不是短链接。关键差异在于:
短链接的目标是创建尽可能短的 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
如果将链接转发器或其他类似的系统部署到另一个域,也会发生类似的情况。甚至可以有多个节点参与在循环中:
尽管现代浏览器会停止这种重定向循环,但攻击者可以通过不使用现代浏览器或根本不使用浏览器来绕过此限制。
对于指向服务器域本身的链接,我们可以轻松地识别和阻止它。但对于有多放参与的重定向环,我找不到识别和阻止请求的可靠方法。因此,我只能绕弯解决,将特定时间段内同一 IP 地址的同一令牌的请求数做限制,本文稍后将对此进行说明。
重定向流程
下图说明了URL重定向流程。(手机上看不清可以稍后查看原文)
数据库设计
我们只需要两张表就能进行重定向和跟踪用户事件。我选择的数据库引擎是用于开发的 LocalDB 和用于生产的 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 应用程序设计
为了避免篇幅又臭又长,本文不列出代码的每处细节。完整参考请查看项目 GitHub 仓库:https://github.com/EdiWang/LinkForwarder
LinkForwarder.Web
ASP.NET Core MVC 应用程序作为入口点。它控制 URL 重定向、链接验证、本地帐户或 Azure AD 的身份验证、创建或编辑链接以及查看报告。
LinkForwarder.Services
定义对数据库的 CRUD 操作,并通过 ILinkForwarderService 接口和实现 LinkForwarderService 获取报告数据。稍后解释的 ITokenGenerator 也在此项目中。
LinkForwarder.Setup
用于运行 SQL 脚本以为新服务器设置数据库。这仅在系统的第一次运行中使用。
关键点
Token生成
"/fw"后面的参数是一个 Token。它用于在数据库中查找源 URL。我不使用 Link.Id 的原因是,当执行数据库迁移或从多个服务器合并数据库时,Id 可能会更改。但Token将保持不变。
系统使用 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主键、索引怎么加等等,类似这些需要经过一段时间的线上实践才能做决定。这是一个开源项目,所以我欢迎大家一起帮它变得更牛逼!
打造 .NET Core 链接转发服务相关推荐
- .net core编写转发服务
我有个小伙伴问我,他需要写一个转发服务的他有很多功能要通过他的服务转发~ 技术栈又不一定asp.net core,我就想起泥水老前辈的BeetleX.FastHttpApi 中午午休,折腾了一会儿前辈 ...
- .net core编写转发服务(三) 接入Polly
在web服务里面,很常见出现各种问题,需要一些响应的策略,比如服务繁忙的时候,重试,或者重试等待 服务繁忙的时候根据策略即使处理 关于接入Polly我还是沿用之前的代码,继续迭代 Web Api用的是 ...
- 全场景AI推理引擎MindSpore Lite, 助力HMS Core视频编辑服务打造更智能的剪辑体验
移动互联网的发展给人们的社交和娱乐方式带来了很大的改变,以vlog.短视频等为代表的新兴文化样态正受到越来越多人的青睐.同时,随着AI智能.美颜修图等功能在图像视频编辑App中的应用,促使视频编辑效率 ...
- HMS Core音频编辑服务3D音频技术,助力打造沉浸式听觉盛宴
2022年6月28日,HDD·HMS Core.Sparkle影音娱乐沙龙在线上与开发者们见面.HMS Core音频编辑服务(Audio Editor Kit)专家为大家详细分享了基于分离的3D音乐创 ...
- .NET Core with 微服务 - Consul 配置中心
上一次我们介绍了Elastic APM组件.这一次我们继续介绍微服务相关组件配置中心的使用方法.本来打算介绍下携程开源的重型配置中心框架 apollo 但是体系实在是太过于庞大,还是让我爱不起来.因为 ...
- .Net Core with 微服务 - Consul 注册中心
上一次我们介绍了 Ocelot 网关的基本用法.这次我们开始介绍服务注册发现组件 Consul 的简单使用方法. 服务注册发现 首先先让我们回顾下服务注册发现的概念. 在实施微服务之后,我们的调用都变 ...
- .Net Core with 微服务 - Ocelot 网关
上一次我们通过一张架构图(.Net Core with 微服务 - 架构图)来讲述了微服务的结构,分层等内容.从现在开始我们开始慢慢搭建一个最简单的微服务架构.这次我们先用几个简单的 web api ...
- 【FAQ】接入HMS Core推送服务过程中一些常见问题总结
HMS Core 推送服务(Push Kit)是华为提供的消息推送平台,建立了从云端到终端的消息推送通道.开发者通过集成推送服务,可以向客户端应用实时推送消息,构筑良好的用户关系,提升用户的感知度和活 ...
- 凌华科技aTCA-6200A服务器刀片完美搭配Intel® DPDK技术显著提升包转发服务性能
前言 近年来,随着市场和技术的发展,越来越多的网络基础架构开始向基于通用计算平台或模块化计算平台的架构方向融合,用以支持和提供多样的网络单元和丰富的功能,如应用处理.控制处理.包处理.信号处理等.除了 ...
最新文章
- SQL 语句中 where 条件后 写上1=1 是什么意思
- 日积月累:ProguardGui进行jar包代码混淆
- spring-aop-01
- java gui 读取文件夹_java Swing GUI 入门-文件读写器
- Python selenium环境搭建
- 机器学习中的群论方法
- sync.Map 源码学习
- 设计模式之对象池模式
- Qt工作笔记-把QTableWidget数据存为XML,启动时加载XML
- flutter闪屏过渡动画,闪光占位动画
- 同一工作组无法访问_工作组、AD、域、DC...
- [导入][转载]超强大的jquery formValidator
- 第2章—装配Bean—自动化装配Bean
- gz键盘增强小工具_资深程序员:Python中你不知道的那些小工具
- NAT,PAT、OSPF的相关配置
- mysql explain select_type
- QT学习资料博客:《Qt 实战一二三》和《Qt 学习之路 2》等
- java epoll 模型_I/O多路复用技术详解之epoll模型
- 小米6手机关于 手机重启后密码策略更改 密码错误 及wifi解决办法
- 邮箱容量多大?163邮箱发邮件无限容量解读
热门文章
- audacity_如何在Audacity中快速编辑多个文件
- 在Windows 7中的Windows Media Player 12中快速预览歌曲
- 黑客攻防:从入门到入狱_每日新闻摘要:游戏服务黑客被判入狱27个月
- 什么是Adobe Lightroom,我需要它吗?
- Optaplanner终于支持多线程并行运行 - Multithreaded incremental solving
- mycat 双主 热切换
- Devuan Jessie beta 释出
- MVC捕获数据保存时的具体字段验证错误代码
- 在不同的ObjectContext中更新数据
- 分库分表下极致的优化