一个客户项目有引进在线文档操作需求,让我这边做一个demo调研下,给我的对接文档里有相关方法的说明,照着对接即可。但在真正对接过程中还是踩过不少坑,这儿对之前的对接工作做个记录。
按照习惯先来一个效果:
Demo下载链接:https://download.csdn.net/download/qxyywy/88117444
接入指引
1. 申请授权证书并进行授权。
2. 登录系统后台页面
3. 创建、获取应用信息 access_key (简称ak)、 secret_key (简称 sk )。开发者在接口调用时,使用ak 、 sk 生成WPS-4签名进行鉴权。
4. 在线编辑、在线预览、格式转换接入按照对应开放能力文档接入。在线编辑、在线预览对接过程中需要设置回调地址。其中,在线编辑可通过配置开启历史版本功能。
5. 使用过程中需通过证书查询接口关注授权证书状态,若证书即将过期或者不可用,需进行更新证书操作。
6. 在线编辑或在线预览服务端对接完毕,对接方可使用JSSDK,调用API实现相关需求。
WPS-4签名算法
在对接时,耗费了一定时间在WPS-4签名处,对接文档中有WPS-4的说明和样例,自己在对接转换成NetCore的时候踩了一些坑,签名算法中最主要是:Wps-Docs-Authorization的计算方法
签名格式:WPS-4 {accessKey}:{signature} 注意WPS-4后面有空格。
signature:hmac-sha256(secret_key, Ver + HttpMethod + URI + Content-Type + WpsDocs-Date + sha256(HttpBody))
signature的样例如下:WPS-4POST/callback/path/demoapplication/jsonWed, 20 Apr 2022 01:33:07GMTfc005f51a6e75586d2d5d078b657dxxxdf9c1dfa6a7c0c0ba38c715daeb6ede9
这是文档中对签名算法的解释,对照着格式完成相关算法,具体算法如下:
signature的组装:
/// <summary>
/// 获取签名
/// </summary>
/// <param name="method">请求方法,如:GET,POST</param>
/// <param name="uri">请求url,带querystring</param>
/// <param name="body">请求body</param>
/// <param name="date">日期</param>
/// <param name="contentType">默认:application/json</param>
/// <param name="secretKey">应用SK</param>
/// <returns></returns>
public static string WPS4Signature(string secretKey,string method,string uri, byte[] body=null,DateTime? date=null,string contentType= "application/json")
{
//获取uri路径
string path = uri;
//日期格式化
if (date == null)
date = DateTime.Now;
string dateStr = String.Format("{0:r}", date);
//open不参与签名,做替换处理
if (path.StartsWith("/open"))
{
path = path.Replace("/open", "");
}
string sha256body;
//body为空则为空,否则返回sha256(body)
if (body != null && body.Length > 0)
{
sha256body = Sha256(body);
}
else
{
sha256body = "";
}
String signature = null;
signature = HmacSHA256Encrypt($"WPS-4{method.ToUpper()}{path}{contentType}{dateStr}{sha256body}", secretKey);
return signature;
}
HmacSHA256加密算法:
/// <summary>
/// HmacSHA256加密
/// </summary>
/// <param name="secret"></param>
/// <param name="signKey"></param>
/// <returns></returns>
public static string HmacSHA256Encrypt(string secret, string signKey)
{
string signRet = string.Empty;
using (HMACSHA256 mac = new HMACSHA256(Encoding.UTF8.GetBytes(signKey)))
{
byte[] hash = mac.ComputeHash(Encoding.UTF8.GetBytes(secret));
//signRet = Convert.ToBase64String(hash);
signRet = ToHexStrFromByte(hash);
}
return signRet;
}
Sha256转换:
/// <summary>
/// Sha256转换
/// </summary>
/// <param name="input">The input.</param>
/// <returns>A hash.</returns>
public static string Sha256(this byte[] input)
{
if (input == null)
{
return null;
}
using (var sha = SHA256.Create())
{
var hash = sha.ComputeHash(input);
return ToHexStrFromByte(hash);
}
}
字节数组转16进制字符串:
/// <summary>
/// 字节数组转16进制字符串:空格分隔
/// </summary>
/// <param name="byteDatas"></param>
/// <returns></returns>
public static string ToHexStrFromByte(this byte[] byteDatas)
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < byteDatas.Length; i++)
{
builder.Append(string.Format("{0:X2}", byteDatas[i]));
}
return builder.ToString().Trim().ToLower();
}
获取在线预览链接
/// <summary>
/// 获取在线预览链接
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[Route("api/wps/previewgenarate")]
[HttpPost]
public Task<GenarateResult> GenarateWPSPreviewUrl(GenarateRequest request)
{
return Task.Run(() =>
{
string wpsHost = "http://10.4.**.**";
string uri = $"/api/preview/v1/files/{defaultFileId}/link?type=w&preview_mode=high_definition";
string fullUrl = $"{wpsHost}/open{uri}";
Dictionary<string, string> headers = new Dictionary<string, string>();
DateTime now = DateTime.Now;
headers.Add("Content-Type", "application/json");
headers.Add("Wps-Docs-Date", String.Format("{0:r}", now));
var signature = WPSLocalSIgnatureHelper.WPS4Signature("SKrpaxjdwoetijjv", "get", uri, null, now);
string docsAuthorization = WPSLocalSIgnatureHelper.WPS4SignAuthorization("UOMYPEVAHWQLTKJF", signature);
headers.Add("Wps-Docs-Authorization", docsAuthorization);
HttpHelper httpHelper = new HttpHelper();
var resultTemp = httpHelper.Get(fullUrl, headers);
var result = JsonConvert.DeserializeObject<ResponseBaseModel<OnlineEditResultModel>>(resultTemp);
string url = "";
if (result != null && result.data != null)
{
url = result.data.link;
}
return new GenarateResult { Url = url };
});
}
这儿比较坑的地方来了,方法参数都组装好了,通过HttpWebRequest后端发起请求获取WPS中台返回的在线预览地址时,始终提示401报错获取不到数据。迫不得已自己通过APIPost组装了相关请求和参数居然又能获取到相关数据。对照了下请求头,又差异后调整了程序的请求头,保证和apiPost里的完全一致,依然返回401,通过查阅相关资料找到一个处理办法,在发起的请求后手动捕获WebException,然后在WebException里解析数据流获取信息。
HttpWebRequest Get方式访问
/// <summary>
/// Get方式访问
/// </summary>
/// <param name="url"></param>
/// <param name="encode"></param>
/// <param name="referer"></param>
/// <param name="headers"></param>
/// <returns></returns>
public string Get(string url, string encode, string referer, Dictionary<string, string> headers=null)
{
int num = _tryTimes;
HttpWebRequest request = null;
HttpWebResponse response = null;
StreamReader reader = null;
while (num-- >= 0)
{
try
{
DelaySomeTime();
ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);//验证服务器证书回调自动验证
request = (HttpWebRequest)WebRequest.Create(url);
request.Headers.Add("accept", "*/*");
request.Headers.Add("accept-encoding", "gzip, deflate, br");
request.Headers.Add("accept-language", "zh-CN");
request.Headers.Add("connection", "keep-alive");
if (headers != null)
{
foreach (var item in headers)
{
request.Headers.Add(item.Key, item.Value);
}
}
//request.UserAgent = reqUserAgent;
request.CookieContainer = _cookie;
request.Referer = referer;
request.Method = "GET";
request.Timeout = _timeOut;
if (_proxy != null && _proxy.Credentials != null)
{
request.UseDefaultCredentials = true;
}
request.Proxy = _proxy;
response = (HttpWebResponse)request.GetResponse();
reader = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding(encode));
return reader.ReadToEnd();
}
catch (WebException ex)
{
response = (HttpWebResponse)ex.Response; //解析401等错误返回的有效信息
var resultTemp = "";
Stream stream = response.GetResponseStream();
using (StreamReader readers = new StreamReader(stream, Encoding.UTF8))
{
resultTemp = readers.ReadToEnd();
}
return resultTemp;
}
catch (Exception ex)
{
_logger.Error(url + "\r\n" + ex.ToString());
continue;
}
finally
{
if (request != null)
{
request.Abort();
}
if (response != null)
{
response.Close();
}
if (reader != null)
{
reader.Close();
}
}
}
return string.Empty;
}
DemoHtml
到上面时后端的处理就基本完成,前端这边因为时临时demo就用了一个html来处理,真实项目中需要用VUE等处理也都类似。
以下为 html的代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- 建议禁用外框浏览器自带的缩放 -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,user-scalable=no"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>WPS Web Office(iframe)接入指南</title>
<style>
* {
box-sizing: border-box;
}
html,
body {
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
height: 100%;
/* 防止双击缩放 */
touch-action: manipulation;
}
iframe {
flex: 1;
}
</style>
<!-- cdn引入JQ -->
<script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.1.min.js"></script>
<script src="./jwps.js"></script>
<script type="text/javascript">
var appHost = "http://10.4.146.19:8890";
// 支持 HTTPS
// 注意:如果通过postMessage来设置token,请在url参数加上_w_tokentype=1
function showWPS(url) {
// 初始化
var wps = WPS.config({
mount: document.querySelector("#wpsPanl"),
// 文字
wpsUrl: url,
headers: {
shareBtn: {
tooltip: "分享",
subscribe: function() {
console.log("click callback");
}
},
otherMenuBtn: {
tooltip: "其他按钮",
items: [
{
// 自定义, type 固定填 'custom'
type: "custom",
icon:
"http://ep.wps.cn/index/images/logo_white2.png",
text: "API 导出 PDF",
subscribe: function(wps) {
if (wps.WpsApplication) {
wps.WpsApplication()
.ActiveDocument.ExportAsFixedFormatAsync()
.then(function(result) {
console.table(result);
});
}
}
},
{
// 自定义, type 固定填 'custom'
type: "custom",
icon:
"http://ep.wps.cn/index/images/logo_white2.png",
text: "API 使用",
subscribe: function(wps) {
let result;
if (wps.WpsApplication) {
wps.WpsApplication()
.ActiveDocument.ExportAsFixedFormatAsync()
.then(function(result) {
console.table(result);
});
}
}
}
]
}
}
});
return wps;
}
window.onload = function() {
$.ajax({
url: appHost+"/api/wps/previewgenarate",
contentType: "application/json",
dataType: "json",
data: JSON.stringify({
fileId: 'a123',
fileName: "test.docx",
fileType: 1,
userId: 1505340867
}),
type: "post",
success: function(res) {
var wpsUrl = res.url;
console.log(wpsUrl);
var wps = showWPS(wpsUrl);
}
});
var fileInput = document.getElementById("bookimg1");
//选择文件
fileInput.addEventListener('change', function () {
//如果未传入文件则中断
if (fileInput.files[0] == undefined) {
return;
}
var file = fileInput.files[0];
//FileReader可直接将上传文件转化为二进制流
var reader = new FileReader();
reader.readAsDataURL(file);//转化二进制流,异步方法
reader.onload = function (result) {//完成后this.result为二进制流
var base64Str = this.result;
$('#uploadImg').attr('src', base64Str);
$('#imgPreview').show();
}
})
};
function replaceWps() {
$('.tdname1').html($('#name1').val());
$('.tddept1').html($('#dept1').val());
$('.tdage1').html($('#age1').val());
$('.tdname2').html($('#name2').val());
$('.tddept2').html($('#dept2').val());
$('.tdage2').html($('#age2').val());
var fileBytes = "";
var fileName = "";
if ($('#bookimg1')[0].files[0] != undefined) {
var imgFile = $('#bookimg1')[0].files[0];
fileName = imgFile.name;
//FileReader可直接将上传文件转化为二进制流
var reader = new FileReader();
reader.readAsDataURL(imgFile);//转化二进制流,异步方法
reader.onload = function (result) {//完成后this.result为二进制流
var base64Str = this.result;
var startNum = base64Str.indexOf("base64,");
startNum = startNum * 1 + 7;
//去除前部格式信息(如果有需求)
var baseStr = base64Str.slice(startNum);
fileBytes = baseStr;
$.ajax({
url: appHost + "/api/wps/wrapheader",
contentType: "application/json",
dataType: "json",
data: JSON.stringify({
sample_list: [
{
bookmark: 'bookmark1',
type: 'TEXT',
text: $('#bookmark1').val()
},
{
bookmark: 'bookmark2',
type: 'TEXT',
text: $('#bookmark2').val()
},
{
bookmark: 'bookmark3',
type: 'TEXT',
text: $('#bookmark3').val()
},
{
bookmark: 'bookimg1',
type: 'IMAGE',
/*sample_url:$('#bookimg1').val(),*/
sample_filename: fileName,
text: fileBytes,
},
{
bookmark: 'bookform1',
type: 'TEXT',
text: $('#testForm').prop("outerHTML")
}
],
}),
type: "post",
success: function (res) {
var wpsUrl = res.url;;
var wps = showWPS(wpsUrl);
}
});
}
} else {
$.ajax({
url: "http://10.4.146.19:8890/api/wps/wrapheader",
contentType: "application/json",
dataType: "json",
data: JSON.stringify({
sample_list: [
{
bookmark: 'bookmark1',
type: 'TEXT',
text: $('#bookmark1').val()
},
{
bookmark: 'bookmark2',
type: 'TEXT',
text: $('#bookmark2').val()
},
{
bookmark: 'bookmark3',
type: 'TEXT',
text: $('#bookmark3').val()
},
{
bookmark: 'bookform1',
type: 'TEXT',
text: $('#testForm').prop("outerHTML")
}
],
}),
type: "post",
success: function (res) {
var wpsUrl = res.url;;
var wps = showWPS(wpsUrl);
}
});
}
}
</script>
</head>
<body>
<div style="width:100%;height:700px;">
<div id="wpsPanl" style="width:65%;float:left;height:100%;"></div>
<div id="form" style="width:34%;float:left;height:100%;padding-top:50px;padding-left: 20px;">
<div><div class="title">课题名称:</div><input class="input" type="text" id="bookmark1" placeholder="请填写课题名称"></div>
<dl style="clear:both;"></dl>
<div><div class="title">课题申报单位:</div><input class="input" type="text" id="bookmark2" placeholder="请填写课题申报单位"></div>
<dl style="clear:both;"></dl>
<div><div class="title">课题负责人:</div><input class="input" type="text" id="bookmark3" placeholder="请填写课题负责人"></div>
<dl style="clear:both;"></dl>
<div style="height:140px">
<div class="title">课题成员:</div>
<table>
<thead><tr><td>姓名</td><td>所属部门</td><td>年龄</td></tr></thead>
<tr><td><input class="forminput" type="text" id="name1" placeholder="请填写姓名"></td><td><input class="forminput" type="text" id="dept1" placeholder="请填写所属部门"></td><td><input class="forminput" type="text" id="age1" placeholder="请填写年龄"></td></tr>
<tr><td><input class="forminput" type="text" id="name2" placeholder="请填写姓名"></td><td><input class="forminput" type="text" id="dept2" placeholder="请填写所属部门"></td><td><input class="forminput" type="text" id="age2" placeholder="请填写年龄"></td></tr>
</table>
<div style="width:100%;display:none;">
<table border="1" cellspacing="0" style="width:90%;" id="testForm">
<thead><tr><td>姓名</td><td>所属部门</td><td>年龄</td></tr></thead>
<tr><td class="tdname1"></td><td class="tddept1"></td><td class="tdage1"></td></tr>
<tr><td class="tdname2"></td><td class="tddept2"></td><td class="tdage2"></td></tr>
</table>
</div>
</div>
<dl style="clear:both;"></dl>
<div style="height:20px;margin:0;"><div class="title"></div><span style="color:red">表格替换在第6页</span></div>
<dl style="clear:both;"></dl>
<div><div class="title">图片上传:</div><input style="padding:8px 0;" type="file" id="bookimg1"></div>
<div style="display:none;" id="imgPreview"><img src="" id="uploadImg" width="100" /></div>
<dl style="clear:both;"></dl>
<div style="height:20px;margin:0;"><div class="title"></div><span style="color:red">图片替换在第7页</span></div>
<dl style="clear:both;"></dl>
<div style="text-align:center;">
<input style="
margin: 0 150px;
height: 40px;
background: rgba(2,128,204,1);
border-radius: 2px;
display: block;
width: 80px;
border: none;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: rgba(255,255,255,1);
" type="button" value="替换" onclick="replaceWps()" id="submit">
</div>
</div>
<div>
</body>
<style>
#wpsPanl iframe{width:99%;height:850px;}
#form div{
margin: 10px 0;
height: 40px;
color: rgba(51,51,51,1);
float: left;
}
#form div .input{
height: 35px;
margin: 2px 0;
width: 358px;
border: solid 1px rgba(193,193,193,.35);
}
#form div .forminput {
height: 35px;
margin: 2px 0;
width: 94px;
border: solid 1px rgba(193,193,193,.35);
}
#form div .title{width:120px;text-align: right;}
td {
border: 1px solid rgba(193,193,193,.35);
padding: 8px 12px;
}
tr {
background-color: inherit;
font-size: 14px;
border-top: 1px solid var(--vp-c-divider);
transition: background-color .5s;
}
table {
/*width: 100%;*/
display: table;
border-collapse: collapse;
/*margin: 20px 0;*/
overflow-x: auto;
}
table thead {
font-weight: bold;
}
</style>
</html>
个人总结:需要在线编辑等相关操作,其实也可以用免费的组件组合,比如可以用onlyoffice+Aspose.Words去操作,仅个人见解。