鍵.png)
ASP.NET Web API快速入門介紹
首先我們創(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了
下載地址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)用成功了,
完美接入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ā)