WebApi详解+Unity注入--上篇:基于Framework的WebApi

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}";
        }
    }
  1. 使用Role/GetVal直接调用Get

    C# 复制代码
      [Route("GetVal")]//自定义路由
            
            public IEnumerable<string> Get(string val1,string val2)
            {
                return new string[] { val1, val2 };
            }
  2. 使用路由模板获取参数值。

    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实现版本控制
  1. 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 }
                );
  2. 不同的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 };
        }
      
    }
}
  1. 创建派生自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;
        }
    }
  1. 将该选择器注册至配置。

    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

  1. 定义类。
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));
            }

        }
    }
  1. 注册过滤器
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());
        }
    }
}
  1. 使用。

    注意:使用[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跳过授权验证

  1. 自定义派生自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;
                }
            }
        }
  2. 在控制器上使用该特性。

    C# 复制代码
     [UserAuthorizationFilter]
        public class UserController : ApiController
        {
            // GET api/<controller>
            public IEnumerable<string> Get()
            {
                return new string[] { "value1", "value2" };
            }
        }
  1. 通过[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部分

注意PayloadHeader 部分都是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

  1. Controller继承自和 MVC一样的Controller路由配置,在Controller上标注[Rounte("api/[controller]")],在方法上标注[HttpGet],[HttpPost]等,也可以使用[HttpGet("id")]这样的格式,可以使用[HttpPost("Test")]这样的方式标注在方法上。这样用http/localhost:9000/api/values/Test访问。
  2. Action支持IActionResult作为返回值,但不支持HttpResponseMessage作为返回值。
  3. 不再支持IHttpControllerSelector,而是用IApplicationModelConvertion实现多版本控制。

Demo链接

https://download.csdn.net/download/lingxiao16888/92544537?spm=1001.2014.3001.5501

相关推荐
ttod_qzstudio19 小时前
从Unity的C#到Babylon.js的typescript:“函数重载“变成“类型魔法“
typescript·c#·重载·babylon.js
eggcode20 小时前
C#读写Bson格式的文件
c#·json·bson
是一个Bug20 小时前
Java后端开发面试题清单(50道) - 分布式基础
java·分布式·wpf
爱说实话20 小时前
C# 20260109
开发语言·c#
无心水20 小时前
【分布式利器:腾讯TSF】4、TSF配置中心深度解析:微服务动态配置的终极解决方案
分布式·微服务·架构·wpf·分布式利器·腾讯tsf·分布式利器:腾讯tsf
一心赚狗粮的宇叔1 天前
中级软件开发工程师2025年度总结
java·大数据·oracle·c#
cplmlm1 天前
EF Core使用CodeFirst生成postgresql数据库表名以及字段名用蛇形命名法,而类名仍使用驼峰命名
c#
lingxiao168881 天前
WebApi详解+Unity注入--下篇:Unity注入
unity·c#·wpf