在此之前,写过一篇 给新手的WebAPI实践 ,获得了很多新人的认可,那时还是基于.net mvc,文档生成还是自己闹洞大开写出来的,经过这两年的时间,netcore的发展已经势不可挡,自己也在不断的学习,公司的项目也转向了netcore。大部分也都是前后分离的架构,后端api开发居多,从中整理了一些东西在这里分享给大家。

源码地址:https://gitee.com/loogn/NetApiStarter,这是一个基于netcore mvc 3.0的模板项目,如果你使用的netcore 2.x,除了引用不通用外,代码基本是可以复用的。下面介绍一下其中的功能。

登录验证

这里我默认使用了jwt登录验证,因为它足够简单和轻量,在netcore mvc中使用jwt验证非常简单,首先在startup.cs文件中配置服务并启用:

ConfigureServices方法中:

  1. var jwtSection = Configuration.GetSection("Jwt");
  2. services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  3. .AddJwtBearer(options =>
  4. {
  5. options.TokenValidationParameters = new TokenValidationParameters
  6. {
  7. ValidateIssuer = true,
  8. ValidateAudience = true,
  9. ValidateLifetime = true,
  10. ValidateIssuerSigningKey = true,
  11. ValidAudience = jwtSection["Audience"],
  12. ValidIssuer = jwtSection["Issuer"],
  13. IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSection["SigningKey"]))
  14. };
  15. });

Configure方法中,在UseRouting和UseEndpoints方法之前:

  1. app.UseAuthorization();

上面我们使用到了jwt配置块,对应appsettings.json文件中有这样的配置:

  1. {
  2. "Jwt": {
  3. "SigningKey": "1234567812345678",
  4. "Issuer": "NetApiStarter",
  5. "Audience": "NetApiStarter"
  6. }
  7. }

我们再操作两步来实现登录验证,

一、提供一个接口生成jwt,

二、在客户端请求头部加上Authorization: Bearer {jwt}

我先封装了一个生成jwt的方法

  1. public static class JwtHelper
  2. {
  3. public static string WriteToken(Dictionary<string, string> claimDict, DateTime exp)
  4. {
  5. var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppSettings.Instance.Jwt.SigningKey));
  6. var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
  7. var token = new JwtSecurityToken(
  8. issuer: AppSettings.Instance.Jwt.Issuer,
  9. audience: AppSettings.Instance.Jwt.Audience,
  10. claims: claimDict.Select(x => new Claim(x.Key, x.Value)),
  11. expires: exp,
  12. signingCredentials: creds);
  13. var jwt = new JwtSecurityTokenHandler().WriteToken(token);
  14. return jwt;
  15. }
  16. }

然后在登录服务中调用

  1. /// <summary>
  2. /// 登录,获取jwt
  3. /// </summary>
  4. /// <param name="request"></param>
  5. /// <returns></returns>
  6. public ResultObject<LoginResponse> Login(LoginRequest request)
  7. {
  8. var user = userDao.GetUser(request.Account, request.Password);
  9. if (user == null)
  10. {
  11. return new ResultObject<LoginResponse>("用户名或密码错误");
  12. }
  13. var dict = new Dictionary<string, string>();
  14. dict.Add("userid", user.Id.ToString());
  15. var jwt = JwtHelper.WriteToken(dict, DateTime.Now.AddDays(7));
  16. var response = new LoginResponse { Jwt = jwt };
  17. return new ResultObject<LoginResponse>(response);
  18. }

在Controller和Action上添加[Authorize]和[AllowAnonymous]两个特性就可以实现登录验证了。

请求响应

这里请求响应的设计依然没有使用restful风格,一是感觉太麻烦,二是真的不太懂(实事求是),所以请求还是以POST方式投递JSON数据,响应当然也是JSON数据这个没啥异议的。

为啥使用POST+JSON呢,主要是简单,大家都懂,而且规则统一、繁简皆宜,比如什么参数都不需要,就传{},根据ID查询文章{articleId:23},或者复杂的查询条件和表单提交{ name:'abc', addr:{provice:'HeNan', city:'ZhengZhou'},tags:['骑马','射箭'] } 等等都可以优雅的传递。

这只是我个人的风格,netcore mvc是支持其他的方式的,选自己喜欢的就行了。

下面的内容还是按照POST+JSON来说。

首先提供请求基类:

  1. /// <summary>
  2. /// 登录用户请求的基类
  3. /// </summary>
  4. public class LoginedRequest
  5. {
  6. #region jwt相关用户
  7. private ClaimsPrincipal _claimsPrincipal { get; set; }
  8. public ClaimsPrincipal GetPrincipal()
  9. {
  10. return _claimsPrincipal;
  11. }
  12. public void SetPrincipal(ClaimsPrincipal user)
  13. {
  14. _claimsPrincipal = user;
  15. }
  16. public string GetClaimValue(string name)
  17. {
  18. return _claimsPrincipal?.FindFirst(name)?.Value;
  19. }
  20. #endregion
  21. #region 数据库相关用户 (如果有必要的话)
  22. //不用属性是因为swagger中会显示出来
  23. private User _user;
  24. public User GetUser()
  25. {
  26. return _user;
  27. }
  28. public void SetUser(User user)
  29. {
  30. _user = user;
  31. }
  32. #endregion
  33. }

这个类中说白了就是两个手写属性,一个ClaimsPrincipal用来保存从jwt解析出来的用户,一个User用来保存数据库中完整的用户信息,为啥不直接使用属性呢,上面注释也提到了,不想在api文档中显示出来。这个用户信息是在服务层使用的,而且User不是必须的,比如jwt中的信息够服务层使用,不定义User也是可以的,总之这里的信息是为服务层逻辑服务的。

我们还可以定义其他的基类,比如经常用的分页基类:

  1. public class PagedRequest : LoginedRequest
  2. {
  3. public int PageIndex { get; set; }
  4. public int PageSize { get; set; }
  5. }

根据项目的实际情况还可以定义更多的基类来方便开发。

响应类使用统一的格式,这里直接提供json方便查看:

  1. {
  2.   "result": {
  3.     "jwt": "string"
  4.   },
  5.   "success": true,
  6.   "code": 0,
  7.   "msg": "错误信息"
  8. }

result是具体的响应对象,如果success为false的话,result一般是null。

ActionFilter

mvc本身是一个扩展性极强的框架,层层有拦截,ActionFilter就是其中之一,IActionFilter接口有两个方法,一个是OnActionExecuted,一个是OnActionExecuting,从命名也能看出,就是在Action的前后分别执行的方法。我们这里主要重写OnActionExecuting方法来做两件事:

一、将登陆信息赋值给请求对象

二、验证请求对象

这里说的请求对象,其类型就是LoginedRequest或者LoginedRequest的子类,看代码:

  1. [AppService]
  2. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  3. public class MyActionFilterAttribute : ActionFilterAttribute
  4. {
  5. /// <summary>
  6. /// 是否验证参数有效性
  7. /// </summary>
  8. public bool ValidParams { get; set; } = true;
  9. public override void OnActionExecuting(ActionExecutingContext context)
  10. {
  11. //由于Filters是套娃模式,使用以下逻辑保证作用域的覆盖 Action > Controller > Global
  12. if (context.Filters.OfType<MyActionFilterAttribute>().Last() != this)
  13. {
  14. return;
  15. }
  16. //默认只有一个参数
  17. var firstParam = context.ActionArguments.FirstOrDefault().Value;
  18. if (firstParam != null && firstParam.GetType().IsClass)
  19. {
  20. //验证参数合法性
  21. if (ValidParams)
  22. {
  23. var validationResults = new List<ValidationResult>();
  24. var validationFlag = Validator.TryValidateObject(firstParam, new ValidationContext(firstParam), validationResults, false);
  25. if (!validationFlag)
  26. {
  27. var ro = new ResultObject(validationResults.First().ErrorMessage);
  28. context.Result = new JsonResult(ro);
  29. return;
  30. }
  31. }
  32. }
  33. var requestParams = firstParam as LoginedRequest;
  34. if (requestParams != null)
  35. {
  36. //设置jwt用户
  37. requestParams.SetPrincipal(context.HttpContext.User);
  38. var userid = requestParams.GetClaimValue("userid");
  39. //如果有必要,可以每次都获取数据库中的用户
  40. if (!string.IsNullOrEmpty(userid))
  41. {
  42. var user = ((UserService)context.HttpContext.RequestServices.GetService(typeof(UserService))).SingleById(long.Parse(userid));
  43. requestParams.SetUser(user);
  44. }
  45. }
  46. base.OnActionExecuting(context);
  47. }
  48. }

模型验证这块使用的是系统自带的,从上面代码也可以看出,如果请求对象定义为LoginedRequest及其子类,每次请求会填充ClaimsPrincipal,如果有必要,可以从数据库中读取User信息填充。

请求经过ActionFilter时,模型验证不通过的,直接返回了验证错误信息,通过之后到达Action和Service时,用户信息已经可以直接使用了。

api文档和日志

api文档首选swagger了,aspnetcore 官方文档也是使用的这个,我这里用的是Swashbuckle,首先安装引用

Install-Package Swashbuckle.AspNetCore -Version 5.0.0-rc4

定义一个扩展类,方便把swagger注入容器中:

  1. public static class SwaggerServiceExtensions
  2. {
  3. public static IServiceCollection AddSwagger(this IServiceCollection services)
  4. {
  5. //https://github.com/domaindrivendev/Swashbuckle.AspNetCore
  6. services.AddSwaggerGen(c =>
  7. {
  8. c.SwaggerDoc("v1", new OpenApiInfo
  9. {
  10. Title = "My Api",
  11. Version = "v1"
  12. });
  13. c.IgnoreObsoleteActions();
  14. c.IgnoreObsoleteProperties();
  15. c.DocumentFilter<SwaggerDocumentFilter>();
  16. //自定义类型映射
  17. c.MapType<byte>(() => new OpenApiSchema { Type = "byte", Example = new OpenApiByte(0) });
  18. c.MapType<long>(() => new OpenApiSchema { Type = "long", Example = new OpenApiLong(0L) });
  19. c.MapType<int>(() => new OpenApiSchema { Type = "integer", Example = new OpenApiInteger(0) });
  20. c.MapType<DateTime>(() => new OpenApiSchema { Type = "DateTime", Example = new OpenApiDateTime(DateTimeOffset.Now) });
  21. //xml注释
  22. foreach (var file in Directory.GetFiles(AppContext.BaseDirectory, "*.xml"))
  23. {
  24. c.IncludeXmlComments(file);
  25. }
  26. //Authorization的设置
  27. c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
  28. {
  29. In = ParameterLocation.Header,
  30. Description = "请输入验证的jwt。示例:Bearer {jwt}",
  31. Name = "Authorization",
  32. Type = SecuritySchemeType.ApiKey,
  33. });
  34. });
  35. return services;
  36. }
  37. /// <summary>
  38. /// Swagger控制器描述文字
  39. /// </summary>
  40. class SwaggerDocumentFilter : IDocumentFilter
  41. {
  42. public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
  43. {
  44. swaggerDoc.Tags = new List<OpenApiTag>
  45. {
  46. new OpenApiTag{ Name="User", Description="用户相关"},
  47. new OpenApiTag{ Name="Common", Description="公共功能"},
  48. };
  49. }
  50. }
  51. }

主要是验证部分,加上去之后就可以在文档中使用jwt测试了

然后在startup.cs的ConfigureServices方法中

services.AddSwagger();

Configure方法中:

  1. if (env.IsDevelopment())
  2. {
  3. app.UseSwagger();
  4. app.UseSwaggerUI(options =>
  5. {
  6. options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
  7. options.DocExpansion(DocExpansion.None);
  8. });
  9. }

这里限制了只有在开发环境才显示api文档,如果是需要外部调用的话,可以不做这个限制。

日志组件使用Serilog。

首先也是安装引用

Install-Package Serilog

Install-Package Serilog.AspNetCore

Install-Package Serilog.Settings.Configuration

Install-Package Serilog.Sinks.RollingFile

然后在appsettings.json中添加配置

  1. {
  2. "Serilog": {
  3. "WriteTo": [
  4. { "Name": "Console" },
  5. {
  6. "Name": "RollingFile",
  7. "Args": { "pathFormat": "logs/{Date}.log" }
  8. }
  9. ],
  10. "Enrich": [ "FromLogContext" ],
  11. "MinimumLevel": {
  12. "Default": "Debug",
  13. "Override": {
  14. "Microsoft": "Warning",
  15. "System": "Warning"
  16. }
  17. }
  18. },
  19. }

更多配置请查看https://github.com/serilog/serilog-settings-configuration

上述配置会在应用程序根目录的logs文件夹下,每天生成一个命名类似20191129.log的日志文件

最后要修改一下Program.cs,代替默认的日志组件

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureWebHostDefaults(webBuilder =>
  4. {
  5. webBuilder.UseConfiguration(new ConfigurationBuilder().SetBasePath(Environment.CurrentDirectory).AddJsonFile("appsettings.json").Build());
  6. webBuilder.UseStartup<Startup>();
  7. webBuilder.UseSerilog((whbContext, configureLogger) =>
  8. {
  9. configureLogger.ReadFrom.Configuration(whbContext.Configuration);
  10. });
  11. });

文件分块上传

文件上传就像登录验证一样常用,哪个应用还不上传个头像啥的,所以我也打算整合到模板项目中,如果是单纯的上传也就没必要说了,这里主要说的是一种大文件上传的解决方法: 分块上传。

分块上传是需要客户端配合的,客户端把一个大文件分好块,一小块一小块的上传,上传完成之后服务端按照顺序合并到一起就是整个文件了。

所以我们先定义分块上传的参数:

  1. string identifier : 文件标识,一个文件的唯一标识,
  2. int chunkNumber :当前块所以,我是从1开始的
  3. int chunkSize :每块大小,客户端设置的固定值,单位为byte,一般2M左右就可以了
  4. long totalSize:文件总大小,单位为byte
  5. int totalChunks:总块数

这些参数都好理解,在服务端验证和合并文件时需要。

开始的时候我是这样处理的,客户端每上传一块,我会把这块的内容写到一个临时文件中,使用identifier和chunkNumber来命名,这样就知道是哪个文件的哪一块了,当上传完最后一块之后,也就是chunkNumber==totalChunks的时候,我将所有的分块小文件合并到目标文件,然后返回url。

这个逻辑是没什么问题,只需要一个机制保证合并文件的时候所有块都已上传就可以了,为什么要这样一个机制呢,主要是因为客户端的上传可能是多线程的,而且也不能完全保证http的响应顺序和请求顺序是一样的,所以虽然上传完最后一块才会合并,但是还是需要一个机制判断一下是否所有块都上传完毕,没有上传完还要等待一下(想一想怎么实现!)。

后来在实际上传过程中发现最后一块响应会比较慢,特别是文件很大的时候,这个也好理解,因为最后一块上传会合并文件,所以需要优化一下。

这里就使用到了队列的概念了,我们可以把每次上传的内容都放在队列中,然后使用另一个线程从队列中读取并写入目标文件。在这个场景中BlockingCollection是最合适不过的了。

我们定义一个实体类,用于保存入列的数据:

  1. public class UploadChunkItem
  2. {
  3. public byte[] Data { get; set; }
  4. public int ChunkNumber { get; set; }
  5. public int ChunkSize { get; set; }
  6. public string FilePath { get; set; }
  7. }

然后定义一个队列写入器

  1. public class UploadChunkWriter
  2. {
  3. public static UploadChunkWriter Instance = new UploadChunkWriter();
  4. private BlockingCollection<UploadChunkItem> _queue;
  5. private int _writeWorkerCount = 3;
  6. private Thread _writeThread;
  7. public UploadChunkWriter()
  8. {
  9. _queue = new BlockingCollection<UploadChunkItem>(500);
  10. _writeThread = new Thread(this.Write);
  11. }
  12. public void Write()
  13. {
  14. while (true)
  15. {
  16. //单线程写入
  17. //var item = _queue.Take();
  18. //using (var fileStream = File.Open(item.FilePath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite))
  19. //{
  20. // fileStream.Position = (item.ChunkNumber - 1) * item.ChunkSize;
  21. // fileStream.Write(item.Data, 0, item.Data.Length);
  22. // item.Data = null;
  23. //}
  24. //多线程写入
  25. Task[] tasks = new Task[_writeWorkerCount];
  26. for (int i = 0; i < _writeWorkerCount; i++)
  27. {
  28. var item = _queue.Take();
  29. tasks[i] = Task.Run(() =>
  30. {
  31. using (var fileStream = File.Open(item.FilePath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite))
  32. {
  33. fileStream.Position = (item.ChunkNumber - 1) * item.ChunkSize;
  34. fileStream.Write(item.Data, 0, item.Data.Length);
  35. item.Data = null;
  36. }
  37. });
  38. }
  39. Task.WaitAll(tasks);
  40. }
  41. }
  42. public void Add(UploadChunkItem item)
  43. {
  44. _queue.Add(item);
  45. }
  46. public void Start()
  47. {
  48. _writeThread.Start();
  49. }
  50. }

主要是Write方法的逻辑,调用_queue.Take()方法从队列中获取一项,如果队列中没有数据,这个方法会堵塞当前线程,这也是我们所期望的,获取到数据之后,打开目标文件(在上传第一块的时候会创建),根据ChunkNumber 和ChunkSize找到开始写入的位置,然后把本块数据写入。

打开目标文件的时候使用了FileShare.ReadWrite,表示这个文件可以同时被多个线程读取和写入。

文件上传方法也简单:

  1. /// <summary>
  2. /// 分片上传
  3. /// </summary>
  4. /// <param name="formFile"></param>
  5. /// <param name="chunkNumber"></param>
  6. /// <param name="chunkSize"></param>
  7. /// <param name="totalSize"></param>
  8. /// <param name="identifier"></param>
  9. /// <param name="totalChunks"></param>
  10. /// <returns></returns>
  11. public ResultObject<UploadFileResponse> ChunkUploadfile(IFormFile formFile, int chunkNumber, int chunkSize, long totalSize,
  12. string identifier, int totalChunks)
  13. {
  14. var appSetting = AppSettings.Instance;
  15. #region 验证
  16. if (formFile == null && formFile.Length == 0)
  17. {
  18. return new ResultObject<UploadFileResponse>("文件不能为空");
  19. }
  20. if (formFile.Length > appSetting.Upload.LimitSize)
  21. {
  22. return new ResultObject<UploadFileResponse>("文件超过了最大限制");
  23. }
  24. var ext = Path.GetExtension(formFile.FileName).ToLower();
  25. if (!appSetting.Upload.AllowExts.Contains(ext))
  26. {
  27. return new ResultObject<UploadFileResponse>("文件类型不允许");
  28. }
  29. if (chunkNumber == 0 || chunkSize == 0 || totalSize == 0 || identifier.Length == 0 || totalChunks == 0)
  30. {
  31. return new ResultObject<UploadFileResponse>("参数错误0");
  32. }
  33. if (chunkNumber > totalChunks)
  34. {
  35. return new ResultObject<UploadFileResponse>("参数错误1");
  36. }
  37. if (totalSize > appSetting.Upload.TotalLimitSize)
  38. {
  39. return new ResultObject<UploadFileResponse>("参数错误2");
  40. }
  41. if (chunkNumber < totalChunks && formFile.Length != chunkSize)
  42. {
  43. return new ResultObject<UploadFileResponse>("参数错误3");
  44. }
  45. if (totalChunks == 1 && formFile.Length != totalSize)
  46. {
  47. return new ResultObject<UploadFileResponse>("参数错误4");
  48. }
  49. #endregion
  50. //写入逻辑
  51. var now = DateTime.Now;
  52. var yy = now.ToString("yyyy");
  53. var mm = now.ToString("MM");
  54. var dd = now.ToString("dd");
  55. var fileName = EncryptHelper.MD5Encrypt(identifier) + ext;
  56. var folder = Path.Combine(appSetting.Upload.UploadPath, yy, mm, dd);
  57. var filePath = Path.Combine(folder, fileName);
  58. //线程安全的创建文件
  59. if (!File.Exists(filePath))
  60. {
  61. lock (lockObj)
  62. {
  63. if (!File.Exists(filePath))
  64. {
  65. if (!Directory.Exists(folder))
  66. {
  67. Directory.CreateDirectory(folder);
  68. }
  69. File.Create(filePath).Dispose();
  70. }
  71. }
  72. }
  73. var data = new byte[formFile.Length];
  74. formFile.OpenReadStream().Read(data, 0, data.Length);
  75. UploadChunkWriter.Instance.Add(new UploadChunkItem
  76. {
  77. ChunkNumber = chunkNumber,
  78. ChunkSize = chunkSize,
  79. Data = data,
  80. FilePath = filePath
  81. });
  82. if (chunkNumber == totalChunks)
  83. {
  84. //等等写入完成
  85. int i = 0;
  86. while (true)
  87. {
  88. if (i >= 20)
  89. {
  90. return new ResultObject<UploadFileResponse>
  91. {
  92. Success = false,
  93. Msg = $"上传失败,总大小:{totalSize},实际大小:{new FileInfo(filePath).Length}",
  94. Result = new UploadFileResponse { Url = "" }
  95. };
  96. }
  97. if (new FileInfo(filePath).Length != totalSize)
  98. {
  99. Thread.Sleep(TimeSpan.FromMilliseconds(1000));
  100. i++;
  101. }
  102. else
  103. {
  104. break;
  105. }
  106. }
  107. var fileUrl = $"{appSetting.RootUrl}{appSetting.Upload.RequestPath}/{yy}/{mm}/{dd}/{fileName}";
  108. var response = new UploadFileResponse { Url = fileUrl };
  109. return new ResultObject<UploadFileResponse>(response);
  110. }
  111. else
  112. {
  113. return new ResultObject<UploadFileResponse>
  114. {
  115. Success = true,
  116. Msg = "uploading...",
  117. Result = new UploadFileResponse { Url = "" }
  118. };
  119. }
  120. }

撇开上面的参数验证,主要逻辑也就是三个,一是创建目标文件,二是分块数据加入队列,三是最后一块的时候要验证文件的完整性(也就是所有的块都上传了,并都写入到了目标文件)

创建目标文件需要保证线程安全,这里使用了双重检查加锁机制,双重检查的优点是避免了不必要的加锁情况。

完整性我只是验证了文件的大小,这只是一种简单的机制,一般是够用了,别忘了我们的接口都是受jwt保护的,包括这里的上传文件。如果要求更高的话,可以让客户端传参整个文件的md5值,然后服务端验证合并之后文件的md5是否和客户端给的一致。

最后要开启写入线程,可以在Startup.cs的Configure方法中开启:

UploadChunkWriter.Instance.Start();

经过这样的整改,上传速度溜溜的,最后一块也不用长时间等待啦!

(项目中当然也包含了不分块上传)

其他功能

自从netcore提供了依赖注入,我也习惯了这种写法,不过在构造函数中写一堆注入实在是难看,而且既要声明字段接收,又要写参数赋值,挺麻烦的,于是乎自己写了个小组件,已经用于手头所有的项目,当然也包含在了NetApiStarter中,不仅解决了属性和字段注入,同时也解决了实现多接口注入的问题,以及一个接口多个实现精准注入的问题,详细说明可查看项目文档Autowired.Core

如果你听过MediatR,那么这个功能不需要介绍了,项目中包含一个应用程序级别的事件发布和订阅的功能,具体使用可查看文档AppEventService

如果你听过AutoMapper,那么这个功能也不需要介绍了,项目中包含一个SimpleMapper,代码不多功能还行,支持嵌套类、数组、IList<>、IDictionary<,>实体映射在多层数据传输的时候可谓是必不可少的功能,用法嘛就不说了,只有一个Map方法太简单了

重中之重

如果你感觉这个项目对你、或者其他人(You or others,没毛病)有稍许帮助,请给个Star好吗!

NetApiStarter仓库地址:https://gitee.com/loogn/NetApiStarter

开始你的api:NetApiStarter的更多相关文章

  1. 干货来袭-整套完整安全的API接口解决方案

    在各种手机APP泛滥的现在,背后都有同样泛滥的API接口在支撑,其中鱼龙混杂,直接裸奔的WEB API大量存在,安全性令人堪优 在以前WEB API概念没有很普及的时候,都采用自已定义的接口和结构,对 ...

  2. 12306官方火车票Api接口

    2017,现在已进入春运期间,真的是一票难求,深有体会.各种购票抢票软件应运而生,也有购买加速包提高抢票几率,可以理解为变相的黄牛.对于技术人员,虽然写一个抢票软件还是比较难的,但是还是简单看看123 ...

  3. 几个有趣的WEB设备API(二)

    浏览器和设备之间还有很多有趣的接口, 1.屏幕朝向接口 浏览器有两种方法来监听屏幕朝向,看是横屏还是竖屏. (1)使用css媒体查询的方法 /* 竖屏 */ @media screen and (or ...

  4. html5 canvas常用api总结(三)--图像变换API

    canvas的图像变换api,可以帮助我们更加方便的绘画出一些酷炫的效果,也可以用来制作动画.接下来将总结一下canvas的变换方法,文末有一个例子来更加深刻的了解和利用这几个api. 1.画布旋转a ...

  5. JavaScript 对数据处理的5个API

    JavaScript对数据处理包括向上取整.向下取整.四舍五入.固定精度和固定长度5种方式,分别对应ceil,floor,round,toFixed,toPrecision等5个API,本文将对这5个 ...

  6. ES5对Array增强的9个API

    为了更方便的对Array进行操作,ES5规范在Array的原型上新增了9个方法,分别是forEach.filter.map.reduce.reduceRight.some.every.indexOf ...

  7. javascript的api设计原则

    前言 本篇博文来自一次公司内部的前端分享,从多个方面讨论了在设计接口时遵循的原则,总共包含了七个大块.系卤煮自己总结的一些经验和教训.本篇博文同时也参考了其他一些文章,相关地址会在后面贴出来.很难做到 ...

  8. 一百元的智能家居——Asp.Net Mvc Api+讯飞语音+Android+Arduino

    大半夜的,先说些废话提提神 如今智能家居已经不再停留在概念阶段,高大上的科技公司都已经推出了自己的部分或全套的智能家居解决方案,不过就目前的现状而言,大多还停留在展厅阶段,还没有广泛的推广起来,有人说 ...

  9. 在一个空ASP.NET Web项目上创建一个ASP.NET Web API 2.0应用

    由于ASP.NET Web API具有与ASP.NET MVC类似的编程方式,再加上目前市面上专门介绍ASP.NET Web API 的书籍少之又少(我们看到的相关内容往往是某本介绍ASP.NET M ...

随机推荐

  1. 处理 Could not find a 'KafkaClient' entry in the JAAS configuration. System property 'java.security.auth.login.config' is

    场景 某监控进程需要访问多个集群的Kafka INFO - org.apache.kafka.common.KafkaException: Failed to construct kafka cons ...

  2. SpringBoot接管SpringMvc

    SpringBoot接管SpringMvc Spring Web MVC framework(通常简称为“Spring MVC”)是一个丰富的“model 视图控制器”web framework. S ...

  3. Linux之Centos7开机之后连不上网

    问题:ns33mtu 1500 qdisc noop state DOWN group default qlen 1000 解决方法: root@topcheer ~]# systemctl stop ...

  4. python argparse:命令行参数解析详解

    简介 本文介绍的是argparse模块的基本使用方法,尤其详细介绍add_argument内建方法各个参数的使用及其效果. 本文翻译自argparse的官方说明,并加上一些笔者的理解 import a ...

  5. [springboot 开发单体web shop] 2. Mybatis Generator 生成common mapper

    Mybatis Generator tool 在我们开启一个新项目的研发后,通常要编写很多的entity/pojo/dto/mapper/dao..., 大多研发兄弟们都会抱怨,为什么我要重复写CRU ...

  6. Centos 7修改hostname浅析

    之前写过一篇博客"深入理解Linux修改hostname",里面总结了RHEL 5.7下面如何修改hostname,当然这篇博客的内容其实也适用于CentOS 6,但是自CentO ...

  7. 【重构】AndroidStudio中代码重构菜单Refactor功能详解

    代码重构几乎是每个程序员在软件开发中必须要不断去做的事情,以此来不断提高代码的质量.Android Stido(以下简称AS)以其强大的功能,成为当下Android开发工程师最受欢迎的开发工具,也是A ...

  8. Windows 程序包管理器 Chocolatey:一条命令装软件

    Windows 程序包管理器 Chocolatey:一条命令装软件 本文原始地址:https://sitoi.cn/posts/46278.html 介绍 Chocolatey 是一种软件管理解决方案 ...

  9. python基础-集合set及内置方法

    数据类型之集合-set 用途:多用于去重,关系运算 定义方式:通过大括号存储,集合中的每个元素通过逗号分隔.集合内存储的元素必须是不可变的,因此,列表-List 和字典dict 不能存储在集合中 注意 ...

  10. 程序员这十个java题你都会吗?

    前言 不论你是职场新人还是步入职场N年的职场新人大哥大~当然这个N<3~,我能担保你答不对这十个题~不要问我为什么这么自信~,这些个题还是"有水平"的javase的基础题,传 ...