Authentication,Authorization

如果公司交给你一个任务让你写一个api接口,那么我们应该如何设计这个api接口来保证这个接口是对外看起来“高大上”,“羡慕崇拜”,并且使用起来和普通api接口无感,并且可以完美接入aspnetcore的认证授权体系呢,而不是自定义签名来进行自定义过滤器实现呢(虽然也可以但是并不是最完美的),如何让小白羡慕一眼就知道你是老鸟。

接下来我将给大家分享你不知道的自定义认证授体系。

我相信这可能是你面对aspnetcore下一个无论如何都要跨过去的坎,也是很多老鸟不熟悉的未知领域(很多人说能用就行,那么你可以直接右上角或者左上角)

如何打造一个最最最安全的api接口

技术选型

在不考虑性能的影响下我们选择非对称加密可以选择sm或者rsa加密,这边我们选择rsa2048位pkcs8密钥来进行,http传输可以分为两个一个是request一个是response两个交互模式。

安全的交互方式在不使用https的前提下那么就是我把明文信息加密并且签名后给你,你收到后自己解密然后把你响应给我的明文信息加密后签名在回给我,这样就可以保证数据交互的安全性,

非对称加密一般拥有两个密钥,一个被称作为公钥,一个被称作为私钥,公钥是可以公开的哪怕放到互联网上也是没关系的,私钥是自己保存的,一般而言永远不会用到自己的私钥。

私钥签名的结果只能被对应的公钥校验成功,公钥加密的数据只能被对应的私钥解密

实现原理

假设我们现在是两个系统间的交互,系统A,系统B。系统A有一对rsa密钥对我们称之为公钥APubKey,私钥APriKey,系统B有一对rsa密钥我们称之为公钥BPubKey,私钥BPriKey。

私钥是每个系统生成后自己内部保存的,私钥的作用就是告诉发送方收到的人一定是我,公钥的作用就是告诉接收到是不是我发送的,基于这两条定理我们来设计程序,

首先我们系统A调用系统B的Api1接口假设我们传递一个hello,然后系统B会回复一个world。那么我们如何设计才可以保证安全呢。首先系统A发送消息如何让系统B知道是系统A发过来的而不是别的中间人共计呢。这里我们需要用到签名,就是说系统A用APriKey进行对hello的加密后那么如果发过去的数据如果签名是x内容是hello,系统B收到了就会对hello进行签名的校验,如果校验出来的结果是用私钥加密的那么你用哪个公钥进行的前面校验就可以保证系统是由哪个系统发送的。用APriKey进行签名的数据只有用APubKey进行签名校验才能通过,所以系统B就可以确保是有系统A发送的而不是别的系统,那么我们到现在还是传送的明文信息,所以我们还需要将数据进行加密,加密一般我们选择的是接收方的公钥,因为只有用接收方的公钥加密后才能由接收方的私钥解密出来

项目创建

首先我们创建一个简单的aspnetcore的webapi项目

创建一个配置选项用来存储私钥公钥


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

创建一个Scheme选项类

    public class AuthSecurityRsaOptions: AuthenticationSchemeOptions
{
}

定义一个常量

    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);
}
}

第三步我们添加扩展方法


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);
}
}

添加返回结果加密解密 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);
}
}
}

新增基础基类来实现认证


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

到这个时候我们的接口已经差不多写完了,只是适配了微软的框架,但是还是不能happy coding,接下来我们要实现模型的解析和校验

模型解析

首先我们要确保微软是如何通过request body的字符串到model的绑定的,通过源码解析我们可以发现aspnetcore是通过IModelBinder

首先实现模型绑定


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); }
}
}
}

添加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; }
}

创建模型控制器


[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 =>
{
//忽略系统自带校验你[ApiController]
options.SuppressModelStateInvalidFilter = true;
});
services.AddControllers(options =>
{
options.Filters.Add<HttpGlobalExceptionFilter>();
options.Filters.Add<ValidateModelStateFilter>();
});
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AspNetCoreSafeApi", Version = "v1" });
}); services.AddAuthentication().AddAuthSecurityRsa();
services.AddSingleton(sp =>
{
return new RsaOptions()
{
PublicKey = Configuration.GetSection("RsaConfig")["PublicKey"],
PrivateKey = Configuration.GetSection("RsaConfig")["PrivateKey"],
};
});
} public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "AspNetCoreSafeApi v1"));
} app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}

到此为止我们服务端的所有api接口和配置都已经完成了接下来我们通过编写客户端接口和生成rsa密钥对就可以开始使用api了

如何生成rsa秘钥首先我们下载openssl

下载地址openssl



双击

输入创建命令

打开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

公钥和私钥不是xml格式的C#使用rsa需要xml格式的秘钥,所以先转换对应的秘钥

首先nuget下载公钥私钥转换工具

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后开始调用





依次启动两个项目就可以看到我们调用成功了,

本项目采用rsa双向签名和加密来接入aspnetcore的权限系统并且可以获取到系统调用方用户

完美接入aspnetcore认证系统和权限系统(后续会出一篇如何设计权限)

系统交互采用双向加密和签名认证

完美接入模型校验

完美处理响应结果

注意本项目仅仅只是是一个学习demo,而且根据实践得出的结论rsa加密仅仅是满足了最最最安全的api这个条件,但是性能上而言会随着body的变大性能急剧下降,所以并不是一个很好的抉择当然可以用在双方交互的时候设置秘钥提供api接口,实际情况下可以选择使用对称加密比如:AES或者DES进行body体的加密解密,但是在签名方面完全没问题可以选择rsa,本次使用的是rsa2(rsa 2048位的秘钥)秘钥位数越大加密等级越高但是解密性能越低

demo

AspNetCoreSafeApi

最后

分享本人开发的efcore分表分库读写分离组件,希望为.net生态做一份共享,如果喜欢或者觉得有用请点下star或者赞让更多的人看到

Gitee Star 助力dotnet 生态 Github Star


博客

QQ群:771630778

个人QQ:326308290(欢迎技术支持提供您宝贵的意见)

个人邮箱:326308290@qq.com

ApsNetCore打造一个“最安全”的api接口的更多相关文章

  1. 怎样提供一个好的移动API接口服务/从零到一[开发篇]

    引语:现在互联网那么热,你手里没几个APP都不好意思跟别人打招呼!但是,难道APP就是全能的神吗?答案是否定的,除了优雅的APP前端展示,其实核心还是服务器端.数据的保存.查询.消息的推送,无不是在服 ...

  2. 设计一个高质量的API接口

    参考网址:http://url.cn/5UaTeyv 前言 在设计接口时,有很多因素要考虑,如接口的业务定位,接口的安全性,接口的可扩展性.接口的稳定性.接口的跨域性.接口的协议规则.接口的路径规则. ...

  3. 如何设计一个牛逼的API接口

    在日常开发中,总会接触到各种接口.前后端数据传输接口,第三方业务平台接口.一个平台的前后端数据传输接口一般都会在内网环境下通信,而且会使用安全框架,所以安全性可以得到很好的保护.这篇文章重点讨论一下提 ...

  4. ASP.NET获取百度地图提供的API接口里面的JSON

    思路:开始是想直接在前台获取,但是跨域访问还是有点难度,而且格式必须是josnp格式的,最后嫌麻烦,不得已放弃. 我做的ASP.NET  而这个有自带的解析类,直接引用就行了 先在后台获取到JOSN: ...

  5. WebApi系列~通过HttpClient来调用Web Api接口~续~实体参数的传递

    回到目录 上一讲中介绍了使用HttpClient如何去调用一个标准的Web Api接口,并且我们知道了Post,Put方法只能有一个FromBody参数,再有多个参数时,上讲提到,需要将它封装成一个对 ...

  6. ASP.NET Core 实战:构建带有版本控制的 API 接口

    一.前言 在上一篇的文章中,主要是搭建了我们的开发环境,同时创建了我们的项目模板框架.在整个前后端分离的项目中,后端的 API 接口至关重要,它是前端与后端之间进行沟通的媒介,如何构建一个 “好用” ...

  7. List多个字段标识过滤 IIS发布.net core mvc web站点 ASP.NET Core 实战:构建带有版本控制的 API 接口 ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目 Using AutoFac

    List多个字段标识过滤 class Program{  public static void Main(string[] args) { List<T> list = new List& ...

  8. SpringBoot实战(二)Restful风格API接口

    在上一篇SpringBoot实战(一)HelloWorld的基础上,编写一个Restful风格的API接口: 1.根据MVC原则,创建一个简单的目录结构,包括controller和entity,分别创 ...

  9. Spring Boot入门系列(二十)快速打造Restful API 接口

    spring boot入门系列文章已经写到第二十篇,前面我们讲了spring boot的基础入门的内容,也介绍了spring boot 整合mybatis,整合redis.整合Thymeleaf 模板 ...

随机推荐

  1. Java第一阶段项目实训

    时间:2016-3-27 17:09 银行综合业务平台业务需求 1.首页  ---------------银行综合业务平台------------------- 1开户     2登录    3.退出 ...

  2. C++回调机制

    一直对回调机制不是很了解,今天索性搜了很多资料顺便整理一下,进步一点点. 1.Callback方式(回调函数) 什么是回调函数? 简而言之,回调函数就是一个通过函数指针调用的函数.如果你把函数的指针( ...

  3. 简单C++线程池

    简单C++线程池 Java 中有一个很方便的 ThreadPoolExecutor,可以用做线程池.想找一下 C++ 的类似设施,尤其是能方便理解底层原理可上手的.网上找到的 demo,基本都是介绍的 ...

  4. 致敬mentohust,路由器使用Socket认证华科校园网

    致敬mentohust,路由器使用Socket认证华科校园网 前言: 上一篇文章中,为了解决ESP32华科无线网认证的问题,我成功把网页认证机制用Python+Socket复现.但痛点依然存在,无线网 ...

  5. 哲学家就餐问题-Java语言实现死锁避免

    哲学家就餐问题-Java语言实现死锁避免 我死锁预防是至少破坏死锁产生的四个必要条件之一,带来的问题就是系统资源利用率低且不符合开发习惯,而死锁避免不是事先釆取某种限制措施破坏死锁的必要条件,只是注意 ...

  6. Sentry-CLI 使用详解(2021 Sentry v21.8.x)

    内容源于:https://docs.sentry.io/platforms/javascript/guides/vue/ 系列 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创 ...

  7. springMVC学习总结(二) --springMVC表单处理、标签库、静态文件处理

    根据springMVC学习总结(一) --springMVC搭建 搭建项目 一.表单处理 1.创建两个java类 Student.java, StudentController.java. 2.在js ...

  8. [源码解析] 深度学习流水线并行 PipeDream(4)--- 运行时引擎

    [源码解析] 深度学习流水线并行 PipeDream(4)--- 运行时引擎 目录 [源码解析] 深度学习流水线并行 PipeDream(4)--- 运行时引擎 0x00 摘要 0x01 前言 1.1 ...

  9. 第二课:启动 GDB 调试

    使用 GDB 调试程序一般有三种方式: gdb filename gdb attach pid gdb filename corename 这也对应着本节课的核心内容: 直接调试目标程序 附加进程 调 ...

  10. windows下nodejs正确安装方式

    ​ 下载安装包: 32 位安装包下载地址 : https://nodejs.org/dist/v4.4.3/node-v4.4.3-x86.msi 64 位安装包下载地址 : https://node ...