前言
很久没写前端代码,这次写了个简单的证书管理平台,后端用 Spring Boot
,前端用 Thymeleaf
,其中证书生成接口涉及到下载功能,下载的证书竟然在客户端无法使用,看了一下文件大小,与用 Postman
请求和浏览器 url
直接请求得到的大小不一致,那就只能面向搜索引擎一波了。
最后虽然解决了问题,但是为什么会有这种情况产生,由于学艺不精暂时不知道为什么,也没搜出个所以然来,这里先做个记录,有懂得小伙伴欢迎留言探讨,如果哪里写的不对的还请指出。
后端接口
Controller 层
@GetMapping("generate")
public void generateLicense(@RequestParam Integer id, HttpServletResponse response) throws Exception {
LicenseParam licenseParam = licenseService.getLicenseInfo(id).getData();
if (ObjectUtils.isEmpty(licenseParam)) {
throw new Exception("证书生成失败! 未找到证书信息!");
}
long expiryTime = licenseParam.getExpiryTime().getTime();
if (expiryTime < System.currentTimeMillis()) {
throw new Exception("证书生成失败! 证书失效时间不应晚于当前时间!");
}
if ("user".equalsIgnoreCase(licenseParam.getConsumerType()) && 1 != licenseParam.getConsumerAmount()) {
throw new Exception("证书生成失败! 使用者类型为“用户”时, 使用者数量只能为 1 !");
}
licenseService.generateLicense(id, response);
}
Service 层
/**
* 生成证书
*
* @param id 编号
* @param response response
*/
public void generateLicense(Integer id, HttpServletResponse response) {
LicenseParam licenseParam = licenseParamMapper.selectLicenseParamById(id);
LicenseCreatorParam creatorParam = ConvertUtils.sourceToTarget(licenseParam, LicenseCreatorParam.class);
if (ObjectUtils.isNotEmpty(licenseParam.getLicenseModel())) {
LicenseCheckModel checkModel = ConvertUtils.sourceToTarget(licenseParam.getLicenseModel(), LicenseCheckModel.class);
creatorParam.setLicenseCheckModel(checkModel);
}
creatorParam.setPrivateAlias(privateAlias);
creatorParam.setKeyPass(keyPass);
creatorParam.setStorePass(storePass);
creatorParam.setPrivateKeysStorePath("privateKeys.keystore");
try (OutputStream outputStream = response.getOutputStream()
) {
LicenseCreator licenseCreator = new LicenseCreator(creatorParam);
byte[] licenseBytes = licenseCreator.createLicense();
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"license.lic\"");
response.setHeader("Accept-Ranges", "bytes");
outputStream.write(licenseBytes);
outputStream.flush();
} catch (IOException e) {
log.error("generate error : ", e);
}
}
后端代码没有什么可说的,可用 POST 也可用 GET ,这里我用的是 GET,并且我需要返回一些校验的信息给前端。
前端代码
原有写法
这里使用了比较熟悉的 jQuery Ajax,并且标记 responseType: 'blob'
,同时在 complete
事件中使用 XMLHttpRequest.responseJSON.message
展示后端在 code 为 500 的时候返回的错误信息。
function generate(id) {
$.ajax({
type: "get",
url: prefix + "/generate",
responseType: 'blob',
data: {
"id": id
},
success: function(data) {
if (!data) {
$.modal.alertError("证书生成失败!");
return;
}
let url = window.URL.createObjectURL(new Blob([data], {type:'application/octet-stream'}))
let link = document.createElement('a')
link.style.display = 'none'
link.href = url
link.setAttribute('download', 'license.lic')
document.body.appendChild(link)
link.click()
// 释放URL对象所占资源
window.URL.revokeObjectURL(url)
// 用完即删
document.body.removeChild(link)
},
complete: function(XMLHttpRequest, textStatus) {
if (textStatus == 'timeout') {
$.modal.alertWarning("服务器超时,请稍后再试!");
$.modal.enable();
$.modal.closeLoading();
} else if (textStatus == "parsererror" || textStatus == "error") {
$.modal.alertWarning(XMLHttpRequest.responseJSON.message != '' ? XMLHttpRequest.responseJSON.message : "服务器错误,请联系管理员!");
$.modal.enable();
$.modal.closeLoading();
}
}
});
}
修正后写法
改为原生的 XMLHttpRequest,在 onreadystatechange
事件中处理结果,并展示后端在 code 为 500 的时候返回的错误信息。
function generate(id) {
let url = prefix + "/generate?id=" + id;
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = "blob";
// 定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
xhr.onload = function () {
// 请求完成
if (this.status === 200) {
// 返回200
let blob = this.response;
let reader = new FileReader();
reader.readAsDataURL(blob);
reader.onload = function (e) {
// 转换完成,创建一个a标签用于下载
let link = document.createElement('a');
link.download = 'license.lic';
link.href = e.target.result;
$("body").append(link); // 修复firefox中无法触发click
link.click();
$(link).remove();
}
}
};
// 发送ajax请求
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 500) {
let reader = new FileReader()
reader.onload = function () {
let jsonData = JSON.parse(reader.result);
let message = jsonData.message
$.modal.alertWarning(_isBlank(message) ? "服务器错误,请联系管理员!" : message);
$.modal.enable();
$.modal.closeLoading();
};
reader.readAsText(this.response);
}
};
}
解决过程
比对结果
页面调试,先打印数据看看,这样更加直观
直接打印结果集,是一堆乱码,这里使用 new Blob 方法打印
可以看到大小为 1563,比接口直接请求得到的数据流大了快一倍,这样生成的证书肯定是不能使用的
改用原生方式请求,并打印结果集
可以看到大小为 864,与接口直接请求得到的数据流一致!
处理后端返回的错误信息
之前用 jQuery Ajax 时在 complete
事件中使用 XMLHttpRequest.responseJSON.message
就可以直接展示了,但是用原生请求的时候,XMLHttpRequest
只有 responseText
属性,并没有 responseJSON
之类的属性
在控制台打印 response 属性,为啥打印这个,只是因为返回值写着 any
…
可以看到 type 是 application/json
,不过这也写着是 Blob 类型,那就用上面的方法写一下,打印出来没啥问题
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 500) {
let reader = new FileReader()
reader.onload = function () {
let jsonData = JSON.parse(reader.result);
let message = jsonData.message
$.modal.alertWarning(_isBlank(message) ? "服务器错误,请联系管理员!" : message);
$.modal.enable();
$.modal.closeLoading();
};
reader.readAsText(this.response);
}
};
评论区