一次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 }
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>
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
"作者:" 数据酷软件工作室
"出处:" http://datacool.cnblogs.com
"专注于CMS(综合赋码系统),MES,WCS(智能仓储设备控制系统),WMS,商超,桑拿、餐饮、客房、足浴等行业收银系统的开发,15年+从业经验。因为专业,所以出色。"
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++