All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.qiniu.util.Auth Maven / Gradle / Ivy

There is a newer version: 7.17.0
Show newest version
package com.qiniu.util;

import com.google.gson.annotations.SerializedName;
import com.qiniu.common.Constants;
import com.qiniu.http.Client;
import com.qiniu.http.Headers;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.*;

public final class Auth {
    public static final String DISABLE_QINIU_TIMESTAMP_SIGNATURE_ENV_KEY = DefaultHeader.DISABLE_TIMESTAMP_SIGNATURE_ENV_KEY;
    public static final String DTOKEN_ACTION_VOD = "linking:vod";
    public static final String DTOKEN_ACTION_STATUS = "linking:status";
    public static final String DTOKEN_ACTION_TUTK = "linking:tutk";
    public static final String HTTP_HEADER_KEY_CONTENT_TYPE = "Content-Type";

    private static final String QBOX_AUTHORIZATION_PREFIX = "QBox ";
    private static final String QINIU_AUTHORIZATION_PREFIX = "Qiniu ";

    /**
     * 上传策略
     * 参考文档:上传策略
     */
    private static final String[] policyFields = new String[]{
            "callbackUrl",
            "callbackBody",
            "callbackHost",
            "callbackBodyType",
            "callbackFetchKey",

            "returnUrl",
            "returnBody",

            "endUser",
            "saveKey",
            "forceSaveKey",
            "insertOnly",
            "isPrefixalScope",

            "detectMime",
            "mimeLimit",
            "fsizeLimit",
            "fsizeMin",
            "trafficLimit",

            "persistentType",
            "persistentOps",
            "persistentNotifyUrl",
            "persistentPipeline",

            "deleteAfterDays",
            "fileType",
    };
    private static final String[] deprecatedPolicyFields = new String[]{
            "asyncOps",
    };
    private static final boolean[] isTokenTable = genTokenTable();
    private static final byte toLower = 'a' - 'A';
    public final String accessKey;
    private final SecretKeySpec secretKey;

    private Auth(String accessKey, SecretKeySpec secretKeySpec) {
        this.accessKey = accessKey;
        this.secretKey = secretKeySpec;
    }

    public static Auth create(String accessKey, String secretKey) {
        if (StringUtils.isNullOrEmpty(accessKey) || StringUtils.isNullOrEmpty(secretKey)) {
            throw new IllegalArgumentException("empty key");
        }
        byte[] sk = StringUtils.utf8Bytes(secretKey);
        SecretKeySpec secretKeySpec = new SecretKeySpec(sk, "HmacSHA1");
        return new Auth(accessKey, secretKeySpec);
    }

    private static void copyPolicy(final StringMap policy, StringMap originPolicy, final boolean strict) {
        if (originPolicy == null) {
            return;
        }
        originPolicy.forEach(new StringMap.Consumer() {
            @Override
            public void accept(String key, Object value) {
                if (StringUtils.inStringArray(key, deprecatedPolicyFields)) {
                    throw new IllegalArgumentException(key + " is deprecated!");
                }
                if (!strict || StringUtils.inStringArray(key, policyFields)) {
                    policy.put(key, value);
                }
            }
        });
    }

    // https://github.com/golang/go/blob/master/src/net/textproto/reader.go#L596
    // CanonicalMIMEHeaderKey returns the canonical format of the
    // MIME header key s. The canonicalization converts the first
    // letter and any letter following a hyphen to upper case;
    // the rest are converted to lowercase. For example, the
    // canonical key for "accept-encoding" is "Accept-Encoding".
    // MIME header keys are assumed to be ASCII only.
    // If s contains a space or invalid header field bytes, it is
    // returned without modifications.
    private static String canonicalMIMEHeaderKey(String name) {
        // com.qiniu.http.Headers 已确保 header name 字符的合法性,直接使用 byte ,否则要使用 char //
        byte[] a = name.getBytes(Constants.UTF_8);
        for (int i = 0; i < a.length; i++) {
            byte c = a[i];
            if (!validHeaderFieldByte(c)) {
                return name;
            }
        }

        boolean upper = true;
        for (int i = 0; i < a.length; i++) {
            byte c = a[i];
            if (upper && 'a' <= c && c <= 'z') {
                c -= toLower;
            } else if (!upper && 'A' <= c && c <= 'Z') {
                c += toLower;
            }
            a[i] = c;
            upper = c == '-'; // for next time
        }
        return new String(a, Constants.UTF_8);
    }

    private static boolean validHeaderFieldByte(byte b) {
        //byte: -128 ~ 127, char:  0 ~ 65535
        return 0 < b && b < isTokenTable.length && isTokenTable[b];
    }

    private static boolean[] genTokenTable() {
        int[] idx = new int[]{
                '!', '#', '$', '%', '&', '\'', '*', '+', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
                'U', 'W', 'V', 'X', 'Y', 'Z', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
                'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~'};
        boolean[] tokenTable = new boolean[127];
        Arrays.fill(tokenTable, false);
        for (int i : idx) {
            tokenTable[i] = true;
        }
        return tokenTable;
    }

    private Mac createMac() {
        Mac mac;
        try {
            mac = javax.crypto.Mac.getInstance("HmacSHA1");
            mac.init(secretKey);
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
            throw new IllegalArgumentException(e);
        }
        return mac;
    }

    @Deprecated // private
    public String sign(byte[] data) {
        Mac mac = createMac();
        String encodedSign = UrlSafeBase64.encodeToString(mac.doFinal(data));
        return this.accessKey + ":" + encodedSign;
    }

    @Deprecated // private
    public String sign(String data) {
        return sign(StringUtils.utf8Bytes(data));
    }

    @Deprecated // private
    public String signWithData(byte[] data) {
        String s = UrlSafeBase64.encodeToString(data);
        return sign(StringUtils.utf8Bytes(s)) + ":" + s;
    }

    @Deprecated // private
    public String signWithData(String data) {
        return signWithData(StringUtils.utf8Bytes(data));
    }

    /**
     * 生成HTTP请求签名字符串
     *
     * @param urlString   签名请求的 url
     * @param body        签名请求的 body
     * @param contentType 签名请求的 contentType
     * @return 签名信息
     */
    @Deprecated // private
    public String signRequest(String urlString, byte[] body, String contentType) {
        URI uri = URI.create(urlString);
        String path = uri.getRawPath();
        String query = uri.getRawQuery();

        Mac mac = createMac();

        mac.update(StringUtils.utf8Bytes(path));

        if (query != null && query.length() != 0) {
            mac.update((byte) ('?'));
            mac.update(StringUtils.utf8Bytes(query));
        }
        mac.update((byte) '\n');
        if (body != null && Client.FormMime.equalsIgnoreCase(contentType)) {
            mac.update(body);
        }

        String digest = UrlSafeBase64.encodeToString(mac.doFinal());

        return this.accessKey + ":" + digest;
    }

    /**
     * 验证回调签名是否正确,此方法仅能验证 QBox 签名以及 GET 请求的 Qiniu 签名
     *
     * @param originAuthorization 待验证签名字符串,以 "QBox " 作为起始字符,GET 请求支持 "Qiniu " 开头。
     * @param url                 回调地址
     * @param body                回调请求体。原始请求体,不要解析后再封装成新的请求体--可能导致签名不一致。
     * @param contentType         回调 ContentType
     * @return 签名是否正确
     */
    @Deprecated
    public boolean isValidCallback(String originAuthorization, String url, byte[] body, String contentType) {
        Headers header = null;
        if (!StringUtils.isNullOrEmpty(contentType)) {
            header = new Headers.Builder()
                    .add(HTTP_HEADER_KEY_CONTENT_TYPE, contentType)
                    .build();
        }
        return isValidCallback(originAuthorization, new Request(url, "GET", header, body));
    }

    /**
     * 验证回调签名是否正确,此方法支持验证 QBox 和 Qiniu 签名
     *
     * @param originAuthorization 待验证签名字符串,以 "QBox " 或 "Qiniu " 作为起始字符
     * @param callback            callback 请求信息
     * @return 签名是否正确
     */
    public boolean isValidCallback(String originAuthorization, Request callback) {
        if (callback == null) {
            return false;
        }

        String authorization = "";
        if (originAuthorization.startsWith(QINIU_AUTHORIZATION_PREFIX)) {
            authorization = QINIU_AUTHORIZATION_PREFIX + signQiniuAuthorization(callback.url, callback.method, callback.body, callback.header);
        } else if (originAuthorization.startsWith(QBOX_AUTHORIZATION_PREFIX)) {
            String contentType = null;
            if (callback.header != null) {
                contentType = callback.header.get(HTTP_HEADER_KEY_CONTENT_TYPE);
            }
            authorization = QBOX_AUTHORIZATION_PREFIX + signRequest(callback.url, callback.body, contentType);
        }
        return authorization.equals(originAuthorization);
    }


    /**
     * 下载签名
     *
     * @param baseUrl 待签名文件url,如 http://img.domain.com/u/3.jpg 、
     *                http://img.domain.com/u/3.jpg?imageView2/1/w/120
     * @return 签名
     */
    public String privateDownloadUrl(String baseUrl) {
        return privateDownloadUrl(baseUrl, 3600);
    }

    /**
     * 下载签名
     *
     * @param baseUrl 待签名文件url,如 http://img.domain.com/u/3.jpg 、
     *                http://img.domain.com/u/3.jpg?imageView2/1/w/120
     * @param expires 有效时长,单位秒。默认3600s
     * @return 签名
     */
    public String privateDownloadUrl(String baseUrl, long expires) {
        long deadline = System.currentTimeMillis() / 1000 + expires;
        return privateDownloadUrlWithDeadline(baseUrl, deadline);
    }

    public String privateDownloadUrlWithDeadline(String baseUrl, long deadline) {
        StringBuilder b = new StringBuilder();
        b.append(baseUrl);
        int pos = baseUrl.indexOf("?");
        if (pos > 0) {
            b.append("&e=");
        } else {
            b.append("?e=");
        }
        b.append(deadline);
        String token = sign(StringUtils.utf8Bytes(b.toString()));
        b.append("&token=");
        b.append(token);
        return b.toString();
    }

    /**
     * scope = bucket
     * 一般情况下可通过此方法获取token
     *
     * @param bucket 空间名
     * @return 生成的上传token
     */
    public String uploadToken(String bucket) {
        return uploadToken(bucket, null, 3600, null, true);
    }

    /**
     * scope = bucket:key
     * 同名文件覆盖操作、只能上传指定key的文件可以可通过此方法获取token
     *
     * @param bucket 空间名
     * @param key    key,可为 null
     * @return 生成的上传token
     */
    public String uploadToken(String bucket, String key) {
        return uploadToken(bucket, key, 3600, null, true);
    }

    /**
     * 生成上传token
     *
     * @param bucket  空间名
     * @param key     key,可为 null
     * @param expires 有效时长,单位秒
     * @param policy  上传策略的其它参数,如 new StringMap().put("endUser", "uid").putNotEmpty("returnBody", "")。
     *                scope通过 bucket、key间接设置,deadline 通过 expires 间接设置
     *                具体参考:  上传策略 
     * @return 生成的上传token
     */
    public String uploadToken(String bucket, String key, long expires, StringMap policy) {
        return uploadToken(bucket, key, expires, policy, true);
    }

    /**
     * 生成上传token
     *
     * @param bucket  空间名
     * @param key     key,可为 null
     * @param expires 有效时长,单位秒。默认3600s
     * @param policy  上传策略的其它参数,如 new StringMap().put("endUser", "uid").putNotEmpty("returnBody", "")。
     *                scope通过 bucket、key间接设置,deadline 通过 expires 间接设置
     *                具体参考:  上传策略 
     * @param strict  是否去除非限定的策略字段,默认true
     * @return 生成的上传token
     */
    public String uploadToken(String bucket, String key, long expires, StringMap policy, boolean strict) {
        long deadline = System.currentTimeMillis() / 1000 + expires;
        return uploadTokenWithDeadline(bucket, key, deadline, policy, strict);
    }

    public String uploadTokenWithDeadline(String bucket, String key, long deadline, StringMap policy, boolean strict) {
        // TODO   UpHosts Global
        String scope = bucket;
        if (key != null) {
            scope = bucket + ":" + key;
        }
        StringMap x = new StringMap();
        copyPolicy(x, policy, strict);
        x.put("scope", scope);
        x.put("deadline", deadline);

        String s = Json.encode(x);
        return signWithData(StringUtils.utf8Bytes(s));
    }

    public String uploadTokenWithPolicy(Object obj) {
        String s = Json.encode(obj);
        return signWithData(StringUtils.utf8Bytes(s));
    }

    @Deprecated
    public StringMap authorization(String url, byte[] body, String contentType) {
        String authorization = "QBox " + signRequest(url, body, contentType);
        return new StringMap().put("Authorization", authorization);
    }

    @Deprecated
    public StringMap authorization(String url) {
        return authorization(url, null, null);
    }

    /**
     * 生成 HTTP 请求签名字符串
     *
     * @param url         签名请求的 url
     * @param method      签名请求的 method
     * @param body        签名请求的 body
     * @param contentType 签名请求的 contentType
     * @return 签名
     */
    @Deprecated
    public String signRequestV2(String url, String method, byte[] body, String contentType) {
        return signQiniuAuthorization(url, method, body, contentType);
    }

    public String signQiniuAuthorization(String url, String method, byte[] body, String contentType) {
        Headers headers = null;
        if (!StringUtils.isNullOrEmpty(contentType)) {
            headers = new Headers.Builder().set(HTTP_HEADER_KEY_CONTENT_TYPE, contentType).build();
        }
        return signQiniuAuthorization(url, method, body, headers);
    }

    public String signQiniuAuthorization(String url, String method, byte[] body, Headers headers) {
        URI uri = URI.create(url);
        if (StringUtils.isNullOrEmpty(method)) {
            method = "GET";
        }

        StringBuilder sb = new StringBuilder();
        sb.append(method).append(" ").append(uri.getPath());

        if (uri.getQuery() != null) {
            sb.append("?").append(uri.getRawQuery());
        }

        sb.append("\nHost: ").append(uri.getHost() != null ? uri.getHost() : "");

        if (uri.getPort() > 0) {
            sb.append(":").append(uri.getPort());
        }

        String contentType = null;

        if (null != headers) {
            contentType = headers.get(HTTP_HEADER_KEY_CONTENT_TYPE);
            if (contentType != null) {
                sb.append("\n").append(HTTP_HEADER_KEY_CONTENT_TYPE).append(": ").append(contentType);
            }

            List
xQiniuheaders = genXQiniuSignHeader(headers); java.util.Collections.sort(xQiniuheaders); if (xQiniuheaders.size() > 0) { for (Header h : xQiniuheaders) { sb.append("\n").append(h.name).append(": ").append(h.value); } } } sb.append("\n\n"); if (body != null && body.length > 0 && null != contentType && !"".equals(contentType) && !"application/octet-stream".equals(contentType)) { sb.append(new String(body, Constants.UTF_8)); } Mac mac = createMac(); mac.update(StringUtils.utf8Bytes(sb.toString())); String digest = UrlSafeBase64.encodeToString(mac.doFinal()); return this.accessKey + ":" + digest; } private List
genXQiniuSignHeader(Headers headers) { ArrayList
hs = new ArrayList
(); for (String name : headers.names()) { if (name.length() > "X-Qiniu-".length() && name.startsWith("X-Qiniu-")) { String value = headers.get(name); if (value != null) { hs.add(new Header(canonicalMIMEHeaderKey(name), value)); } } } return hs; } public Headers qiniuAuthorization(String url, String method, byte[] body, Headers headers) { Headers.Builder builder = null; if (headers == null) { builder = new Headers.Builder(); } else { builder = headers.newBuilder(); } final Headers.Builder finalBuilder = builder; DefaultHeader.setDefaultHeader(new DefaultHeader.HeadAdder() { @Override public void addHeader(String key, String value) { finalBuilder.set(key, value); } }); String authorization = "Qiniu " + signQiniuAuthorization(url, method, body, finalBuilder.build()); builder.set("Authorization", authorization).build(); return builder.build(); } @Deprecated public StringMap authorizationV2(String url, String method, byte[] body, String contentType) { Headers headers = null; if (!StringUtils.isNullOrEmpty(contentType)) { headers = new Headers.Builder().set(HTTP_HEADER_KEY_CONTENT_TYPE, contentType).build(); } headers = qiniuAuthorization(url, method, body, headers); if (headers == null) { return null; } Set headerKeys = headers.names(); StringMap headerMap = new StringMap(); for (String key : headerKeys) { String value = headers.get(key); if (value != null) { headerMap.put(key, value); } } return headerMap; } @Deprecated public StringMap authorizationV2(String url) { return authorizationV2(url, "GET", null, (String) null); } //连麦 RoomToken public String signRoomToken(String roomAccess) throws Exception { String encodedRoomAcc = UrlSafeBase64.encodeToString(roomAccess); byte[] sign = createMac().doFinal(encodedRoomAcc.getBytes()); String encodedSign = UrlSafeBase64.encodeToString(sign); return this.accessKey + ":" + encodedSign + ":" + encodedRoomAcc; } public String generateLinkingDeviceToken(String appid, String deviceName, long deadline, String[] actions) { LinkingDtokenStatement[] staments = new LinkingDtokenStatement[actions.length]; for (int i = 0; i < actions.length; ++i) { staments[i] = new LinkingDtokenStatement(actions[i]); } SecureRandom random = new SecureRandom(); StringMap map = new StringMap(); map.put("appid", appid).put("device", deviceName).put("deadline", deadline) .put("random", random.nextInt()).put("statement", staments); String s = Json.encode(map); return signWithData(StringUtils.utf8Bytes(s)); } public String generateLinkingDeviceTokenWithExpires(String appid, String deviceName, long expires, String[] actions) { long deadline = (System.currentTimeMillis() / 1000) + expires; return generateLinkingDeviceToken(appid, deviceName, deadline, actions); } public String generateLinkingDeviceVodTokenWithExpires(String appid, String deviceName, long expires) { return generateLinkingDeviceTokenWithExpires(appid, deviceName, expires, new String[]{DTOKEN_ACTION_VOD}); } public String generateLinkingDeviceStatusTokenWithExpires(String appid, String deviceName, long expires) { return generateLinkingDeviceTokenWithExpires(appid, deviceName, expires, new String[]{DTOKEN_ACTION_STATUS}); } class LinkingDtokenStatement { @SerializedName("action") private String action; LinkingDtokenStatement(String action) { this.action = action; } public String getAction() { return action; } public void setAction(String action) { this.action = action; } } private class Header implements Comparable
{ String name; String value; Header(String name, String value) { this.name = name; this.value = value; } @Override public int compareTo(Header o) { int c = this.name.compareTo(o.name); if (c == 0) { return this.value.compareTo(o.value); } else { return c; } } } public static class Request { private final String url; private final String method; private final Headers header; private final byte[] body; /** * @param url 回调地址 * @param method 回调请求方式 * @param header 回调头,注意 Content-Type Key 字段需为:{@link Auth#HTTP_HEADER_KEY_CONTENT_TYPE},大小写敏感 * @param body 回调请求体。原始请求体,不要解析后再封装成新的请求体--可能导致签名不一致。 **/ public Request(String url, String method, Headers header, byte[] body) { this.url = url; this.method = method; this.header = header; this.body = body; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy