1,创建Framework框架下的WebApi
1.1,添加项目模板

1.2,创建空项目
勿选择WebApi模板,因该模板是基于MVC,若选择该模板将创建很多与MVC相关的文件。

1.3,选中项目中的Controllers文件夹创建控制器。


2,配置
2.1,配置项目的URL
项目->右键属性->Web->项目URL 配置项目启动的IPAddress与Port。

2.2,添加支持的内容类型
打开 App_Start文件下的WebApiConfig.cs
C#
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API 配置和服务
// Web API 路由
config.MapHttpAttributeRoutes();
//新增支持的内容类型
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue(System.Net.Mime.MediaTypeNames.Text.Plain));
config.Formatters.JsonFormatter.SupportedMediaTypes.Add(new System.Net.Http.Headers.MediaTypeHeaderValue("text/html"));
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
3,WebApi参数
参数传递,接收规则(Get,Post均是如此):
- 普通类型未加特性修饰的参数例如string、int,其参数值来源于查询字符串。
- 使用
[FromBody]修饰的普通类型参数,传递参数时不需要设置key,设置key则接收的值为null - 自定义的类未加特性修饰作为参数时,其参数值来源于Body(消息体),而非查询字符串。即使查询字符串设置的参数值,如果消息体未添加值,接收到的也是
null。 - 使用
[FromUri]修饰的自定义类作为参数时,其参数值来源查询字符串。 - 使用
[FromBody]修饰的自定义类作为参数时,其参数值来源于Body(消息体)。
3.1,返回类型为Void。
返回类型为Void时,返回的状态码为204(No Content)
C#
public void Delete(int id)
{
}

3.2,无参的Get请求
C#
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}

3.2,有参的Get请求
3.2.1,通过Id传递参数
C#
public string Get2(int id)
{
return id.ToString();
}

3.2.2,通过查询字符串传递参数
C#
[HttpGet]
public string Get3(string userName,string roleName)
{
return $"{userName}----{roleName}";
}

3.2.3,以模型作为参数进行传递
-
使用
[FromUri]进行修饰时([FromUri]只能在形参中使用一次):参数值来自于查询字符串
C#
[HttpGet]
public UserRole Get4([FromUri] UserRole userRole)
{
return userRole;
}

-
模型没有任何特性修饰时
模型参数值来自于
Body
C#
[HttpGet]
public UserRole Get5( UserRole userRole)
{
return userRole;
}

-
使用特性
[FromBody]修饰模型参数时([FromBody]只能在形参中使用一次):模型参数值来源于
Body
C#
[HttpGet]
public UserRole Get6([FromBody] UserRole userRole)
{
return userRole;
}

3.3,无参的Post请求
C#
[HttpPost]
public IEnumerable<string> Post1()
{
return new string[] { "value1", "value2" };
}

3.4,有参的Post请求
3.4.1,通过Id传递参数
C#
[HttpPost]
public string Post2(int id)
{
return id.ToString();
}

3.4.2,通过查询字符串传递参数
C#
[HttpPost]
public string Post4(string userName, string roleName)
{
return $"{userName}----{roleName}";
}

3.4.3,传递[FromBody]修饰的普通类型的参数
C#
[HttpPost]
public string Post3([FromBody] string value)
{
return $"{value}----{DateTime.Now}";
}
参数不能以常规的KeyValue形式传递,而是需要缺省Key,直接传递Value,若以KeyValue进行传递则Value接收到是null
例如以下:

实际上value获得的参数值为:null
正确做法是:缺省键

value接收到传递的参数值:aaa
3.4.4,以模型作为参数进行传递
-
使用
[FromUrl]特性修饰时:模型参数值来源于查询字符串。
C#
[HttpPost]
public UserRole Post5([FromUri] UserRole userRole)
{
return userRole;
}

由结果截图可知:查询字符串与表单同时传参时,对于使用[FromUrl]特性修饰的模型参数,其参数值来源于查询字符串,而非消息Body的表单
-
不使用任何特性修饰时:
模型参数值来源于Body
C#[HttpPost] public UserRole Post6(UserRole userRole) { return userRole; }
-
使用
[FromBody]修饰时:模型参数来源于Body
C#
[HttpPost]
public UserRole Post7([FromBody] UserRole userRole)
{
return userRole;
}

3.5,路由参数
使用[RoutePrefix]路由前缀特性修饰控制器类,可跳过WebApiConfig.cs中的路由模板规则。
C#
[RoutePrefix("Role")]//路由前缀通过该可跳过WebApiConfig.cs中的路由模板
public class RoleController : ApiController
{
// GET api/<controller>
[Route("GetVal")]//自定义路由
public IEnumerable<string> Get(string val1,string val2)
{
return new string[] { val1, val2 };
}
[Route("{userName}/{roleName}")]
public string Post3(string userName, string roleName)
{
return $"{userName}---{roleName}";
}
}
-
使用
Role/GetVal直接调用GetC#[Route("GetVal")]//自定义路由 public IEnumerable<string> Get(string val1,string val2) { return new string[] { val1, val2 }; }
-
使用路由模板获取参数值。
C#[Route("{userName}/{roleName}")] public string Post3(string userName, string roleName) { return $"{userName}---{roleName}"; }
4,WebAPI多版本管理
4.1,自定义路由实现多版本路由控制
[RoutePrefix("Role/v1")] [RoutePrefix("Role/v2")]
4.2,自定义IHttpControllerSelector实现版本控制
-
WebApiConfig中的路由改成如下:
C#config.Routes.MapHttpRoute( name: "DefaultApiv1", routeTemplate: "api/v1/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.Routes.MapHttpRoute( name: "DefaultApiv2", routeTemplate: "api/v2/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); -
不同的Controller放在不同的namespace下

C#
//v1
namespace MultiVersionControl.Controllers.v1
{
public class UserController : ApiController
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
}
}
//v2
namespace MultiVersionControl.Controllers.v2
{
public class UserController : ApiController
{
// GET api/<controller>
[HttpGet]
public IEnumerable<string> Get1()
{
return new string[] { "value1", "value2" };
}
[HttpGet]
public IEnumerable<string> Get2(string userName,string roleName)
{
return new string[] { userName, roleName };
}
}
}
-
创建派生自
DefaultHttpControllerSelector的类,并进行相应配置DefaultHttpControllerSelector是实现接口IHttpControllerSelector的类。
C#
public class HttpControllerSelector : DefaultHttpControllerSelector
{
HttpConfiguration configuration;
public HttpControllerSelector(HttpConfiguration configuration) : base(configuration)
{
this.configuration = configuration;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
var pathStr = request.RequestUri.PathAndQuery;
Match match = Regex.Match(pathStr, @"\/api\/(?<versions>\w+)\/(?<controllerName>\w+)",RegexOptions.IgnoreCase);
// 示例: controllerName:V1.User
if (match.Success)
{
string key = $"{match.Groups["versions"].Value}.{match.Groups["controllerName"].Value}".ToLower();
var dic = GetControllerMapping();
if (dic.Keys.Contains(key))
{
return dic[key];
}
}
//其他类型交由父类处理
return base.SelectController(request);
}
public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
{
Dictionary<string, HttpControllerDescriptor> dic = new Dictionary<string, HttpControllerDescriptor>();
var assemblies = configuration.Services.GetAssembliesResolver().GetAssemblies();
//Controllers.v2
foreach (var assem in assemblies)
{
foreach (Type type in assem.GetTypes())
{
if (!type.IsAbstract && type.IsSubclassOf(typeof(ApiController)))
{
Match match = Regex.Match(type.FullName, @"Controllers\.(?<Name>v\d+\.\w+)Controller",RegexOptions.IgnoreCase);
if (match.Success)
{
string controllerName = match.Groups["Name"].Value?.ToLower();
if (controllerName == null) continue;
HttpControllerDescriptor httpControllerDescriptor = new HttpControllerDescriptor(configuration, type.Name, type);
dic.Add(controllerName, httpControllerDescriptor);
}
}
}
}
return dic;
}
}
-
将该选择器注册至配置。
C#namespace MultiVersionControl { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 配置和服务 // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApiv1", routeTemplate: "api/v1/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } ); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/v2/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } ); //注册控制器选择器 config.Services.Replace(typeof(IHttpControllerSelector), new HttpControllerSelector(config)); } } }
5,WebAPI的Filter
5.1,权限认证的过滤
5.1.1,注册**实现接口:IAuthorizationFilter**的类实现过滤。
作用范围:所有的Controller与Action
- 定义类。
C#
public class MyAuthorFilter : IAuthorizationFilter
{
public bool AllowMultiple => true;
public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
{
//筛选出报文头中的Username
if(actionContext.Request.Headers.TryGetValues("UserName",out IEnumerable<string> values))
{
string userName = values.FirstOrDefault();
if (userName.Equals("admin", StringComparison.OrdinalIgnoreCase))
{
//验证通过
return continuation();
}
else
{
return Task.FromResult(new HttpResponseMessage {
StatusCode = System.Net.HttpStatusCode.OK,
Content = new StringContent("该用户:"+userName+"无权限"),
});
}
}
else
{
return Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized));
}
}
}
- 注册过滤器
C#
namespace WebApiFilter
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API 配置和服务
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//添加过滤器
config.Filters.Add(new MyAuthorFilter());
}
}
}
-
使用。
注意:使用
[AllowAnonymous]特性不能跳过通过IAuthorizationFilter要求的授权验证。C#public class UserController : ApiController { // GET api/<controller> public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/<controller>/5 // [AllowAnonymous]//该特性不能跳过实现于IAuthorizationFilter的授权要求 public string Get(int id) { return id.ToString(); } }


5.1.2,使用[Authorize]特性实现过滤
通过派生自[Authorize]特性的自定义特性可以自定义授权验证条件。
[Authorize]特性作用于控制器时,可通过[AllowAnonymous]特性使该控制器下的某个Action跳过授权验证。
-
自定义派生自
AuthorizeAttribute的类。C#public class UserAuthorizationFilter : AuthorizeAttribute { public override void OnAuthorization(HttpActionContext actionContext) { base.OnAuthorization(actionContext); } protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) { //base.HandleUnauthorizedRequest(actionContext); actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized) { Content = new System.Net.Http.StringContent( $"{{\"success\":false,\"msg\":\"没有权限,拒绝访问\"}}", System.Text.Encoding.UTF8, "application/json") }; } protected override bool IsAuthorized(HttpActionContext actionContext) { if (actionContext.Request.Headers.TryGetValues("userName", out IEnumerable<string> values)) { string userName = values.FirstOrDefault(); return (userName?.Equals("admin")) == true; } else { return false; } } } -
在控制器上使用该特性。
C#[UserAuthorizationFilter] public class UserController : ApiController { // GET api/<controller> public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } }


- 通过
[AllowAnonymous]特性使控制器类的某个操作跳过授权验证。
C#
[UserAuthorizationFilter]
public class UserController : ApiController
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<controller>/5
// [Authorize]
[AllowAnonymous]//该操作跳过授权验证
public string Get(int id)
{
return "value";
}
}

5.2,WebAPI异常处理过滤
1,默认情况服务器抛出异常包含了服务器相关信息
C#
public class UserController : ApiController
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<controller>/5
public string Get(int id)
{
if (id == 10) throw new ArgumentException("参数错误,Id不能为10");
return id.ToString();
}
}

2,注册实现接口IExceptionFilter的类对异常进行处理
C#
public class UserController : ApiController
{
// GET api/<controller>
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/<controller>/5
public string Get(int id)
{
if (id == 10) throw new ArgumentException("参数错误,Id不能为10");
return id.ToString();
}
}
public class MyExceptionFilter : IExceptionFilter
{
public bool AllowMultiple => true;
public Task ExecuteExceptionFilterAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
//使用日志记录异常....
Exception ex = actionExecutedContext.Exception;
//自定义异常返回信息
actionExecutedContext.Response = new System.Net.Http.HttpResponseMessage {
StatusCode = System.Net.HttpStatusCode.InternalServerError,
Content = new System.Net.Http.StringContent($"{{\"success\":false,\"msg\":\"{ex.Message}\"}}", System.Text.Encoding.UTF8, "application/json")
}
;
return Task.CompletedTask;
}
}
注册过滤器
C#
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API 配置和服务
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//注册异常处理过滤
config.Filters.Add(new MyExceptionFilter());
}
}

6,接口的安全性问题
6.1,JWT介绍
JWT由三块组成,可以把用户名,用户id等保存都Payload部分

注意Payload 和Header 部分都是Base64编码,可以轻松的Base64解码回来,因此Payload 部分约等于是明文,因此不能在Payload 中保存不能让他人看到的敏感信息。虽然Payload 部分约等于明文,但是不用担心Payload 被篡改,因为Signature 部分是根据header+payload+secretkey 进行加密算出来的,如果payload被篡改,就可以根据Signature解密时候校验。
例如以下计算出来的token:


Header部分可直接Base64解码

Payload部分可直接Base6464解码

Signature部分为加密算法加密,Base64无法解码

6.2,安装JWT

6.3,过期时间
在Payload 中增加一个名字为exp 的值(exp为固定名),值为过期时间(格林时间非本地时间)和1970/1/1 00:00:00相差的秒数,需要注意的是时间是Utc标准时间而不是本地时间。

6.4,应用示例
C#
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
var payload = new Dictionary<string, object>
{
{ "UserId",12},
{"UserName","admin" },
//过期时间(格林时间非本地时间)和1970/1/1 00:00:00相差的秒数
//exp为固定名
//这里表示这个token的有效时间为10s
{"exp",DateTime.UtcNow.AddSeconds(10).Subtract(DateTime.Parse("1970/1/1 00:00:00")).TotalSeconds }
};
string secret = "12345abcde";//秘钥
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer jsonSerializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, jsonSerializer, urlEncoder);
var token = encoder.Encode(payload, secret);
textBox1.Text = token;
}
private void button2_Click(object sender, EventArgs e)
{
//解密
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer jsonSerializer = new JsonNetSerializer();
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(jsonSerializer, dateTimeProvider);
IBase64UrlEncoder base64UrlEncoder = new JwtBase64UrlEncoder();
IJwtDecoder decoder = new JwtDecoder(jsonSerializer, validator, base64UrlEncoder, algorithm);
string secret = "12345abcde";//秘钥
try
{
textBox2.Text = decoder.Decode(textBox1.Text,secret);
}
catch (TokenExpiredException )
{
//该token过期
MessageBox.Show("Token has expired");
}
catch (SignatureVerificationException)
{
//该签名无效
MessageBox.Show("Token has invalid signature");
}
}
}

6.5,注意事项
启用https
https可以增加被抓包的难度,所以只要是部署到客户端,必须启用https。- 篡改请求,比如用户只能看到
/user?id=5,但是用户截获请求后修改id从而能够查看其它数据,把所有请求参数和value按照名字排序后拼接在一起,加上AppSerect计算散列值作为Signature,传送过去,服务器在校验一次。 - 请求重放,重复的发送请求。每次请求的时间都带有当前时间(时间戳),服务器端比较一下如果这个时间和当前时间相差超过一定时间,则失效。因此最多被重放一段时间,这个要求客户端的时间和服务器的时间要保持相差不大。
sign、时间戳也是可以通过表单、QueryString或者报文头等传递。没有绝对安全,所有还是需要通过业务流程来保证安全,比如后端再次校验权限、重要操作要短信验证等。
7,.net core的WebAPI
Controller继承自和MVC一样的Controller路由配置,在Controller上标注[Rounte("api/[controller]")],在方法上标注[HttpGet],[HttpPost]等,也可以使用[HttpGet("id")]这样的格式,可以使用[HttpPost("Test")]这样的方式标注在方法上。这样用http/localhost:9000/api/values/Test访问。Action支持IActionResult作为返回值,但不支持HttpResponseMessage作为返回值。- 不再支持
IHttpControllerSelector,而是用IApplicationModelConvertion实现多版本控制。
Demo链接
https://download.csdn.net/download/lingxiao16888/92544537?spm=1001.2014.3001.5501