一次asp.net core3.1打造webapi开发框架的实践
实践技术看点
- 1、Swagger管理API说明文档
- 2、JwtBearer token验证
- 3、Swagger UI增加Authentication
- 4、EntityFrameworkCore+MySQL
- 5、在.net core 3.1下使用Log4net
前言
元旦过后就没什么工作上的任务了,这当然不能让领导看在眼里,动手实践一下新技术吧。于是准备搭一个webapi的中间件框架。
由于自己的云主机是台linux服务器,双核2G的centos+1M 没有数据盘,也用不起RDS,如果装个Windows Server那么肯定卡的不行,所以一直想尝试一下跨平台的感觉。
由于这篇随笔不是定位于教程,故基础知识一概略过。
项目中使用到的包清单:
- <ItemGroup>
- <PackageReference Include="IdentityModel" Version="4.1.1" />
- <PackageReference Include="log4net" Version="2.0.8" />
- <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.1" />
- <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.1" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.1">
- <PrivateAssets>all</PrivateAssets>
- <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
- </PackageReference>
- <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.1" />
- <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
- <PackageReference Include="MySql.Data.EntityFrameworkCore" Version="8.0.19" />
- <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.0" />
- <PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="5.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="5.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.0.0" />
- </ItemGroup>
关键代码点评
1)Startup
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Text;
- using System.Threading.Tasks;
- using IdentityModel;
- using Microsoft.AspNetCore.Authentication.JwtBearer;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.Extensions.Hosting;
- using Microsoft.Extensions.Logging;
- using Microsoft.IdentityModel.Tokens;
- using Microsoft.OpenApi.Models;
- using MySql.Data.EntityFrameworkCore.Extensions;
- using Swashbuckle.AspNetCore.Swagger;
- using tokendemo.Models;
- namespace tokendemo
- {
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
- public IConfiguration Configuration { get; }
- // This method gets called by the runtime. Use this method to add services to the container.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddControllers();
- services.Configure<TokenManagement>(Configuration.GetSection("tokenManagement"));
- var token = Configuration.GetSection("tokenManagement").Get<TokenManagement>();
- services.AddAuthentication(x =>
- {
- x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
- x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
- }).AddJwtBearer(x =>
- {
- x.RequireHttpsMetadata = false;
- x.SaveToken = true;
- x.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
- ValidIssuer = token.Issuer,
- ValidAudience = token.Audience,
- ValidateIssuer = false,
- ValidateAudience = false
- };
- });
- services.AddSwaggerGen(c =>
- {
- c.SwaggerDoc("v1",
- new OpenApiInfo
- {
- Title = "XXX项目接口文档",
- Version = "v1",
- Contact = new OpenApiContact
- {
- Email = "xyf_xiao@cquni.com",
- Name = "肖远峰",
- Url = new Uri("http://datacool.cnblogs.com")
- }
- });
- // 为 Swagger 设置xml文档注释路径
- var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
- var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
- c.IncludeXmlComments(xmlPath);
- c.AddSecurityDefinition("Bearer",
- new OpenApiSecurityScheme
- {
- Description = "请输入OAuth接口返回的Token,前置Bearer。示例:Bearer {Roken}",
- Name = "Authorization",
- In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
- Type = SecuritySchemeType.ApiKey
- });
- c.AddSecurityRequirement(new OpenApiSecurityRequirement
- {
- {
- new OpenApiSecurityScheme
- {
- Reference = new OpenApiReference()
- {
- Id = "Bearer",
- Type = ReferenceType.SecurityScheme
- }
- }, Array.Empty<string>()
- }
- });
- });
- var posdbConnString = Configuration.GetConnectionString("POS_Db");
- services.AddDbContext<posdbContext>(option =>
- {
- option.UseMySql(posdbConnString, null);
- });
- services.AddScoped<IAuthenticateService, TokenAuthenticationService>();
- services.AddScoped<IUserService, UserService>();
- }
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
- app.UseHttpsRedirection();
- app.UseAuthentication();
- app.UseRouting();
- app.UseAuthorization();
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- });
- app.UseSwagger();
- //启用中间件服务生成SwaggerUI,指定Swagger JSON终结点
- app.UseSwaggerUI(c =>
- {
- c.SwaggerEndpoint("/swagger/v1/swagger.json", "XXX接口文档 V1");
- c.RoutePrefix = string.Empty;//设置根节点访问
- });
- app.UseLog4net();
- }
- }
- }
- using Microsoft.AspNetCore.Builder;
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Threading.Tasks;
- namespace tokendemo
- {
- public static class LoggeServiceExt
- {
- /// 使用log4net配置
- /// <param name="app"></param>
- /// <returns></returns>
- public static IApplicationBuilder UseLog4net(this IApplicationBuilder app)
- {
- var logRepository = log4net.LogManager.CreateRepository(Assembly.GetEntryAssembly(), typeof(log4net.Repository.Hierarchy.Hierarchy));
- log4net.Config.XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));
- return app;
- }
- }
- }
- public interface IUserService
- {
- bool IsValid(LoginRequestDTO req);
- }
- public interface IAuthenticateService
- {
- bool IsAuthenticated(LoginRequestDTO request, out string token);
- }
- public class UserService : IUserService
- {
- public bool IsValid(LoginRequestDTO req)
- {
- return true;
- }
- }
- public class TokenAuthenticationService : IAuthenticateService
- {
- private readonly IUserService _userService;
- private readonly TokenManagement _tokenManagement;
- private readonly posdbContext db;
- public TokenAuthenticationService(IUserService userService, IOptions<TokenManagement> tokenManagement, posdbContext posdb)
- {
- _userService = userService;
- _tokenManagement = tokenManagement.Value;
- db = posdb;
- }
- public string GetAuthentUser()
- {
- return JsonConvert.SerializeObject(db.SysApiAuthorize.ToList());
- }
- public bool IsAuthenticated(LoginRequestDTO request, out string token)
- {
- token = string.Empty;
- if (!_userService.IsValid(request))
- return false;
- var claims = new[]
- {
- new Claim(ClaimTypes.Name,request.Username)
- };
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenManagement.Secret));
- var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
- var jwtToken = new JwtSecurityToken(_tokenManagement.Issuer, _tokenManagement.Audience, claims, expires: DateTime.Now.AddMinutes(_tokenManagement.AccessExpiration), signingCredentials: credentials);
- token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
- return true;
- }
- }
token验证是我关注的重点,而Swagger支持查看文档的同时调用API,也支持授权认证,所以水到渠成。代码命名都是比较规范的,当然大部分来源于别人的文章,这里就不作过多说明了。
asp.net core对依赖注入思想是贯彻始终的,新人需要在这个思想的领悟上下苦功夫才能驾驭她。
2)配置文件
- 1 {
- 2 "Logging": {
- 3 "LogLevel": {
- 4 "Default": "Information",
- 5 "Microsoft": "Warning",
- 6 "Microsoft.Hosting.Lifetime": "Information"
- 7 }
- 8 },
- 9 "AllowedHosts": "*",
- 10 "tokenManagement": {
- 11 "secret": "157300523770123456",
- 12 "issuer": "datacool.cnblogs.com",
- 13 "audience": "WebApi",
- 14 "accessExpiration": 30,
- 15 "refreshExpiration": 60
- 16 },
- 17 "ConnectionStrings": {
- 18 "POS_Db": "server=localhost;userid=root;pwd=dba#2020;port=3306;database=posdb;sslmode=none"
- 19 }
- 20 }
appsettings
- 1 <?xml version="1.0" encoding="utf-8" ?>
- 2 <log4net>
- 3 <!--定义输出到文件中-->
- 4 <appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
- 5 <!--定义文件存放位置-->
- 6 <param name="AppendToFile" value="true"/>
- 7 <!--最小锁定模型以允许多个进程可以写入同一个文件-->
- 8 <param name="LockingModel" value="log4net.Appender.FileAppender.MinimalLock"/>
- 9 <param name="StaticLogFileName" value="true"/>
- 10 <!--保存路径-->
- 11 <param name="File" value="Log/Logs_"/>
- 12 <param name="DatePattern" value="yyyyMMdd".txt""/>
- 13 <param name="StaticLogFileName" value="false"/>
- 14 <param name="RollingStyle" value="Date"/>
- 15 <layout type="log4net.Layout.PatternLayout">
- 16 <!--每条日志末尾的文字说明-->
- 17 <footer value=""/>
- 18 <!--输出格式-->
- 19 <!--样例:2008-03-26 13:42:32,111 [10] INFO Log4NetDemo.MainClass [(null)] - info-->
- 20 <conversionPattern value="%newline%date 级别:%-5level 日志描述:%message%newline"/>
- 21 </layout>
- 22 </appender>
- 23 <root>
- 24 <!--文件形式记录日志-->
- 25 <appender-ref ref="LogFileAppender"/>
- 26 </root>
- 27 </log4net>
log4net.config
Scaffold-DbContext “server=localhost;userid=root;pwd=dba#2020;port=3306;database=posdb;sslmode=none;” Pomelo.EntityFrameworkCore.MySql -OutputDir Models -Force
由于我的数据库是先存在了,所以直接使用了nutget控制台生成了数据库上下文对象和实体。注意向导生成的数据库上下文里是把数据库连接字符串写死的,需要修改。本例是写入appsettings.json里的。请重点看一下上面的配置和Startup里获取配置的代码。
3)关联代码,几个数据传输类
- public class TokenManagement
- {
- [JsonProperty("secret")]
- public string Secret { get; set; }
- [JsonProperty("issuer")]
- public string Issuer { get; set; }
- [JsonProperty("audience")]
- public string Audience { get; set; }
- [JsonProperty("accessExpiration")]
- public int AccessExpiration { get; set; }
- [JsonProperty("refreshExpiration")]
- public int RefreshExpiration { get; set; }
- }
- public class WeatherForecast
- {
- public DateTime Date { get; set; }
- public int TemperatureC { get; set; }
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
- public string Summary { get; set; }
- }
- public class LoginRequestDTO
- {
- [Required]
- [JsonProperty("username")]
- public string Username { get; set; }
- [Required]
- [JsonProperty("password")]
- public string Password { get; set; }
- }
3)API控制器
- 1 using System;
- 2 using System.Collections.Generic;
- 3 using System.Linq;
- 4 using System.Threading.Tasks;
- 5 using log4net;
- 6 using Microsoft.AspNetCore.Authorization;
- 7 using Microsoft.AspNetCore.Mvc;
- 8
- 9 namespace tokendemo.Controllers
- 10 {
- 11 [ApiController]
- 12 [Route("[controller]")]
- 13 [Authorize]
- 14 public class WeatherForecastController : ControllerBase
- 15 {
- 16 private readonly ILog _logger;
- 17 public WeatherForecastController()
- 18 {
- 19 _logger = LogManager.GetLogger(typeof(WeatherForecastController));
- 20 }
- 21 private static readonly string[] Summaries = new[]
- 22 {
- 23 "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- 24 };
- 25
- 26
- 27 [HttpGet]
- 28 public IEnumerable<WeatherForecast> Get()
- 29 {
- 30 var rng = new Random();
- 31 _logger.Info("OK");
- 32 return Enumerable.Range(1, 5).Select(index => new WeatherForecast
- 33 {
- 34 Date = DateTime.Now.AddDays(index),
- 35 TemperatureC = rng.Next(-20, 55),
- 36 Summary = Summaries[rng.Next(Summaries.Length)]
- 37 })
- 38 .ToArray();
- 39 }
- 40
- 41 }
- 42 }
这个大家应该很熟悉了,这就是vs2019向导创建的API控制器。[Authorize]标记会导致401错误,就是表示先要去获取access token,在Header里带入Bearer+空格+token才可以正常调用。
授权控制器
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- namespace tokendemo.Controllers
- {
- [Route("api/[controller]")]
- [ApiController]
- public class AuthenticationController : ControllerBase
- {
- private readonly IAuthenticateService _authService;
- public AuthenticationController(IAuthenticateService service)
- {
- _authService = service;
- }
- [AllowAnonymous]
- [HttpPost, Route("requestToken")]
- public ActionResult RequestToken([FromBody] LoginRequestDTO request)
- {
- if (!ModelState.IsValid)
- {
- return BadRequest("Invalid Request");
- }
- string token;
- var authTime = DateTime.UtcNow;
- if (_authService.IsAuthenticated(request, out token))
- {
- return Ok(new
- {
- access_token = token,
- token_type = "Bearer",
- profile = new
- {
- sid = request.Username,
- auth_time = new DateTimeOffset(authTime).ToUnixTimeSeconds()
- }
- });
- }
- return BadRequest("Invalid Request");
- }
- }
- }
收获与感想
- 1、妥妥的吃了次螃蟹,收获了经验
- 2、正在“为自己挖一口井”的路上
- 3、.net core算是入门了
- 4、源码我是没自信放到github的,后面会加上下载链接
- 5、伙计们分享起来吧,这个生态建设任重而道远啊。
这里是源码下载的地址:https://files.cnblogs.com/files/datacool/tokendemo.zip