項目創(chuàng)建

首先我們創(chuàng)建一個簡單的aspnetcore的webapi項目

創(chuàng)建一個配置選項用來存儲私鑰公鑰

public class RsaOptions
{
public string PrivateKey { get; set; }
}

創(chuàng)建一個Scheme選項類

public class AuthSecurityRsaOptions: AuthenticationSchemeOptions
{
}

定義一個常量

public class AuthSecurityRsaDefaults
{
public const string AuthenticationScheme = "SecurityRsaAuth";
}

創(chuàng)建我們的認(rèn)證處理器 AuthSecurityRsaAuthenticationHandler

public class AuthSecurityRsaAuthenticationHandler: AuthenticationHandler<AuthSecurityRsaOptions>
{
//正式替換成redis
private readonly ConcurrentDictionary<string, object> _repeatRequestMap =
new ConcurrentDictionary<string, object>();

public AuthSecurityRsaAuthenticationHandler(IOptionsMonitor<AuthSecurityRsaOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
{
}

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
try
{
string authorization = Request.Headers["AuthSecurity-Authorization"];
// If no authorization header found, nothing to process further
if (string.IsNullOrWhiteSpace(authorization))
return AuthenticateResult.NoResult();

var authorizationSplit = authorization.Split('.');
if (authorizationSplit.Length != 4)
return await AuthenticateResultFailAsync("簽名參數(shù)不正確");
var reg = new Regex(@"[0-9a-zA-Z]{1,40}");
var requestId = authorizationSplit[0];
if (string.IsNullOrWhiteSpace(requestId) || !reg.IsMatch(requestId))
return await AuthenticateResultFailAsync("請求Id不正確");
var appid = authorizationSplit[1];
if (string.IsNullOrWhiteSpace(appid) || !reg.IsMatch(appid))
return await AuthenticateResultFailAsync("應(yīng)用Id不正確");

var timeStamp = authorizationSplit[2];
if (string.IsNullOrWhiteSpace(timeStamp) || !long.TryParse(timeStamp, out var timestamp))
return await AuthenticateResultFailAsync("請求時間不正確");
//請求時間大于30分鐘的就拋棄
if (Math.Abs(UtcTime.CurrentTimeMillis() - timestamp) > 30 * 60 * 1000)
return await AuthenticateResultFailAsync("請求已過期");
var sign = authorizationSplit[3];
if (string.IsNullOrWhiteSpace(sign))
return await AuthenticateResultFailAsync("簽名參數(shù)不正確");
//數(shù)據(jù)庫獲取
//Request.HttpContext.RequestServices.GetService<DbContext>()
var app = AppCallerStorage.ApiCallers.FirstOrDefault(o=>o.Id==appid);
if (app == null)
return AuthenticateResult.Fail("未找到對應(yīng)的應(yīng)用信息");
//獲取請求體
var body = await Request.RequestBodyAsync();

//驗證簽名
if (!RsaFunc.ValidateSignature(app.AppPublickKey, $"{requestId}{appid}{timeStamp}{body}", sign))
return await AuthenticateResultFailAsync("簽名失敗");
var repeatKey = $"AuthSecurityRequestDistinct:{appid}:{requestId}";
//自行替換成緩存或者redis本項目不帶刪除key功能沒有過期時間原則上需要設(shè)置1小時過期,前后30分鐘服務(wù)器時間差
if (_repeatRequestMap.ContainsKey(repeatKey) || !_repeatRequestMap.TryAdd(repeatKey,null))
{
return await AuthenticateResultFailAsync("請勿重復(fù)提交");
}

//給Identity賦值
var identity = new ClaimsIdentity(AuthSecurityRsaDefaults.AuthenticationScheme);
identity.AddClaim(new Claim("appid", appid));
identity.AddClaim(new Claim("appname", app.Name));
identity.AddClaim(new Claim("role", "app"));
//......

var principal = new ClaimsPrincipal(identity);
return HandleRequestResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name));
}
catch (Exception ex)
{
Logger.LogError(ex, "RSA簽名失敗");
return await AuthenticateResultFailAsync("認(rèn)證失敗");
}
}

private async Task<AuthenticateResult> AuthenticateResultFailAsync(string message)
{
Response.StatusCode = 401;
await Response.WriteAsync(message);
return AuthenticateResult.Fail(message);
}
}

第三步我們添加擴(kuò)展方法

public static class AuthSecurityRsaExtension
{
public static AuthenticationBuilder AddAuthSecurityRsa(this AuthenticationBuilder builder)
=> builder.AddAuthSecurityRsa(AuthSecurityRsaDefaults.AuthenticationScheme, _ => { });

public static AuthenticationBuilder AddAuthSecurityRsa(this AuthenticationBuilder builder, Action<AuthSecurityRsaOptions> configureOptions)
=> builder.AddAuthSecurityRsa(AuthSecurityRsaDefaults.AuthenticationScheme, configureOptions);

public static AuthenticationBuilder AddAuthSecurityRsa(this AuthenticationBuilder builder, string authenticationScheme, Action<AuthSecurityRsaOptions> configureOptions)
=> builder.AddAuthSecurityRsa(authenticationScheme, displayName: null, configureOptions: configureOptions);

public static AuthenticationBuilder AddAuthSecurityRsa(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action<AuthSecurityRsaOptions> configureOptions)
{
return builder.AddScheme<AuthSecurityRsaOptions, AuthSecurityRsaAuthenticationHandler>(authenticationScheme, displayName, configureOptions);
}
}

添加返回結(jié)果加密解密 SafeResponseMiddleware

public class SafeResponseMiddleware
{
private readonly RequestDelegate _next;

public SafeResponseMiddleware(RequestDelegate next)
{
_next = next;
}

public async Task Invoke(HttpContext context)
{
//AuthSecurity-Authorization
if ( context.Request.Headers.TryGetValue("AuthSecurity-Authorization", out var authorization) && !string.IsNullOrWhiteSpace(authorization))
{
//獲取Response.Body內(nèi)容
var originalBodyStream = context.Response.Body;
await using (var newResponse = new MemoryStream())
{
//替換response流
context.Response.Body = newResponse;
await _next(context);
string responseString = null;
var identityIsAuthenticated = context.User?.Identity?.IsAuthenticated;
if (identityIsAuthenticated.HasValue && identityIsAuthenticated.Value)
{
var authorizationSplit = authorization.ToString().Split('.');
var requestId = authorizationSplit[0];
var appid = authorizationSplit[1];

using (var reader = new StreamReader(newResponse))
{
newResponse.Position = 0;
responseString = (await reader.ReadToEndAsync())??string.Empty;
var responseStr = JsonConvert.SerializeObject(responseString);
var app = AppCallerStorage.ApiCallers.FirstOrDefault(o => o.Id == appid);
var encryptBody = RsaFunc.Encrypt(app.AppPublickKey, responseStr);
var signature = RsaFunc.CreateSignature(app.MyPrivateKey, $"{requestId}{appid}{encryptBody}");
context.Response.Headers.Add("AuthSecurity-Signature", signature);
responseString = encryptBody;
}

await using (var writer = new StreamWriter(originalBodyStream))
{
await writer.WriteAsync(responseString);
await writer.FlushAsync();
}
}
}
}
else
{
await _next(context);
}
}
}

新增基礎(chǔ)基類來實現(xiàn)認(rèn)證

[Authorize(AuthenticationSchemes =AuthSecurityRsaDefaults.AuthenticationScheme )]
public class RsaBaseController : ControllerBase
{
}

到這個時候我們的接口已經(jīng)差不多寫完了,只是適配了微軟的框架,但是還是不能happy coding,接下來我們要實現(xiàn)模型的解析和校驗

型解析

首先我們要確保微軟是如何通過request body的字符串到model的綁定的,通過源碼解析我們可以發(fā)現(xiàn)aspnetcore是通過IModelBinder

首先實現(xiàn)模型綁定

public class EncryptBodyModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var httpContext = bindingContext.HttpContext;
//if (bindingContext.ModelType != typeof(string))
// return;
string authorization = httpContext.Request.Headers["AuthSecurity-Authorization"];
if (!string.IsNullOrWhiteSpace(authorization))
{
//有參數(shù)接收就反序列化并且進(jìn)行校驗
if (bindingContext.ModelType != null)
{
//獲取請求體
var encryptBody = await httpContext.Request.RequestBodyAsync();
if (string.IsNullOrWhiteSpace(encryptBody))
return;
//解密
var rsaOptions = httpContext.RequestServices.GetService<RsaOptions>();
var body = RsaFunc.Decrypt(rsaOptions.PrivateKey, encryptBody);
var request = JsonConvert.DeserializeObject(body, bindingContext.ModelType);
if (request == null)
{
return;
}
bindingContext.Result = ModelBindingResult.Success(request);

}
}
}
}

添加attribute的特性解析

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public class RsaModelParseAttribute : Attribute, IBinderTypeProviderMetadata, IBindingSourceMetadata, IModelNameProvider
{
private readonly ModelBinderAttribute modelBinderAttribute = new ModelBinderAttribute() { BinderType = typeof(EncryptBodyModelBinder) };

public BindingSource BindingSource => modelBinderAttribute.BindingSource;

public string Name => modelBinderAttribute.Name;

public Type BinderType => modelBinderAttribute.BinderType;
}

添加測試dto

[RsaModelParse]
public class TestModel
{
[Display(Name = "id"),Required(ErrorMessage = "{0}不能為空")]
public string Id { get; set; }
}

創(chuàng)建模型控制器

[Route("api/[controller]/[action]")]
[ApiController]
public class TestController: RsaBaseController
{
[AllowAnonymous]
public IActionResult Test()
{
return Ok();
}

//正常測試
public IActionResult Test1()
{
var appid = Request.HttpContext.User.Claims.FirstOrDefault(o=>o.Type== "appid").Value;
var appname = Request.HttpContext.User.Claims.FirstOrDefault(o=>o.Type== "appname").Value;

return Ok($"appid:{appid},appname:{appname}");
}
//模型校驗
public IActionResult Test2(TestModel request)
{
return Ok(JsonConvert.SerializeObject(request));
}
//錯誤校驗
public IActionResult Test3(TestModel request)
{
var x = 0;
var a = 1 / x;
return Ok("ok");
}
}

添加異常全局捕獲

public class HttpGlobalExceptionFilter : IExceptionFilter
{
private readonly ILogger<HttpGlobalExceptionFilter> _logger;

public HttpGlobalExceptionFilter(ILogger<HttpGlobalExceptionFilter> logger)
{
_logger = logger;
}

public void OnException(ExceptionContext context)
{
_logger.LogError(new EventId(context.Exception.HResult),
context.Exception,
context.Exception.Message);
context.Result = new OkObjectResult("未知異常");
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
context.ExceptionHandled = true;
}
}

添加模型校驗

public class ValidateModelStateFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ModelState.IsValid)
{
return;
}

var validationErrors = context.ModelState
.Keys
.SelectMany(k => context.ModelState[k].Errors)
.Select(e => e.ErrorMessage)
.ToArray();

context.Result = new OkObjectResult(string.Join(",", validationErrors));
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK;
}

}

startup配置

public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApiBehaviorOptions>(options =>
{
//忽略系統(tǒng)自帶校驗?zāi)?#91;ApiController]
options.SuppressModelStateInvalidFilter = true;
});
services.AddControllers(options =>
{
options.Filters.Add<HttpGlobalExceptionFilter>();
options.Filters.Add<ValidateModelStateFilter>();
});
services.AddControllers();

services.AddAuthentication().AddAuthSecurityRsa();
services.AddSingleton(sp =>
{
return new RsaOptions()
{
PrivateKey = Configuration.GetSection("RsaConfig")["PrivateKey"],
};
});
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

到此為止我們服務(wù)端的所有api接口和配置都已經(jīng)完成了接下來我們通過編寫客戶端接口和生成rsa密鑰對就可以開始使用api了

如何生成rsa秘鑰首先我們下載openssl

下載地址openssl:https://slproweb.com/products/Win32OpenSSL.html

輸入創(chuàng)建命令

打開bin下openssl.exe
生成RSA私鑰
openssl>genrsa -out rsa_private_key.pem 2048

生成RSA公鑰
openssl>rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem

將RSA私鑰轉(zhuǎn)換成PKCS8格式
openssl>pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out rsa_pkcs8_private_key.pem

公鑰和私鑰不是xml格式的C#使用rsa需要xml格式的秘鑰,所以先轉(zhuǎn)換對應(yīng)的秘鑰

首先nuget下載公鑰私鑰轉(zhuǎn)換工具

Install-Package BouncyCastle.NetCore -Version 1.8.8
public class RsaKeyConvert
{
private RsaKeyConvert()
{

}
public static string RsaPrivateKeyJava2DotNet(string privateKey)
{
RsaPrivateCrtKeyParameters privateKeyParam = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(TrimPrivatePrefixSuffix(privateKey)));

return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned()));
}

public static string RsaPrivateKeyDotNet2Java(string privateKey)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(TrimPrivatePrefixSuffix(privateKey));
BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText));
BigInteger exp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText));
BigInteger d = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("D")[0].InnerText));
BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("P")[0].InnerText));
BigInteger q = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Q")[0].InnerText));
BigInteger dp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DP")[0].InnerText));
BigInteger dq = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DQ")[0].InnerText));
BigInteger qinv = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("InverseQ")[0].InnerText));

RsaPrivateCrtKeyParameters privateKeyParam = new RsaPrivateCrtKeyParameters(m, exp, d, p, q, dp, dq, qinv);

PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParam);
byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetEncoded();
return Convert.ToBase64String(serializedPrivateBytes);
}

public static string RsaPublicKeyJava2DotNet(string publicKey)
{
RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(TrimPublicPrefixSuffix(publicKey)));
return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
}

public static string RsaPublicKeyDotNet2Java(string publicKey)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(TrimPublicPrefixSuffix(publicKey));
BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText));
BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText));
RsaKeyParameters pub = new RsaKeyParameters(false, m, p);

SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(pub);
byte[] serializedPublicBytes = publicKeyInfo.ToAsn1Object().GetDerEncoded();
return Convert.ToBase64String(serializedPublicBytes);
}

public static string TrimPublicPrefixSuffix(string publicKey)
{
return publicKey
.Replace("-----BEGIN PUBLIC KEY-----", string.Empty)
.Replace("-----END PUBLIC KEY-----", string.Empty)
.Replace("\r\n", "");
}
public static string TrimPrivatePrefixSuffix(string privateKey)
{
return privateKey
.Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
.Replace("-----END PRIVATE KEY-----", string.Empty)
.Replace("\r\n", "");
}
}

編寫好client后開始調(diào)用

依次啟動兩個項目就可以看到我們調(diào)用成功了,

本項目采用rsa雙向簽名和加密來接入aspnetcore的權(quán)限系統(tǒng)并且可以獲取到系統(tǒng)調(diào)用方用戶

完美接入aspnetcore認(rèn)證系統(tǒng)和權(quán)限系統(tǒng)(后續(xù)會出一篇如何設(shè)計權(quán)限)

系統(tǒng)交互采用雙向加密和簽名認(rèn)證

完美接入模型校驗

完美處理響應(yīng)結(jié)果注意本項目僅僅只是是一個學(xué)習(xí)demo,而且根據(jù)實踐得出的結(jié)論rsa加密僅僅是滿足了最最最安全的api這個條件,但是性能上而言會隨著body的變大性能急劇下降,所以并不是一個很好的抉擇當(dāng)然可以用在雙方交互的時候設(shè)置秘鑰提供api接口,實際情況下可以選擇使用對稱加密比如:AES或者DES進(jìn)行body體的加密解密,但是在簽名方面完全沒問題可以選擇rsa,本次使用的是rsa2(rsa 2048位的秘鑰)秘鑰位數(shù)越大加密等級越高但是解密性能越低。當(dāng)然你可以直接上https,本文章也不是說一定要雙向處理更多的是分享如何接入aspnetcore的認(rèn)證體系中和模型校驗,而不用帖一大堆的attribute。

demo地址:https://github.com/xuejmnet/AspNetCoreSafeApi

最后

分享本人開發(fā)的efcore分表分庫讀寫分離組件,希望為.NET生態(tài)做一份共享

Gitee址:https://gitee.com/dotnetchina/sharding-core

GitHub地址:https://github.com/xuejmnet/sharding-core

本文章轉(zhuǎn)載微信公眾號@dotNET全棧開發(fā)

上一篇:

在不破壞更改的情況下保護(hù)公共API

下一篇:

在Node.js中為Restful API編寫單元測試
#你可能也喜歡這些API文章!

我們有何不同?

API服務(wù)商零注冊

多API并行試用

數(shù)據(jù)驅(qū)動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內(nèi)容創(chuàng)意新穎性、情感共鳴力、商業(yè)轉(zhuǎn)化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準(zhǔn)確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費