从壹开始前后端分离【 .NET Core2.2/3.0 +Vue2.0 】框架之十二 || 三种跨域方式比较,DTOs(数据传输对象)初探
本文3.0版本文章
更新反馈
1、博友@落幕残情童鞋说到了,Nginx反向代理实现跨域,因为我目前还没有使用到,给忽略了,这次记录下,为下次补充。此坑已填
2、提示:跨域的姊妹篇——《三十三║ ⅖ 种方法实现完美跨域》
代码已上传Github+Gitee,文末有地址
今天忙着给小伙伴们提出的问题解答,时间上没把握好,都快下班了,赶紧发布:书说上文《从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之十一 || AOP自定义筛选,Redis入门 11.1》,昨天咱们说到了分布式缓存键值数据库,主要讲解了如何安装,使用,最后遗留了一个问题,同步+Redis缓存还是比较简单,如何使用异步泛型存取Redis,还是一直我的心结,希望大家有会的,可以不吝赐教,本系列教程已经基本到了尾声,今天就说两个小的知识点,既然本系列是讲解前后端分离的,那一定会遇到跨域的问题,没错,今天将说下跨域!然后顺便说一下DTOs(数据传输对象),这些东西大家都用过,比如,在MVC中定义一个ViewModel,是基于Model实体类的,然后做了相应的变化,以适应前端需求,没错,就是这个,如果大型的实体类,一个个复杂的话会稍显费力,今天就是用一个自动映射工具——AutoMapper。
零、今天完成左下角的深紫色部分
一、为什么会出现跨域的问题
跨域问题由来已久,主要是来源于浏览器的”同源策略”。
何为同源?只有当协议、端口、和域名都相同的页面,则两个页面具有相同的源。只要网站的 协议名protocol、 主机host、 端口号port 这三个中的任意一个不同,网站间的数据请求与传输便构成了跨域调用,会受到同源策略的限制。 同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本(如 JavaScript)对不同域的服务进行跨站调用(通常指使用XMLHttpRequest请求)。
所以说我们在web中,我们无法去获取跨域的请求,常见的就是无法通过js获取接口(这里要说下我的以前使用的经验:在同源系统下,前端js去调用后端接口,然后后端C#去调取跨域接口,这是我以前采用的办法,但是前后端分离,这个办法肯定就是不行了,因为那时候已经没有了前后端之分,是两个项目),所以我们只要合理使用同源策略,就可以达到跨域访问的目的。
二、三种跨域方式 之JsonP
我自己建立了一个静态页面,用来模拟前端访问,具体如下步骤:
1、模拟前端访问页面
新建一个Html页面,使用Jquery来发送请求(文件在项目的WWW文件夹下,大家可以自己下载,或者Copy下边代码)。
一共三种跨域方法:
<html> <head> <meta charset="utf-8"> <title>Blog.Core</title> <script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js"></script> <style> div { margin: 10px; word-wrap: break-word; } </style> <script> $(document).ready(function () { $("#jsonp").click(function () { $.getJSON("/api/Login/jsonp?callBack=?", function (data) { $("#data-jsonp").html("数据: " + data.value); }); }); $("#cors").click(function () { $.get("/api/Login/Token", function (data, status) { console.log(data); $("#status-cors").html("状态: " + status); $("#data-cors").html("数据: " + data? data.token:"失败"); }); }); $("#cors-post").click(function () { let postdata = { "bID": 10, "bsubmitter": "222", "btitle": "33333", "bcategory": "4444", "bcontent": "5555", "btraffic": 0, "bcommentNum": 0, "bUpdateTime": "2018-11-08T02:36:26.557Z", "bCreateTime": "2018-11-08T02:36:26.557Z", "bRemark": "string" }; $.ajax({ type: 'post', url: '/api/Values', contentType: 'application/json', data: JSON.stringify(postdata), success: function (data, status) { console.log(data); $("#status-cors-post").html("状态: " + status); $("#data-cors-post").html("数据: " + JSON.stringify(data)); } }); //$.ajax({ // type: "POST", // url: "/api/Values", // success: function (data, status) { // console.log(data); // $("#status-cors-post").html("状态: " + status); // $("#data-cors-post").html("数据: " + data); // } //}); }); }); </script> </head> <body> <h3>通过JsonP实现跨域请求</h3> <button id="jsonp">发送一个 GET </button> <div id="status-jsonp"></div> <div id="data-jsonp"></div> <hr /> <h3>添加请求头实现跨域</h3> 无 <hr /> <h3>通过CORS实现跨域请求,另需要在服务器端配置CORE</h3> <button id="cors">发送一个 GET </button> <div id="status-cors"></div> <div id="data-cors"></div> <hr /> <button id="cors-post">发送一个 POST </button> <div id="status-cors-post"></div> <div id="data-cors-post"></div> <hr /> </body> </html>
注意:这里一定要注意jsonp的前端页面请求写法,要求很严谨
2、请求页面部署
1、其实只需要当前Blog.Core 项目配置了静态文件中间件,直接访问就可以
比如我的在线地址:查看右侧公告栏
2、单独部署:将这个页面部署到自己的IIS中(拷贝到文件里,直接在iis添加该文件,访问刚刚的Html文件目录就行)
3、设计后台接口
在我们的项目 LoginController 中,设计Jsonp接口,Core调用的接口我们已经有了,就是之前获取Token的接口GetJWTStr
[HttpGet] [Route("jsonp")] public void Getjsonp(string callBack, long id = 1, string sub = "Admin", int expiresSliding = 30, int expiresAbsoulute = 30) { TokenModel tokenModel = new TokenModel(); tokenModel.Uid = id; tokenModel.Sub = sub; DateTime d1 = DateTime.Now; DateTime d2 = d1.AddMinutes(expiresSliding); DateTime d3 = d1.AddDays(expiresAbsoulute); TimeSpan sliding = d2 - d1; TimeSpan absoulute = d3 - d1; string jwtStr = BlogCoreToken.IssueJWT(tokenModel, sliding, absoulute);
//重要,一定要这么写 string response = string.Format("\"value\":\"{0}\"", jwtStr); string call = callBack + "({"+response+"})"; Response.WriteAsync(call); }
注意:这里一定要注意jsonp的接口写法,要求很严谨
4、点击”通过JsonP实现跨域请求“按钮,发现已经有数据了,证明Jsonp跨域已经成功,你可以换成自己的域名试一试,但是Cors的还不行
三、三种跨域方式 之添加请求头实现跨域
这里我没有写到代码里,是在一般处理程序里之前用到的
1、后端
public void ProcessRequest(HttpContext context) { //接收参数 string uName = context.Request["name"]; string data = "{\"name\":\"" + uName + "\",\"age\":\"18\"}"; //只需在服务端添加以下两句 context.Response.AddHeader("Access-Control-Allow-Origin", "*"); //跨域可以请求的方式 context.Response.AddHeader("Access-Control-Allow-Methods", "POST,GET"); context.Response.Write(data); }
2、前端
function ashxRequest() { $.post("http://localhost:5551/ashxRequest.ashx", { name: "halo" }, function (data) { for (var i in data) { alert(data[i]); } }, "json") }
大家感兴趣可以自己实验下。有问题请留言
四、三种跨域方式 之 高效CORS
1、前端ajax调用
前端的代码在jsonp的时候已经写好,请往上看第二节,后端接口也是Token接口
剩下的就是配置跨域了,很简单!
2、配置 CORS 跨域
在ConfigureServices中添加
#region CORS //跨域第一种方法,先注入服务,声明策略,然后再下边app中配置开启中间件 services.AddCors(c => { //↓↓↓↓↓↓↓注意正式环境不要使用这种全开放的处理↓↓↓↓↓↓↓↓↓↓ c.AddPolicy("AllRequests", policy => { policy .AllowAnyOrigin()//允许任何源 .AllowAnyMethod()//允许任何方式 .AllowAnyHeader()//允许任何头 .AllowCredentials();//允许cookie }); //↑↑↑↑↑↑↑注意正式环境不要使用这种全开放的处理↑↑↑↑↑↑↑↑↑↑ //一般采用这种方法 c.AddPolicy("LimitRequests", policy => { policy .WithOrigins("http://127.0.0.1:1818", "http://localhost:8080", "http://localhost:8021", "http://localhost:8081", "http://localhost:1818")//支持多个域名端口,注意端口号后不要带/斜杆:比如localhost:8000/,是错的 .AllowAnyHeader()//Ensures that the policy allows any header. .AllowAnyMethod(); }); }); // 这是第二种注入跨域服务的方法,这里有歧义,部分读者可能没看懂,请看下边解释 //services.AddCors(); #endregion
歧义解释:
可能有些读者会说,你这里写错了,应该是 app.UseCors() ,我肯定是知道的,那为啥还要这么写呢,是因为这里我提供了两套 Cors 跨域写法:
1、配置在 configureServices 中,然后再在管道中开启中间件,就是上边的写法;
2、还有一个是,只在 configureServices 中,开启服务,然后在中间件中,具体的配置:
//跨域第一种版本,请要ConfigureService中配置服务 services.AddCors(); // app.UseCors(options => options.WithOrigins("http://localhost:8021").AllowAnyHeader() //.AllowAnyMethod());
基本注释都有,大家都能看的懂,就这么简单!
注意:在定义策略 LimitRequests 的时候,源域名应该是客户端请求的端口域名,不是当前API的域名端口。
感谢博友 @学弱 提醒:CORS的配置一定要放在AutoFac前面,否则builder.Populate(services);后,你再进行配置会没有效果。
3、启动中间件
在启动文件 的Configure 配置方法里,添加启用Cors中间件服务
感谢博友@kiritio_ooo的提醒,Git已更新
注意:如果你使用了 app.UserMvc() 或者 app.UseHttpsRedirection()这类的中间件,一定要把 app.UseCors() 写在它们的上边,先进行跨域,再进行 Http 请求,否则会提示跨域失败。
因为这两个都是涉及到 Http请求的,如果你不跨域就直接转发或者mvc,那肯定报错。
4、运行调试,一切正常
至此,跨域的问题已经完成辣
重要:如果你想查看效果,我的最新的Github上代码已经给大家写好了,大家clone以后,只需要执行 http://localhost:8081/corspost.html ,就能看到各种效果了。当然如果懒得下载,可以看我的在线效果:查看右侧公告栏
注意:这里要说下,如果遇到了跨域失败的提示,比如这样:
这个并不一定是没有配置好导致的跨域失败,还有可能是接口有错误,比如 500,或者是 404 了,导致的接口异常,所以就提示访问有错误。
五、其他跨域方法补充
请参考我的文章:
nginx是一个高性能的web服务器,常用作反向代理服务器。nginx作为反向代理服务器,就是把http请求转发到另一个或者一些服务器上。
通过把本地一个url前缀映射到要跨域访问的web服务器上,就可以实现跨域访问。
对于浏览器来说,访问的就是同源服务器上的一个url。而nginx通过检测url前缀,把http请求转发到后面真实的物理服务器。并通过rewrite命令把前缀再去掉。这样真实的服务器就可以正确处理请求,并且并不知道这个请求是来自代理服务器的。
简单说,nginx服务器欺骗了浏览器,让它认为这是同源调用,从而解决了浏览器的跨域问题。又通过重写url,欺骗了真实的服务器,让它以为这个http请求是直接来自与用户浏览器的。
这样,为了解决跨域问题,只需要动一下nginx配置文件即可。
六、结语
三种办法其实都能达到目的,但是优缺点也很明显
1、手动创建JSONP跨域
优点:无浏览器要求,可以在任何浏览器中使用此方式
缺点:格式要求很严格,只支持get请求方式,请求的后端出错不会有提示,造成不能处理异常
2、添加请求头实现跨域
优点:支持任意请求方式,并且后端出错会像非跨域那样有报错,可以对异常进行处理
缺点:兼容性不是很好,IE的话 <IE10 都不支持此方式
虽然CORS的方法有点儿类似请求头,但是封装,兼容性,灵活性都要好的很多,强烈推荐。
七、初探DTOs
请看以下实体类
//数据库实体类 public class Author { public string Name { get; set; } } public class Book { public string Title { get; set; } public Author Author { get; set; } } //页面实体类 public class BookViewModel { public string Title { get; set; } public string Author { get; set; } } //api调用 BookViewModel model = new BookViewModel { Title = book.Title, Author = book.Author.Name }
上面的例子相当的直观了,我们平时也是这么用的基本,但是问题也随之而来了,我们可以看到在上面的代码中,如果一旦在Book对象里添加了一个额外的字段,而后想在前台页面输出这个字段,那么就需要去在项目里找到每一处有这样BookViewModel转换字段的地方,这是非常繁琐的。另外,BookViewModel.Author是一个string类型的字段,但是Book.Author属性却是Author对象类型的,我们用的解决方法是通过Book.Auther对象来取得Author的Name属性值,然后再赋值给BookViewModel的Author属性,这样看起行的通,但是想一想,如果打算在以后的开发中把Name拆分成两个-FisrtName和LastName,我的天呐!我们得去把原来的ViewModel对象也拆分成对应的两个字段,然后在项目中找到所有的转换,然后替换。
那么有什么办法或者工具来帮助我们能够避免这样的情况发生呢?AutoMapper正是符合要求的一款插件。只需一键操作,就能一劳永逸,解决所有问题,然后通过依赖注入,快速使用:
//AutoMapper自动映射 //Mapper.Initialize(cfg => cfg.CreateMap<BlogArticle, BlogViewModels>()); //BlogViewModels models = Mapper.Map<BlogArticle, BlogViewModels>(blogArticle); BlogViewModels models = IMapper.Map<BlogViewModels>(blogArticle);//就这一句话完全搞定所有转换
今天因为时间的关系,没有说到Automapper,明天再见吧~
八、CODE