aaarticlea/png;base64,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" alt="">

实践技术看点

  • 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)配置文件

 {
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"tokenManagement": {
"secret": "",
"issuer": "datacool.cnblogs.com",
"audience": "WebApi",
"accessExpiration": ,
"refreshExpiration":
},
"ConnectionStrings": {
"POS_Db": "server=localhost;userid=root;pwd=dba#2020;port=3306;database=posdb;sslmode=none"
}
}

appsettings

 <?xml version="1.0" encoding="utf-8" ?>
<log4net>
<!--定义输出到文件中-->
<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
<!--定义文件存放位置-->
<param name="AppendToFile" value="true"/>
<!--最小锁定模型以允许多个进程可以写入同一个文件-->
<param name="LockingModel" value="log4net.Appender.FileAppender.MinimalLock"/>
<param name="StaticLogFileName" value="true"/>
<!--保存路径-->
<param name="File" value="Log/Logs_"/>
<param name="DatePattern" value="yyyyMMdd&quot;.txt&quot;"/>
<param name="StaticLogFileName" value="false"/>
<param name="RollingStyle" value="Date"/>
<layout type="log4net.Layout.PatternLayout">
<!--每条日志末尾的文字说明-->
<footer value=""/>
<!--输出格式-->
<!--样例:2008-03-26 13:42:32,111 [10] INFO Log4NetDemo.MainClass [(null)] - info-->
<conversionPattern value="%newline%date 级别:%-5level 日志描述:%message%newline"/>
</layout>
</appender>
<root>
<!--文件形式记录日志-->
<appender-ref ref="LogFileAppender"/>
</root>
</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 => + (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控制器

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using log4net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; namespace tokendemo.Controllers
{
[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{
private readonly ILog _logger;
public WeatherForecastController()
{
_logger = LogManager.GetLogger(typeof(WeatherForecastController));
}
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
}; [HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
_logger.Info("OK");
return Enumerable.Range(, ).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-, ),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
} }
}

这个大家应该很熟悉了,这就是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

一次asp.net core3.1打造webapi开发框架的实践的更多相关文章

  1. 使用OAuth打造webapi认证服务供自己的客户端使用

    一.什么是OAuth OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版.注意是Authorization(授权),而不是Authentication(认证). ...

  2. OAuth打造webapi认证服务

    使用OAuth打造webapi认证服务供自己的客户端使用 一.什么是OAuth OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.0版.注意是Authorizati ...

  3. 探索Asp net core3中的 项目文件、Program.cs和通用host(译)

    引言 原文地址 在这篇博客中我将探索一些关于Asp.net core 3.0应用的基础功能--.csproj 项目文件和Program源文件.我将会描述他们从asp.net core 2.X在默认模版 ...

  4. 使用OAuth打造webapi认证服务供自己的客户端使用(二)

    在上一篇”使用OAuth打造webapi认证服务供自己的客户端使用“的文章中我们实现了一个采用了OAuth流程3-密码模式(resource owner password credentials)的W ...

  5. asp.net core系列 38 WebAPI 返回类型与响应格式--必备

    一.返回类型 ASP.NET Core 提供以下 Web API Action方法返回类型选项,以及说明每种返回类型的最佳适用情况: (1) 固定类型 (2) IActionResult (3) Ac ...

  6. asp.net core 2.0 webapi集成signalr

    asp.net core 2.0 webapi集成signalr   在博客园也很多年了,一直未曾分享过什么东西,也没有写过博客,但自己也是汲取着博客园的知识成长的: 这两天想着不能这么无私,最近.N ...

  7. ASP.NET Core 2.2 WebApi 系列【九】使用SignalR (作者:tenghao510 ) 学习及内容补充

    原文地址:  ASP.NET Core 2.2 WebApi 系列[九]使用SignalR 今天,看到了大牛的这篇博文,  发了一下评论, 我很惊喜, 没想到他很快就回复了我,  而且通过QQ帮助了S ...

  8. ASP.NET Core3.1使用IdentityServer4中间件系列随笔(三):创建使用[ClientCredentials客户端凭证]授权模式的客户端

    配套源码:https://gitee.com/jardeng/IdentitySolution 上一篇<ASP.NET Core3.1使用IdentityServer4中间件系列随笔(二):创建 ...

  9. ASP.NET Core3.1使用IdentityServer4中间件系列随笔(二):创建API项目,配置IdentityServer保护API资源

    配套源码:https://gitee.com/jardeng/IdentitySolution 接上一篇<ASP.NET Core3.1使用IdentityServer4中间件系列随笔(一):搭 ...

随机推荐

  1. POJ 2251宽搜、

    因为这个题做了两次犯了两次不同的错误. 第一次用的dfs死活都超时 第二次把定义队列定义在了全局变量的位置,导致连WA了几次.最后找到原因的我真的想一巴掌拍死自己 #include<cstdio ...

  2. Python--day40--复习和回调函数实例

  3. 浅谈集合框架六——集合扩展:Arrays工具类、集合与数组相互转换方式;

    最近刚学完集合框架,想把自己的一些学习笔记与想法整理一下,所以本篇博客或许会有一些内容写的不严谨或者不正确,还请大神指出.初学者对于本篇博客只建议作为参考,欢迎留言共同学习. 之前有介绍集合框架的体系 ...

  4. 2019-10-24-dotnet-列表-Linq-的-Take-用法

    title author date CreateTime categories dotnet 列表 Linq 的 Take 用法 lindexi 2019-10-24 9:4:23 +0800 201 ...

  5. 51nod1327 棋盘游戏

    远古大坑 神仙DP状态设计题 https://blog.csdn.net/white_elephant/article/details/83592103 从行的角度入手,无论如何都要状压 每列最多放一 ...

  6. H3C MSTP

  7. Online Classification

    Another challenging trend in Internet evolution is the tremendous growth of the infrastructure in ev ...

  8. dotnet core 使用 GBK 编码

    本文告诉大家如何在 .NET Core 中使用 GBK 编码 默认的 .NET Core 框架不包含 GBK 编码,不包含除了代码页为 28591 和 Unicode(utf-8,utf-16) 之外 ...

  9. boostrap-非常好用但是容易让人忽略的地方【5】:input-group-btn

    1.正常的使用 <div class="form-group"> <div class="input-group"> <input ...

  10. 16.python内置函数

    Python 内置函数:https://www.runoob.com/python/python-built-in-functions.html 原文:https://www.cnblogs.com/ ...