.NET?API?接口數(shù)據(jù)傳輸加密最佳實(shí)踐記錄
我們?cè)谧?Api 接口時(shí),相信一定會(huì)有接觸到要給傳輸?shù)恼?qǐng)求 body 的內(nèi)容進(jìn)行加密傳輸。其目的就是為了防止一些敏感的內(nèi)容直接被 UI 層查看或篡改。
其實(shí)粗略一想就能想到很多種方案,但是哪些方案是目前最適合我們項(xiàng)目的呢?
硬編碼方式
最先想到的應(yīng)該就是硬編碼方式,就是哪個(gè)接口需要進(jìn)行傳輸加密,那么就針對(duì)該接口特殊處理:
public class SecurityApiController { ... public async Task<Result> UpdateUser([FromBody] SecurityRequest request) { var requestBody = RsaHelper.Decrypt(privateKey, request.Content); var user = JsonHelper.Deserialize<UserDto>(requestBody); await UpdateUserAsync(user); return new Result(RsaHelper.Encrypt(publicKey, new{ Success=true})); } }
這種方式好處是簡(jiǎn)單明了,按需編程即可,不會(huì)對(duì)其它接口造成污染。
一旦這種需求越來(lái)越多,我們就會(huì)寫大量如上的重復(fù)性代碼;而對(duì)于前端而言也是如此,所以當(dāng)我們需要傳輸加密乃是最基礎(chǔ)的需求時(shí),上面硬編碼的方式就顯得很不合適了。
這個(gè)時(shí)候我們可以采用統(tǒng)一入口的方式來(lái)實(shí)現(xiàn)
統(tǒng)一入口
回顧上面的硬編碼方式,其實(shí)每個(gè)接口處的加解密處理從 SRP 原則上理解,不應(yīng)該是接口的職責(zé)。所以需要把這部分的代碼移到一個(gè)單獨(dú)的方法,再加解密之后我們?cè)侔言撜?qǐng)求調(diào)度到具體的接口。
這種方式其實(shí)有很多種實(shí)現(xiàn)方式,在這里我先說(shuō)一下我司其中一個(gè) .NET4.5 的項(xiàng)目采取的方式。
其實(shí)就是額外提供了一個(gè)統(tǒng)一的入口,所有需要傳輸加密的需求都走這一個(gè)接口:如http://api.example.com/security
public class SecurityController { ... public async Task<object> EntryPoint([FromBody] SecurityRequest request) { var requestBody = RsaHelper.Decrypt(privateKey, request.Content); var user = JsonHelper.Deserialize<UserDto>(requestBody); var obj = await DispathRouter(requestBody.Router, user); return new Result(RsaHelper.Encrypt(publicKey, obj)); } public async Task<object> DispathRouter(Router router, object body) { ... Type objectCon = typeof(BaseController); MethodInfo methInfo = objectCon.GetMethod(router.Name); var resp = (Task<object>)methInfo.Invoke(null, body); return await resp; } }
很明顯這是通過(guò)統(tǒng)一入口地址調(diào)用并配合反射來(lái)實(shí)現(xiàn)這種目的。
這種好處如前面所說(shuō),統(tǒng)一了調(diào)用入口,這樣提高了代碼復(fù)用率,讓加解密不再是業(yè)務(wù)接口的一部分了。同樣,這種利用一些不好的點(diǎn);比如用了反射性能會(huì)大打折扣。并且我們過(guò)度的進(jìn)行統(tǒng)一了。我們看到這種方式只能將所有的接口方法都寫到 BaseController。所以我司項(xiàng)目的 Controller 部分,會(huì)看到大量如下的寫法:
// 文件 UserController.cs public partial class BaseController { ... } // 文件 AccountController.cs public partial class BaseController { } // ...
這樣勢(shì)必就會(huì)導(dǎo)致一個(gè)明顯的問(wèn)題,就是“代碼爆炸”。這相當(dāng)于將所有的業(yè)務(wù)邏輯全部灌輸?shù)揭粋€(gè)控制器中,剛開(kāi)始寫的時(shí)候方便了,但是后期維護(hù)以及交接換人的時(shí)候閱讀代碼是非常痛苦的一個(gè)過(guò)程。因?yàn)樵诓煌?Controller 文件中勢(shì)必會(huì)重復(fù)初始化一些模塊,而我們?cè)谝梅椒ǖ臅r(shí)候 IDE 每次都會(huì)顯示上千個(gè)方法,有時(shí)候還不得不查看哪些方法名一樣或相近的具體意義。
針對(duì)上述代碼爆炸的方式還有一種優(yōu)化,就是將控制器的選擇開(kāi)放給應(yīng)用端,也就是將方法名和控制器名都作為請(qǐng)求參數(shù)暴露給前端,但是這樣會(huì)加大前端的開(kāi)發(fā)心智負(fù)擔(dān)。
綜上所述我是非常不建議采用這種方式的。雖說(shuō)是很古老的.Net4/4.5 的框架,但是我們還是有其它相對(duì)更優(yōu)雅的實(shí)現(xiàn)方式。
中間件
其實(shí)我們熟悉了 .NETCore 下的 Middleware機(jī)制,我們會(huì)很容易的在 .NETCore 下實(shí)現(xiàn)如標(biāo)題的這種需求:
// .NET Core 版本 public class SecuriryTransportMiddleware { private readonly RequestDelegate _next; public RequestCultureMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { // request handle var encryptedBody = context.Request.Body; var encryptedContent = new StreamReader(encryptedBody).ReadToEnd(); var decryptedBody = RsaHelper.Decrypt(privateKey, encryptedContent); var originBody = JsonHelper.Deserialize<object>(decryptedBody); var json = JsonHelper.Serialize(dataSource); var requestContent = new StringContent(json, Encoding.UTF8, "application/json"); stream = await requestContent.ReadAsStreamAsync(); context.Request.Body = stream; await _next(context); // response handle var originContent = new StreamReader(context.Response.Body).ReadToEnd(); var encryptedBody = RsaHelper.Encrypt(privateKey, originContent); var responseContent = new StringContent(json, Encoding.UTF8, "application/json"); context.Response.Body = await responseContent.ReadAsStreamAsync(); // 或者直接 // await context.Response.WriteAsync(encryptedBody); } }
為了方便描述,以上代碼我省略了必要的校驗(yàn)和異常錯(cuò)誤處理
這樣有什么好處呢?一個(gè)最明顯的好處就是解耦了加解密與真正業(yè)務(wù)需求。對(duì)真正的業(yè)務(wù)代碼幾乎沒(méi)有侵略性。其實(shí)我認(rèn)為業(yè)務(wù)開(kāi)發(fā)能做到這里其實(shí)就差不多了,還有其它需求都可以基于這個(gè)中間件進(jìn)行拓展開(kāi)發(fā)。
那么在 .NET Framwork 沒(méi)有中間件怎么辦呢?
其實(shí)在 .NET Framwork 框架下 IHttpModule 能和中間件一樣能做到這點(diǎn):
public class SecurityTransportHttpModule : IHttpModule { ... public void Init(HttpApplication context) { ... context.BeginRequest += ContextBeginRequest; context.PostRequestHandlerExecute += ContextPostRequestHandlerExecute; } private void ContextBeginRequest(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; var encryptedBody = context.Request.Body; ... context.Request.Body = stream; } private void ContextPostRequestHandlerExecute(object sender, EventArgs e) { HttpContext context = ((HttpApplication)sender).Context; ... context.Response.Write(encryptedBody) } }
為什么之前提到這種方案就“差不多”了呢,實(shí)際上上面這種方式在某些場(chǎng)景下會(huì)顯得比較“累贅”。因?yàn)闊o(wú)論通過(guò)中間件和還是 IHttpModule 都會(huì)導(dǎo)致所有請(qǐng)求都會(huì)經(jīng)過(guò)它,相當(dāng)于增加了一個(gè)過(guò)濾器,如果這時(shí)候我要新增一個(gè)上傳文件接口,那必然也會(huì)經(jīng)過(guò)這個(gè)處理程序。說(shuō)的更直接一點(diǎn),如果碰到那些少數(shù)不需要加解密的接口請(qǐng)求那要怎么辦呢?
其實(shí)上面可以進(jìn)行拓展處理,比如對(duì)特定的請(qǐng)求進(jìn)行過(guò)濾:
if(context.Request.Path.Contains("upload")) { return; }
注意上述代碼只是做個(gè) demo 展示,真正還是需要通過(guò)如
context.GetRouterData()
獲取路由數(shù)據(jù)進(jìn)行精準(zhǔn)比對(duì)。
當(dāng)類似于這種需求開(kāi)始變多以后(吐槽:誰(shuí)知道業(yè)務(wù)是怎么發(fā)展的呢?)原來(lái)的中間件的“任務(wù)量”開(kāi)始變得厚重了起來(lái)。到時(shí)候也會(huì)變得難以維護(hù)和閱讀。
這個(gè)時(shí)候就是我目前較為滿意的解決方案登場(chǎng)了,它就是模型綁定 ModelBinding。
模型綁定
回到需求的開(kāi)端,不難發(fā)現(xiàn),我們其實(shí)要是如何將前端加密后的請(qǐng)求體綁定到我們編寫的接口方法中。這里面的過(guò)程很復(fù)雜,需要解析前端發(fā)起的請(qǐng)求,解密之后還要反序列化成目標(biāo)接口需要的方法參數(shù)。而這個(gè)過(guò)程還要伴隨著參數(shù)校驗(yàn),如這個(gè)請(qǐng)求是否符合加密格式。而這個(gè)過(guò)程的一切都是模型綁定要解決的事。我們以 NETCore 版本為例子,講一下大概的流程;
模型綁定的過(guò)程其實(shí)就是將請(qǐng)求體的各個(gè)字段于具體的 CLR 類型的字段屬性進(jìn)行一一匹配的過(guò)程。.NetCore 再程序啟動(dòng)時(shí)會(huì)默認(rèn)提供了一些內(nèi)置的模型綁定器,并開(kāi)放 IModelBinderProvider 接口允許用戶自定義模型綁定器。我們通過(guò)查看 MvcCoreMvcOptionsSetup 就清楚看到框架為我們添加 18 個(gè)自帶的模型綁定器。以及如何調(diào)用的方式。
所以接下來(lái)我們很容易的可以一葫蘆畫瓢的照抄下來(lái):
public class SecurityTransportModelBinder : IModelBinder { ... public async Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } try { var request = bindingContext.HttpContext.Request; var model = await JsonSerializer.DeserializeAsync<SafeDataWrapper>(request.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); var decryptContent = RsaHelper.Decrypt(model.Info, privateKey); var activateModel = JsonSerializer.Deserialize(decryptContent, bindingContext.ModelMetadata.ModelType, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }); //重新包裝 if (activateModel == null) { bindingContext.Result = ModelBindingResult.Failed(); } else { bindingContext.Result = ModelBindingResult.Success(activateModel); } } catch (Exception exception) { bindingContext.ModelState.TryAddModelError( bindingContext.ModelName, exception, bindingContext.ModelMetadata); } _logger.DoneAttemptingToBindModel(bindingContext); //return Task.CompletedTask; } }
抄了 ModelBinder 還不行,還要抄 ModelBinderProvider:
public class SecurityTransportModelBinderProvider : IModelBinderProvider { public IModelBinder GetBinder(ModelBinderProviderContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (context.Metadata.IsComplexType && typeof(IApiEncrypt).IsAssignableFrom(context.Metadata.ModelType)) { var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>(); var configuration = context.Services.GetRequiredService<IConfiguration>(); return new SecurityTransportModelBinder(loggerFactory, configuration); } return null; } }
這里我做了一個(gè)方便我自己的拓展功能,就是顯示打了 IApiEncrypt
接口標(biāo)簽的才會(huì)正常進(jìn)行解析綁定。
剩下的就是在 ConfigureService 中添加進(jìn)去即可:
services.AddControllers(options => { ... options.ModelBinderProviders.Insert(0, new SecurityTransportModelBinderProvider()); })
這樣實(shí)現(xiàn)過(guò)后,我們就能像使用 FromBody
那樣就能按需調(diào)用即可:
[HttpPost("security")] public async Task<ResultDto> DemoDecrypt([ModelBinder(typeof(SecurityTransportModelBinder))] OriginBusinessRequest request) { //激活結(jié)果 ... return await Task.FromResult(WriteSafeData(data, publicKey)); }
如果是默認(rèn)處理加解密也是可以的,直接在對(duì)應(yīng)的請(qǐng)求實(shí)體類打上 IApiEncrypt
標(biāo)簽就會(huì)自動(dòng)執(zhí)行模型綁定
public class UserUpdateRequest: IApiEncrypt { public int UserId { get; set; } public string Phone { get; set; } public string Address { get; set; } ... }
這種方案其實(shí)也還是有缺點(diǎn)的,從剛剛的使用來(lái)看就知道,模型綁定無(wú)法解決返回自動(dòng)加密處理。所以我們不得不在每個(gè)接口處寫下如 WriteSafeData(data, publicKey)
這種顯式加密的代碼。
優(yōu)化的方式也很簡(jiǎn)單,其實(shí)我們可以通過(guò)過(guò)濾器可以解決,這也是為什么我要加 IApiEncrypt
的原因,因?yàn)橛辛诉@個(gè)就能確定知道這是一個(gè)安全傳輸?shù)恼?qǐng)求,進(jìn)而進(jìn)行特殊處理。
注意,這不是 .NET Core 獨(dú)有的特性,.Net Framework 也有模型綁定器
總結(jié)
針對(duì)接口級(jí)傳輸加密這個(gè)需求,我們總共討論了四種實(shí)現(xiàn)方式。其實(shí)各有各的好處和缺點(diǎn)。
硬編碼方式適合只有特定需求的場(chǎng)景下是適合這種方案的。但是一旦這種需求成為一種規(guī)范或普遍場(chǎng)景時(shí),這個(gè)方法就不合適了
統(tǒng)一入口適合制定了一種接口規(guī)范,所有加密的請(qǐng)求都走這一個(gè)接口。然后通過(guò)路由解析調(diào)度到不同的控制器。其實(shí)我講了我司在 .NET Framework 下采取的一種方案,好處時(shí)實(shí)現(xiàn)簡(jiǎn)單,做到了代碼復(fù)用,對(duì)業(yè)務(wù)代碼進(jìn)行了解耦。但缺點(diǎn)是用了反射成為了性能消耗點(diǎn),并且當(dāng)業(yè)務(wù)越來(lái)越多就會(huì)產(chǎn)生代碼爆炸,成為了維護(hù)災(zāi)難。
而中間件或 IHttpModule 方式就很好的解決這了這點(diǎn)。但同樣也不是使用所有場(chǎng)景,如對(duì)新增的不需要加密的接口要進(jìn)行過(guò)濾處理等。
最后介紹了模型綁定這種方式,技能很方便的滿足大多數(shù)場(chǎng)景,也滿足個(gè)別列外的需求。但同樣也有缺點(diǎn),就是無(wú)法針對(duì)接口響應(yīng)體進(jìn)行自動(dòng)加密處理,所謂好事做到底,送佛送到西,這事只能算是做到一半吧。
其實(shí)我還想到了一種類似 Aop 的方案,那就是實(shí)現(xiàn)一個(gè)路由過(guò)濾器的功能,當(dāng)請(qǐng)求進(jìn)來(lái),通過(guò)路由處理程序?qū)φ?qǐng)求體進(jìn)行解密,然后重寫請(qǐng)求流。然后調(diào)度到對(duì)應(yīng)的原始路由,最后在響應(yīng)的時(shí)候再加密重寫一次。不過(guò)查閱了一番資料,并沒(méi)有收獲。
參考資料
- https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.imodelbinderprovider?view=aspnetcore-7.0
- https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write?view=aspnetcore-6.0
- https://www.stevejgordon.co.uk/html-encode-string-aspnet-core-model-binding
到此這篇關(guān)于.NET API 接口數(shù)據(jù)傳輸加密最佳實(shí)踐記錄的文章就介紹到這了,更多相關(guān).net api接口數(shù)據(jù)傳輸加密內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET調(diào)用WebService服務(wù)的方法詳解
這篇文章主要介紹了ASP.NET調(diào)用WebService服務(wù)的方法,較為詳細(xì)的分析了WebService服務(wù)的功能,創(chuàng)建步驟與使用方法,需要的朋友可以參考下2016-05-05此頁(yè)的狀態(tài)信息無(wú)效,可能已損壞 的處理辦法及原因分析
此頁(yè)的狀態(tài)信息無(wú)效,可能已損壞 的處理辦法及原因分析,需要的朋友可以參考一下2013-06-06.Net?Api?中使用Elasticsearch存儲(chǔ)文檔的方法
Elasticsearch 是一個(gè)分布式、高擴(kuò)展、高實(shí)時(shí)的搜索與數(shù)據(jù)分析引擎,在C# 的環(huán)境中,有一個(gè)Es的官方拓展包Nest,可以讓我們方便快捷的使用上Es數(shù)據(jù)庫(kù),本文重點(diǎn)給大家介紹.Net?Api?中使用Elasticsearch存儲(chǔ)文檔的方法,感興趣的朋友一起看看吧2022-01-01ASP.NET過(guò)濾HTML字符串方法總結(jié)
這篇文章主要介紹了ASP.NET過(guò)濾HTML字符串方法總結(jié),需要的朋友可以參考下2014-08-08ASP.NET母版頁(yè)基礎(chǔ)知識(shí)介紹
這篇文章主要介紹了ASP.NET母版頁(yè)基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09asp.net實(shí)現(xiàn)將ppt文檔轉(zhuǎn)換成pdf的方法
這篇文章主要介紹了asp.net實(shí)現(xiàn)將ppt文檔轉(zhuǎn)換成pdf的方法,通過(guò)自定義函數(shù)實(shí)現(xiàn)將pptx格式的文件轉(zhuǎn)換成pdf格式文件的功能,是非常實(shí)用的技巧,需要的朋友可以參考下2014-11-11C# SetCursorPos簡(jiǎn)介及使用說(shuō)明
該函數(shù)把光標(biāo)移到屏幕的指定位置,如果新位置不在由ClipCursor函數(shù)設(shè)置的屏幕矩形區(qū)域之內(nèi),則系統(tǒng)自動(dòng)調(diào)整坐標(biāo),使得光標(biāo)在矩形之內(nèi)2012-12-12MVC使用MvcPager實(shí)現(xiàn)分頁(yè)效果
這篇文章主要為大家詳細(xì)介紹了MVC使用MvcPager實(shí)現(xiàn)分頁(yè)效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03asp.net core 2.0 webapi集成signalr(實(shí)例講解)
下面小編就為大家分享一篇asp.net core 2.0 webapi集成signalr的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2017-11-11