ASP.NET Core 2.0集成Office Online Server(OWAS)实现办公文档的在线预览与编辑(支持wordexcelpptpdf等格式)

发布日期:2019-02-28

Office Online Server是微软开发的一套基于Office实现在线文档预览编辑的技术框架(支持当前主流的浏览器,且浏览器上无需安装任何插件,支持word、excel、ppt、pdf等文档格式),其客户端通过WebApi方式可集成到自已的应用中,支持Java、C#等语言。Office Online Server原名为:Office Web Apps Server(简称OWAS)。因为近期有ASP.NET Core 2.0的项目中要实现在线文档预览与编辑,就想着将Office Online Server集成到项目中来,通过网上查找,发现大部分的客户端的实现都是基于ASP.NET的,而我在实现到ASP.NET Core 2.0的过程中也遇到了不少的问题,所以就有了今天这篇文章。

 

安装Office Online Server

微软的东西在安装上都是很简单的,下载安装包一路”下一步“就可完成。也可参考如下说明来进行安装:https://docs.microsoft.com/zh-cn/officeonlineserver/deploy-office-online-server

完成安装后会在服务器上的IIS上自动创建两个网站,分别为:HTTP80、HTTP809。其中HTTP80站绑定80、443端口,HTTP809站绑定809、810端口。

 

业务关系

1、Office Online Server服务端(WOPI Server),安装在服务器上用于受理来自客户端的预览、编辑请求等。服务端很吃内存的,单机一定不能低于8G内存。

2、Office Online Server客户端(WOPI Client),这里因为集成在了自已的项目中,所以Office Online Server客户端也就是自已的项目中的子系统。

用户通过项目中的业务系统请求客户端并发起对某一文档的预览或编辑请求,客户端接受请求后再通过调用服务端的WebApi完成一系列约定通讯后,服务端在线输出文档并完成预览与编辑功能。

 

实现原理

可通过如下图(图片来自互联网)能清晰的看出浏览器、Office Online Server服务端、Office Online Server客户端之间的交互顺序与关系。在这过程中,Office Online Server客户端需自行生成Token及身份验证,这也是为保障Office Online Server客户端的安全手段。

 

实现代码

客户端编写拦截器,拦截器中主要接受来自服务端的请求,并根据服务端的请求类型做出相应动作,请求类型包含如下几种:CheckFileInfo、GetFile、Lock、GetLock、RefreshLock、Unlock、UnlockAndRelock、PutFile、PutRelativeFile、RenameFile、DeleteFile、PutUserInfo等。具体代码如下:

1 using Microsoft.AspNetCore.Http 2 using Newtonsoft.Json 3 using System 4 using System.Collections.Generic 5 using System.IO 6 using System.Linq 7 using System.Text 8 using System.Threading 9 using System.Threading.Tasks 10 using System.Web 11 //编写一个处理WOPI请求的客户端拦截器 12 namespace Lezhima.Wopi.Base 13 { 14 public class ContentProvider 15 { 16 //声明请求代理 17 private readonly RequestDelegate _nextDelegate 18 19 20 public ContentProvider(RequestDelegate nextDelegate) 21 { 22 _nextDelegate = nextDelegate 23 } 24 25 26 //拉截并接受所有请求 27 public async Task Invoke(HttpContext context) 28 { 29 //判断是否为来自WOPI服务端的请求 30 if (context.Request.Path.ToString().ToLower().IndexOf("files") >= 0) 31 { 32 WopiRequest requestData = ParseRequest(context.Request) 33 34 switch (requestData.Type) 35 { 36 //获取文件信息 37 case RequestType.CheckFileInfo: 38 await HandleCheckFileInfoRequest(context requestData) 39 break 40 41 //尝试解锁并重新锁定 42 case RequestType.UnlockAndRelock: 43 HandleUnlockAndRelockRequest(context requestData) 44 break 45 46 //获取文件 47 case RequestType.GetFile: 48 await HandleGetFileRequest(context requestData) 49 break 50 51 //写入文件 52 case RequestType.PutFile: 53 await HandlePutFileRequest(context requestData) 54 break 55 56 default: 57 ReturnServerError(context.Response) 58 break 59 } 60 } 61 else 62 { 63 await _nextDelegate.Invoke(context) 64 } 65 } 66 67 68 69 70 /// <summary> 71 /// 接受并处理获取文件信息的请求 72 /// </summary> 73 /// <remarks> 74 /// </remarks> 75 private async Task HandleCheckFileInfoRequest(HttpContext context WopiRequest requestData) 76 { 77 //判断是否有合法token 78 if (!ValidateAccess(requestData writeAccessRequired: false)) 79 { 80 ReturnInvalidToken(context.Response) 81 return 82 } 83 //获取文件 84 IFileStorage storage = FileStorageFactory.CreateFileStorage() 85 DateTime? lastModifiedTime = DateTime.Now 86 try 87 { 88 CheckFileInfoResponse responseData = new CheckFileInfoResponse() 89 { 90 //获取文件名称 91 BaseFileName = Path.GetFileName(requestData.Id) 92 Size = Convert.ToInt32(size) 93 Version = Convert.ToDateTime((DateTime)lastModifiedTime).ToFileTimeUtc().ToString() 94 SupportsLocks = true 95 SupportsUpdate = true 96 UserCanNotWriteRelative = true 97 98 ReadOnly = false 99 UserCanWrite = true100 }101 102 var jsonString = JsonConvert.SerializeObject(responseData)103 104 ReturnSuccess(context.Response)105 106 await context.Response.WriteAsync(jsonString)107 108 }109 catch (UnauthorizedAccessException ex)110 {111 ReturnFileUnknown(context.Response)112 }113 }114 115 /// <summary>116 /// 接受并处理获取文件的请求117 /// </summary>118 /// <remarks>119 /// </remarks>120 private async Task HandleGetFileRequest(HttpContext context WopiRequest requestData)121 {122 //判断是否有合法token 123 if (!ValidateAccess(requestData writeAccessRequired: false))124 {125 ReturnInvalidToken(context.Response)126 return127 }128 129 130 //获取文件 131 var stream = await storage.GetFile(requestData.FileId)132 133 if (null == stream)134 {135 ReturnFileUnknown(context.Response)136 return137 }138 139 try140 {141 int i = 0142 List<byte> bytes = new List<byte>()143 do144 {145 byte[] buffer = new byte[1024]146 i = stream.Read(buffer 0 1024)147 if (i > 0)148 {149 byte[] data = new byte[i]150 Array.Copy(buffer data i)151 bytes.AddRange(data)152 }153 }154 while (i > 0)155 156 157 ReturnSuccess(context.Response)158 await context.Response.Body.WriteAsync(bytes bytes.Count)159 160 }161 catch (UnauthorizedAccessException)162 {163 ReturnFileUnknown(context.Response)164 }165 catch (FileNotFoundException ex)166 {167 ReturnFileUnknown(context.Response)168 }169 170 }171 172 /// <summary>173 /// 接受并处理写入文件的请求174 /// </summary>175 /// <remarks>176 /// </remarks>177 private async Task HandlePutFileRequest(HttpContext context WopiRequest requestData)178 {179 //判断是否有合法token 180 if (!ValidateAccess(requestData writeAccessRequired: true))181 {182 ReturnInvalidToken(context.Response)183 return184 }185 186 try187 {188 //写入文件189 int result = await storage.UploadFile(requestData.FileId context.Request.Body)190 if (result != 0)191 {192 ReturnServerError(context.Response)193 return194 }195 196 ReturnSuccess(context.Response)197 }198 catch (UnauthorizedAccessException)199 {200 ReturnFileUnknown(context.Response)201 }202 catch (IOException ex)203 {204 ReturnServerError(context.Response)205 }206 }207 208 209 210 private static void ReturnServerError(HttpResponse response)211 {212 ReturnStatus(response 500 "Server Error")213 }214 215 }216 }

 

拦截器有了后,再到Startup.cs文件中注入即可,具体代码如下:

1 public void Configure(IApplicationBuilder app IHostingEnvironment env) 2 { 3 if (env.IsDevelopment()) 4 { 5 app.UseDeveloperExceptionPage() 6 app.UseBrowserLink() 7 } 8 else 9 { 10 app.UseExceptionHandler("/Home/Error") 11 } 12 13 app.UseStaticFiles() 14 app.UseAuthentication() 15 16 //注入中间件拦截器,这是将咱们写的那个Wopi客户端拦截器注入进来 17 app.UseMiddleware<ContentProvider>() 18 19 app.UseMvc(routes => 20 { 21 routes.MapRoute( 22 name: "default" 23 template: "{controller=Home}/{action=Index}/{name?}") 24 }) 25 }

 

至止,整个基于Office Online Server技术框架在ASP.NET Core上的文档预览/编辑功能就完成了。够简单的吧!!

 

总结

1、Office Online Server服务端建议在服务器上独立部署,不要与其它业务系统混合部署。因为这货实在是太能吃内存了,其内部用了WebCached缓存机制是导致内存增高的一个因素。

2、Office Online Server很多资料上要求要用AD域,但我实际在集成客户端时没有涉及到这块,也就是说服务端是开放的,但客户端是通过自行颁发的Token与验证来保障安全的。

3、利用编写中间件拦截器,并在Startup.cs文件中注入中间件的方式来截获来自WOPI服务端的所有请求,并对不同的请求类型做出相应的处理。