If the company gives you a task to write an api interface, then how should we design this api interface to ensure that it looks "high-quality" and "admires" to the outside world, and is insensitive to ordinary api interfaces to use, and can it be perfectly connected to aspnetcore's authentication and authorization system, rather than custom signatures to implement custom filters?(Although it is possible, it is not the most perfect one), how to make Xiaobai envy and know at a glance that you are an old bird.
Next, I will share with you the custom certification system that you don't know about.
I believe this may be the next hurdle you have to overcome no matter what when facing aspnetcore. It is also an unknown territory that many old people are unfamiliar with (** Many people say it can be used, then you can go straight to the upper right corner or the upper left corner **)
How to create the most secure api interface
technology selection
Regardless of the impact of performance, we can choose asymmetric encryption and choose sm or rsa encryption. Here we choose rsa 2048-bit pkcs8 key. HTTP transmission can be divided into two interactive modes: request and response.
A secure interaction method without using https is that I encrypt and sign the plaintext information and give it to you. After you receive it, you decrypt it yourself, and then encrypt the plaintext information that you responded to me, sign it, and return it to me. This can ensure the security of data interaction.
Asymmetric encryption generally has two keys, one is called a public key and the other is called a private key. The public key can be made public and it doesn't matter even if it is placed on the Internet. The private key is kept by itself. Generally speaking, you will never use your own private key.
-
- The result of the private key signing can only be verified successfully by the corresponding public key, and the data encrypted by the public key can only be decrypted by the corresponding private key **
realization principle
Suppose we are now interacting between two systems, System A and System B. System A has a pair of rsa keys that we call public key APubKey and private key APriKey, and system B has a pair of rsa keys that we call public key BPubKey and private key BPriKey.
The private key is stored internally by each system after it is generated. The purpose of the private key is to tell the sender that the person who received it must be me, and the purpose of the public key is to tell the person who received it is me. Based on these two theorems, we design the program.
First, our system A calls the Api1 interface of system B, assuming we pass a hello, and then system B will reply with a world. So how can we design to ensure safety? First of all, how can system A send a message to let system B know that it was sent by system A and not another middleman? Here we need to use a signature, which means that after system A uses APriKey to encrypt hello, if the data sent is signed x and the content is hello, system B will verify the signature of hello after receiving it. If the result of the verification is encrypted with a private key, then the previous verification you use can ensure that the system was sent by which system. Data signed with APriKey can only pass the signature verification with AubKey, so System B can ensure that it is sent by System A and not another system. So we still transmit plaintext information until now, so we still need to encrypt the data. Generally, we choose the receiver's public key, because it can only be decrypted by the receiver's private key after being encrypted with the receiver's public key.

project creation
First we create a simple webapi project for aspnetcore

Create a configuration option to store private key public key
public class RsaOptions
{
public string PrivateKey { get; set; }
}
Create a Scheme option class
public class AuthSecurityRsaOptions: AuthenticationSchemeOptions
{
}
Define a constant
public class AuthSecurityRsaDefaults
{
public const string AuthenticationScheme = "SecurityRsaAuth";
}
创建我们的认证处理器 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("签名参数不正确");
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("应用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("签名参数不正确");
//数据库获取
//Request.HttpContext.RequestServices.GetService<DbContext>()
var app = AppCallerStorage.ApiCallers.FirstOrDefault(o=>o.Id==appid);
if (app == null)
return AuthenticateResult.Fail("未找到对应的应用信息");
//获取请求体
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功能没有过期时间原则上需要设置1小时过期,前后30分钟服务器时间差
if (_repeatRequestMap.ContainsKey(repeatKey) || !_repeatRequestMap.TryAdd(repeatKey,null))
{
return await AuthenticateResultFailAsync("请勿重复提交");
}
//给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("认证失败");
}
}
private async Task<AuthenticateResult> AuthenticateResultFailAsync(string message)
{
Response.StatusCode = 401;
await Response.WriteAsync(message);
return AuthenticateResult.Fail(message);
}
}
The third step is to add an extension method
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);
}
}
Add returned results Encryption and decryption 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内容
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);
}
}
}
Add a basic base class to implement authentication
[Authorize(AuthenticationSchemes =AuthSecurityRsaDefaults.AuthenticationScheme )]
public class RsaBaseController : ControllerBase
{
}
By this time, our interface has almost been completed, but it is only adapted to Microsoft's framework, but it still cannot be ** happy coding **. Next, we need to implement the analysis and verification of the model.
model analysis
首先我们要确保微软是如何通过 request body 的字符串到 model 的绑定的,通过源码解析我们可以发现 aspnetcore 是通过IModelBinder。
First implement model binding
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))
{
//有参数接收就反序列化并且进行校验
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);
}
}
}
}
Add attribute analysis
[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;
}
Add test dto
[RsaModelParse]
public class TestModel
{
[Display(Name = "id"),Required(ErrorMessage = "{0}不能为空")]
public string Id { get; set; }
}
Create a model controller
[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");
}
}
Add exception global capture
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;
}
}
Add model verification
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 configuration
public void ConfigureServices(IServiceCollection services)
{
services.Configure<ApiBehaviorOptions>(options =>
{
//忽略系统自带校验你[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.UseMiddleware<SafeResponseMiddleware>();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
So far, all the api interfaces and configurations of our server have been completed. Next, we can start using the api by writing the client interface and generating an rsa key pair
How to generate an rsa key First of all, we download openssl
下载地址openssl

double-click

Enter create command
打开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私钥转换成PKCS8格式
openssl>pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out rsa_pkcs8_private_key.pem

The public key and private key are not in xml format. C#uses rsa to require a secret key in xml format, so convert the corresponding secret key first
First, nuget downloads the public key and private key conversion tool
Install-Package BouncyCastle.NET Core -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", "");
}
}
After writing the client, start calling


Start two projects in turn and you can see that our call was successful.
-
- This project uses rsa two-way signature and encryption to access aspnetcore's authority system and can obtain system caller users **
Perfect access to the aspnetcore authentication system and permissions system (how to design permissions will be published later)
System interaction uses two-way encryption and signature authentication
Perfect access model verification
Perfectly handles response results
-
- Note that this project is just a learning demo, and based on practice, the conclusion that rsa encryption only meets the most secure api condition, but in terms of performance, the performance will drop sharply as the body becomes larger, so it is not a good choice. Of course, you can set a secret key to provide an api interface when the two parties interact. In reality, you can choose to use symmetric encryption such as: AES or DES performs encryption and decryption of the body body, but there is no problem with signing. You can choose rsa. This time, rsa2 (rsa 2048-bit secret key) is used. The larger the number of key bits, the higher the encryption level, but the lower the decryption performance. **
Of course, you can go directly to https. This article does not mean that it has to be processed in both directions. It is more about sharing how to access aspnetcore's authentication system and model verification without posting a lot of attributes.
demo:AspNetCoreSafeApi
last
Share the efcore sub-table and sub-library read-write separation component I developed. I hope to share it for the.net ecosystem. If you like it or find it useful, please click star or like it to let more people see it.
Gitee Star 助力 dotnet 生态 GitHub Star