本文参加又拍云原创技术征文活动https://www.upyun.com/tech/article/551/1.html
公司此前一直使用七牛云,最近为了向海外用户提供更优质的网络访问服务,开始入手又拍云,官方在客户端新js框架(react/vue/angular)下提供的参考文档和代码都不够完整,摸索了好久,最终在官方的在线支持下,实现了客户端js(目前仅是react)配合Java服务端实现文件上传的功能。
官方支持node.js处理上传的代码在github上有一个项目:https://www.npmjs.com/package/upyun,其中有两行“精炼”的代码:
const service = new upyun.Service('your service name')
const client = new upyun.Client(service, getSignHeader);
实际上,‘your service name'就是你的桶名;可能在又拍云某段时间的概念里,对象存储的“桶”称为服务(service)。那么这个是你的运维人员在又拍云控制台里配置的,而getSignHeader是对应服务端接口的Promise函数,官方举了一个node.js服务端实现的例子,其返回值又说得不明不白,在此处摸索了好一些时间,最终是在客服不间断的支持下解决的……汗!具体请参考我的代码:
……
import upyun from 'upyun';
……
const bucketName = "<YOUR-BUCKET-NAME>";
const service = new upyun.Bucket(bucketName);
const client = new upyun.Client(service, getUpyunUploadHeader);
……
// 返回 Promise
export async function getUpyunUploadHeader(bucket, method = "POST", path) {
console.log('upyun bucket', bucket, method, path)
return request(`${apiDomain}/upyun/sign/head?bucket=${bucket.bucketName}&method=${method}&path=${path}`);
}
注意:这个返回Promise的函数的入参,是由upyun包控制的:它给第1个参数bucket传入一个对象,第2个参数传入的值是PUT,第三个参数是文件上传的目标路径,如/demo/file1.pdf。
request函数可以在antd pro脚手架示例项目代码里找到,文末附。
客户端js部分主要就是这样了,client.putFile(……)相关的代码,比较简单,根据官方例子写就可以了。
服务端接口/upyun/sign/head返回什么样的数据呢?官方也没有说,事实上应该返回一个json对象,里面至少需要包含Authoriztion这个值,即最后生成的签名描述符(形式为UPYUN <用户名>:<签名>)——事实上还需要一个日期字符串,详见下文。为快捷起见,我就直接用了Map,代码如下:
// 生成upyun js sdk需要的上传参数
public Map<String, String> uploadHeader(String bucket, String uri, String method) throws Exception {
logger.debug("UPYUN uploadHeader: bucket={}, uri/path={}, method={}", bucket, uri, method);
String key = username;
String secret = md5(password);
String date = getRfc1123Time();
// 上传,处理,内容识别有存储
String s = sign(key, secret, method, "/" + bucket + uri, date, "", "");
logger.debug("Generated {}", s);
Map<String, String> map = new HashMap<>();
map.put("x-date", date);
map.put("Authorization", s);
return map;
}
其中md5、getRfc1123Time、sign这三个方法,官方API中Java部分有,直接照搬即可。要注意的是,参与签名的path,是要拼上桶名的("/" + bucket + uri),最终生成的资源URL中也是带桶名的(域名+桶名+资源路径),这一点与七牛云不一样。这个getRfc1123Time也有一些绕,某段官方文档里是说,参与签名计算的时间用于表示请求的有效期限,最好是半年……所以一开始调测失败,我把这个时间用当前时间加上了一个月,后来又发现只需要用当前时间戳就可以……无语。
服务端接口返回的这两个数据(x-date与authorization),将直接被客户端加入请求头中。使用x-date是因为date会被浏览器屏蔽(认为是个危险的头),而x-date的值是参与了签名计算,所以必须提供给客户端用于请求(putFile)。
附一:md5、getRfc1123Time、sign三个方法的Java实现:
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
private static String md5(String string) {
byte[] hash;
try {
hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 is unsupported", e);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MessageDigest不支持MD5Util", e);
}
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash) {
if ((b & 0xFF) < 0x10) hex.append("0");
hex.append(Integer.toHexString(b & 0xFF));
}
return hex.toString();
}
private static byte[] hashHmac(String data, String key)
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
return mac.doFinal(data.getBytes());
}
private static String sign(String key, String secret, String method, String uri, String date, String policy,
String md5) throws Exception {
String value = method + "&" + uri + "&" + date;
if (policy != null && policy.length() > 0) {
value = value + "&" + policy;
}
if (md5 != null && md5.length() > 0) {
value = value + "&" + md5;
}
byte[] hmac = hashHmac(value, secret);
String sign = Base64.getEncoder().encodeToString(hmac);
return "UPYUN " + key + ":" + sign;
}
private static String getRfc1123Time() {
Calendar calendar = Calendar.getInstance();
SimpleDateFormat dateFormat = new SimpleDateFormat(
"EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
logger.debug("upyun token time (format) {}", calendar);
return dateFormat.format(calendar.getTime());
}
附二:客户端request函数:
/**
* Requests a URL, returning a promise.
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
* @return {object} An object containing either "data" or "err"
*/
export default function request(url, options, isLogin = false) {
const defaultOptions = {
// credentials: 'include',
};
const newOptions = { ...defaultOptions, ...options };
if (
newOptions.method === 'POST' ||
newOptions.method === 'PUT' ||
newOptions.method === 'DELETE'
) {
if (!(newOptions.data instanceof FormData)) {
newOptions.headers = {
Accept: 'application/json',
'Content-Type': 'application/json; charset=utf-8',
...newOptions.headers,
};
newOptions.body = JSON.stringify(newOptions.data);
} else {
// newOptions.body is FormData
newOptions.headers = {
Accept: 'application/json',
...newOptions.headers,
};
newOptions.body = newOptions.data;
}
}
if (!isLogin) {
// 请求的时候,如果storage有token,则携带token访问
const session = getSession();
if (session) {
newOptions.headers = {
'Access-Token': session.data.accessToken,
...newOptions.headers,
}
}
}
return fetch(url, newOptions)
.then(checkStatus)
.then(response => {
// 处理图片、PDF的情况
const contentType = response.headers.get('Content-Type');
if (contentType.indexOf('image') > -1 || contentType.indexOf('pdf') > -1) {
return response.blob();
}
if (newOptions.method === 'DELETE' || response.status === 204) {
return response.text();
}
// 返回的时候,如果服务端返回令牌过期,则清除令牌并返回转到登录页面
// code=20180/20181
const p = Promise.resolve(response.json());
p.then( r => {
const { code } = r;
if (code !== undefined && code !== 1) {
// message.error(msg);
if (code === 20180 || code === 20181) {
clearSession();
const { dispatch } = store;
dispatch(routerRedux.push('/user/login'));
}
}
});
return p;
})
.catch(e => {
const { dispatch } = store;
const status = e.name;
if (status === 401) {
dispatch({
type: 'login/logout',
});
return;
}
if (status === 403) {
dispatch(routerRedux.push('/exception/403'));
return;
}
if (status <= 504 && status >= 500) {
dispatch(routerRedux.push('/exception/500'));
return;
}
if (status >= 404 && status < 422) {
dispatch(routerRedux.push('/exception/404'));
}
});
}
网友评论