1. ASP.NET Core WebAPI

1.1. 基本配置

1.1.1. 项目初始化修改

新建项目,选择API模板,基于ASP.NET Core3.1的默认模板结构如下图所示:

通常采用控制台方式执行调试,修改项目中【launchSettings.json】配置文件如下:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "MyCoreWebAPI": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "weatherforecast",
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

此时,运行程序会弹出控制台窗口,本机【Kestrel】监听【applicationUrl】配置的端口,并打开浏览器访问

API的调试围绕数据,后续将采用【PostMan】进行测试运行。

1.1.2. watch_run

dotnet-watch 是 asp.net 项目下的一个工具,用于实时监视项目文件夹中的文件变动,

一旦有文件变动,自动重新编译并运行项目,在调试过程中,你将无需重复:

修改源代码->CTRL+SHIFT+B编译->F5调试->发现问题->修改源代码........

右键当前项目,选择【在文件资源管理器中打开文件夹】,在地址栏输入 cmd 进入命令提示符:

命令提示符中输入 dotnet watch run 自动监视编译命令,后面修改后端代码也会自动编译,提高效率

1.2. 认证与授权

1.2.1. 认证

修改 WeatherForecastController 控制器,对 Get 接口增加 [Authorize]标签

[HttpGet]
[Authorize] // 新增该标签
public IEnumerable<WeatherForecast> Get()
{
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

通过 PostMan 接口测试,返回内容如下:

返回信息提示我们需要添加认证服务,对请求进行身份认证。此处采用JWT令牌认证方式。

1.2.2. JWT

JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。

JWT的官网地址:https://jwt.io/

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。

这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  1. 用户使用用户名密码来请求服务器
  2. 服务器进行验证用户的信息
  3. 服务器通过验证发送给用户一个token
  4. 客户端存储token,并在每次请求时附送上这个token值
  5. 服务端验证token值,并返回数据

1.2.3. JWT组件

通过 NuGet 进行安装 JwtBearer 组件

修改【Startup】类中注入服务方法 ConfigureServices 和配置中间件方法 Configure 如下:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 加密的秘钥,不能少于16位
    SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("wangyuanweiwangyuanwei"));

    // 添加认证服务,用于对用户验证,相当于登录拦截
    services.AddAuthentication("Bearer").AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = securityKey,

            // 是否验证颁发者
            ValidateIssuer = true,
            // 颁发者的名称
            ValidIssuer = "颁发者",

            // 是否验证接收者
            ValidateAudience = true,
            // 接受者名称
            ValidAudience = "接收者",

            // 是否必须具有“过期”值。
            RequireExpirationTime = true,
            // 是否在令牌验证期间验证生存期
            ValidateLifetime = true
        };
    });

}

// 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.UseRouting();

    // 管道中应用认证中间件
    app.UseAuthentication();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

此时访问webapi显示为401未授权

1.2.4. 获取token值

新增控制器 【LoginController】,核心代码如下:

[Route("[controller]")]
[ApiController]
public class LoginController : ControllerBase
{
    /// <summary>
    /// 获取令牌
    /// </summary>
    /// <returns></returns>
    public string GetToken()
    {
        // 此处秘钥需要和 Startup 中保持一致
        var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("wangyuanweiwangyuanwei"));

        SecurityToken securityToken = new JwtSecurityToken(
            issuer: "颁发者",
            audience: "接收者",
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256),
            expires: DateTime.Now.AddMinutes(20),
            claims: new Claim[] { }
        );

        string jwt = new JwtSecurityTokenHandler().WriteToken(securityToken);
        return jwt;
    }
}

【PostMan】中进行测试,返回一个 token 值:

这个 token 值可以在官网 https://jwt.io/ 进行校验:

在【PostMan】中测试 WeatherForecast 接口,请求头中附加 Authorization 信息【Bearer token】

如上所示,token验证通过。

1.2.5. 授权

前面通过获取 token ,我们已经可以认证通过并访问所有接口资源。

但实际业务场景中,经常会有不同角色权限的区分,比如某些接口只允许管理员进行访问。

授权对应的中间件是: app.UseAuthorization();

首先,我们针对接口的特性 Authorize 增加角色配置,修改【WeatherForecastController】控制器:

[HttpGet]
[Authorize(Roles = "Admin")]// 角色为Admin用户才有权限访问
public IEnumerable<WeatherForecast> Get()
{
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateTime.Now.AddDays(index),
        TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
    })
    .ToArray();
}

此时访问 WeatherForecast 接口,虽然通过了服务器的认证,但未通过该接口的授权

服务端返回的 403 ,并非之前的 401。注意这两个状态码的区分

为了通过 token 可以确认当前用户是否可以授权,我们需要在 token 中增加更多的 Claim 身份信息。

修改【LoginController】中 GetToken 方法

/// <summary>
/// 获取令牌
/// </summary>
/// <returns></returns>
public string GetToken()
{
    // 此处秘钥需要和 Startup 中保持一致
    var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("wangyuanweiwangyuanwei"));

    SecurityToken securityToken = new JwtSecurityToken(
        issuer: "颁发者",
        audience: "接收者",
        signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256),
        expires: DateTime.Now.AddMinutes(20),
        claims: new Claim[] {
            // 增加身份信息,此处设置角色为 Admin,与标签 [Authorize(Roles = "Admin")] 匹配
            new Claim(ClaimTypes.Role,"Admin")
        }
    );

    string jwt = new JwtSecurityTokenHandler().WriteToken(securityToken);
    return jwt;
}

重新获取新的添加有身份信息的 token 值,并重新请求前面案例中的 WeatherForecast 接口测试。结果略。。。

1.3. AOP

1.3.1. AOP说明

AOP全称 Aspect Oriented Progarmming(面向切面编程),其实 AOP 对 ASP.NET 程序员来说一点都不神秘,

你也许早就通过Filter来完成一些通用的功能,例如你使用 Authorization Filter 来拦截所有的用户请求,

验证 Http Header 中是否有合法的token。或者使用 Exception Filter 来处理某种特定的异常。

你之所以可以拦截所有的用户请求,能够在期望的时机来执行某些通用的行为,是因为 ASP.NET Core 在框架级别预留了一些钩子,

他允许你在特定的时机注入一些行为。对 ASP.NET Core 应用程序来说,这个时机就是 HTTP 请求在执行 MVC Action 的中间件时。

显然这个时机并不能满足你的所有求,比如你在 Repository 层有一个读取数据库的方法:

public void GetUser()
{
    //Get user from db
}

你试图得到该方法执行的时间,首先想到的方式就是在整个方法外面包一层用来计算时间的代码:

public void GetUserWithTime()
{
    var stopwatch = Stopwatch.StartNew();
    try
    {
        //Get user from db
    }
    finally
    {
        stopwatch.Stop();
        Trace.WriteLine("Total" + stopwatch.ElapsedMilliseconds + "ms");
    }
}

如果仅仅是为了得到这一个方法的执行时间,这种方式可以满足你的需求。

问题在于你有可能还想得到 DeleteUser 或者 UpdateUser 等方法的执行时间。

修改每一个方法并添加计算时间的代码明显是不合适的。

一个比较优雅的做法是给需要计算时间的方法标记一个Attribute,但只适用于

[Time]
public void GetUser()
{
    //Get user from db
}

把计算时间这个功能当做一个切面(Aspect)注入到了现有的逻辑中,这是一个AOP的典型应用。

1.3.2. AOP开源类库

C#中可以用来做AOP的类库有很多,我们以使用率较高的 【Castle.Core】举例说明:

通过 NuGet 程序包管理添加【Castle.Core】组件,并添加 ServiceInterceptor 拦截器文件夹,项目结构如下:

创建 IUserServiceUserService 模拟具体的业务操作,相当于 Repository 层:

public interface IUserService
{
    bool Login(string uid, string pwd);
    bool Delete(string uid);
}
public class UserService : IUserService
{
    public bool Delete(string uid)
    {
        Console.WriteLine("正在删除中....");
        Thread.Sleep(1000);// 模拟业务耗时操作
        Console.WriteLine("已成功删除!");
        return true;
    }

    public bool Login(string uid, string pwd)
    {
        Console.WriteLine("正在登录中....");
        Thread.Sleep(1500);// 模拟业务耗时操作
        bool success = uid.Equals("admin");
        if (success)
        {
            Console.WriteLine("登录成功!");
        }
        else
        {
            Console.WriteLine("登录成功!");
        }
        return success;
    }
}

增加拦截器(代理) TimeWatcherInterceptor 类:

public class TimeWatcherInterceptor : Castle.DynamicProxy.IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Stopwatch stopwatch = Stopwatch.StartNew();
        Console.WriteLine($"TimeWatcherInterceptor:开始...");
        stopwatch.Start();

        invocation.Proceed();

        stopwatch.Stop();
        Console.WriteLine($"TimeWatcherInterceptor:结束,耗时:{stopwatch.Elapsed.TotalSeconds}s");
    }
}

我们需要在 Startup 中进行注入,并且设置接口的实现以及针对接口实现的代理:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddScoped<UserService>();
    services.AddScoped<TimeWatcherInterceptor>();

    services.AddScoped(provider =>
    {
        var generator = new ProxyGenerator();
        // 服务实例,实际业务操作
        var target = provider.GetService<UserService>();
        // 拦截器(代理)的实例化
        var interceptor = provider.GetService<TimeWatcherInterceptor>();

        // 根据 接口创建对应服务的代理
        return generator.CreateInterfaceProxyWithTarget<IUserService>(target, interceptor);
    });
}

WeatherForecastController 控制器中即可通过构造的注入获取到服务实例,并进行模拟业务操作:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IUserService _userService;

    public WeatherForecastController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpGet]
    public bool Get()
    {
        return _userService.Login(HttpContext.Request.Form["uname"], "");
    }
}

可以观察到,对于原先的业务接口 IUserService 和业务实现 UserService 没有任何侵入性

通过拦截(代理)的方式,实现了运行时间的统计,其余的切面操作也是类似。

多个服务可以采用批量注入的方式,todo。。。。

1.4. 跨域

如果两个 Url 具有相同的方案、主机和端口(RFC 6454),则它们具有相同的源。

对于不同源的请求,通常都会报错,如下:

Access to XMLHttpRequest at 'http://localhost:5000/xxxxx' from origin 'null' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

WebAPI 通常都需要进行跨域处理,CORS 中间件处理跨域请求。

修改【Startup】类中 ConfigureServices 方法,注入跨域配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 注入跨域设置
    services.AddCors(opt =>
    {
        // 增加全局跨域规则,global_cors 是规则名称,UseCors需要使用到
        opt.AddPolicy("global_cors", cor =>
        {
            // 允许所有来源的 CORS 请求和任何方案(http 或 https)
            cor.AllowAnyOrigin();
        });
    });
}

修改 Configure 方法,启用注入的跨域配置:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // 按照规则名称应用跨域规则,必须在 UseRouting 前应用
    app.UseCors("global_cors");

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

按照以上配置即可启用全局的允许所有来源的跨域请求。

1.5. 数据绑定与获取

1.5.1. 来源注解

HTTP 请求中,会携带很多参数,这些参数可以在前端设置,例如表单、Header、文件、Cookie、Session、Token等。

微软定义了以下注解用于区分数据获取的方式:

特性 绑定源
[FromBody] 请求正文
[FromForm] 请求正文中的表单数据
[FromHeader] 请求标头
[FromQuery] 请求查询字符串参数
[FromRoute] 当前请求中的路由数据
[FromServices] 作为操作参数插入的请求服务

1.5.2. 简单类型和复杂类型

下面我们以较为常见的 [FromBody][FromForm] 举例说明。

对于 WebApi 来说,如果默认不加任何注解,简单类型默认为 Query ,复杂类型为 Json。

以微软官网WebAPI中Product案例为基础,【Product】类:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Category { get; set; }
    public decimal Price { get; set; }
}

创建API控制器【ProductController】:

// 通过 action 区分 api,不是 RestfulAPI
[Route("api/[controller]/[action]")]
[ApiController]
public class ProductController : ControllerBase
{
    static IList<Product> products = new List<Product>()
    {
        new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 },
        new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M },
        new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M }
    };

    // 此处 id 为默认类型,参数来源Query,即url传参
    [HttpGet]
    public IList<Product> GetProducts(int? id)
    {
        if (id.HasValue)
        {
            return products.Where(t => t.Id == id.Value).ToList();
        }
        else
        {
            return products;
        }
    }

    [HttpPost]
    public bool AddProduct(Product entity)
    {
        // Id 值不允许重复
        if (products.Count(t => t.Id == entity.Id) > 0)
        {
            return false;
        }
        products.Add(entity);
        return true;
    }
}

【ProductController】类中 GetProducts 方法,基本类型参数 id 来源于 [FromQuery]

下图 PostMan 中测试情况,在 Body 中附加参数无法获取。

【ProductController】类中 AddProduct 方法,复杂类型(对象) entity 来源于 [FromBody]

等价于下面的定义:

[HttpPost]
public bool AddProduct([FormBody]Product entity){
    // .....
}

在 PostMan 中调用如下:

默认是以 content-type : application/json; 进行传参的,这里必须要注意!

针对传统的 content-type : application/x-www-form-urlencoded 类型,需要修改数据注解:

[HttpPost]
public bool AddProduct([FormForm]Product entity){
    // .....
}

此处省略 PostMan 测试...

1.6. 其他

1.6.1. json数据key大小写

在 Startup 类里的 ConfigureServices 方法里进行配置即可:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers().AddJsonOptions(opt =>
    {
        // json序列化默认方式 camel命名
        //opt.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

        // json序列化时不修改大小写,与属性大小写保持一致
        opt.JsonSerializerOptions.PropertyNamingPolicy = null;
    });
}

1.6.2. api返回DataTable

webapi返回DataTable数据,抛出以下问题:

System.NotSupportedException: The collection type 'System.Data.DataRelationCollection' on 'System.Data.DataTable.ChildRelations' is not supported.

Exception in serialization of System.Data.DataTable using the new “System.Text.Json” class (Asp.net core 3.0 preview 8)

安装 Newtonsoft 代替原 System.Text.Json。

NuGet 包管理器中安装 Microsoft.AspNetCore.Mvc.NewtonsoftJson ,在 ConfigureServices() 方法注入 AddNewtonsoftJson() 处理方法

services.AddControllers()
    .AddNewtonsoftJson(set =>
    {
        // Use the default property (Pascal) casing
        set.SerializerSettings.ContractResolver = new DefaultContractResolver();
        // 修改 json 序列号时时间格式
        set.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
    });

参考引用:

什么是Security token? 什么是Claim?

.NET Core中实现AOP编程

小范笔记:ASP.NET Core API 基础知识与Axios前端提交数据

ASP.NET Core 中的模型绑定

results matching ""

    No results matching ""