Commit e77dd4d2 by 罗长华

权限控制 sdk客户端加密逻辑完成

parent 8fbabb83
...@@ -46,6 +46,12 @@ ...@@ -46,6 +46,12 @@
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>
<version>5.7.22</version> <version>5.7.22</version>
</dependency> </dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
......
package com.wecloud.im.sdk;
public interface ClientErrorCode {
/**
* Unknown error. This means the error is not expected.
*/
static final String UNKNOWN = "Unknown";
/**
* Unknown host. This error is returned when a
* {@link java.net.UnknownHostException} is thrown.
*/
static final String UNKNOWN_HOST = "UnknownHost";
/**
* connection times out.
*/
static final String CONNECTION_TIMEOUT = "ConnectionTimeout";
/**
* Socket times out
*/
static final String SOCKET_TIMEOUT = "SocketTimeout";
/**
* Socket exception
*/
static final String SOCKET_EXCEPTION = "SocketException";
/**
* Connection is refused by server side.
*/
static final String CONNECTION_REFUSED = "ConnectionRefused";
/**
* The input stream is not repeatable for reading.
*/
static final String NONREPEATABLE_REQUEST = "NonRepeatableRequest";
/**
* Thread interrupted while reading the input stream.
*/
static final String INPUTSTREAM_READING_ABORTED = "InputStreamReadingAborted";
/**
* Ssl exception
*/
static final String SSL_EXCEPTION = "SslException";
}
package com.wecloud.im.sdk;
/**
* <p>
* This exception is the one thrown by the client side when accessing OSS.
* </p>
*
* <p>
* {@link ClientException} is the class to represent any exception in OSS client
* side. Generally ClientException occurs either before sending the request or
* after receving the response from OSS server side. For example, if the network
* is broken when it tries to send a request, then the SDK will throw a
* {@link ClientException} instance.
* </p>
*
* <p>
* {@link ServiceException} is converted from error code from OSS response. For
* example, when OSS tries to authenticate a request, if Access ID does not
* exist, the SDK will throw a {@link ServiceException} or its subclass instance
* with the specific error code, which the caller could handle that with
* specific logic.
* </p>
*
*/
public class ClientException extends RuntimeException {
private static final long serialVersionUID = 1870835486798448798L;
private String errorMessage;
private String requestId;
private String errorCode;
/**
* Creates a default instance.
*/
public ClientException() {
super();
}
/**
* Creates an instance with error message.
*
* @param errorMessage
* Error message.
*/
public ClientException(String errorMessage) {
this(errorMessage, null);
}
/**
* Creates an instance with an exception
*
* @param cause
* An exception.
*/
public ClientException(Throwable cause) {
this(null, cause);
}
/**
* Creates an instance with error message and an exception.
*
* @param errorMessage
* Error message.
* @param cause
* An exception.
*/
public ClientException(String errorMessage, Throwable cause) {
super(null, cause);
this.errorMessage = errorMessage;
this.errorCode = ClientErrorCode.UNKNOWN;
this.requestId = "Unknown";
}
/**
* Creates an instance with error message, error code, request Id
*
* @param errorMessage
* Error message.
* @param errorCode
* Error code, which typically is from a set of predefined
* errors. The handler code could do action based on this.
* @param requestId
* Request Id.
*/
public ClientException(String errorMessage, String errorCode, String requestId) {
this(errorMessage, errorCode, requestId, null);
}
/**
* Creates an instance with error message, error code, request Id and an
* exception.
*
* @param errorMessage
* Error message.
* @param errorCode
* Error code.
* @param requestId
* Request Id.
* @param cause
* An exception.
*/
public ClientException(String errorMessage, String errorCode, String requestId, Throwable cause) {
this(errorMessage, cause);
this.errorCode = errorCode;
this.requestId = requestId;
}
/**
* Get error message.
*
* @return Error message in string.
*/
public String getErrorMessage() {
return errorMessage;
}
/**
* Get error code.
*
* @return Error code.
*/
public String getErrorCode() {
return errorCode;
}
/**
* Gets request id.
*
* @return The request Id.
*/
public String getRequestId() {
return requestId;
}
@Override
public String getMessage() {
return getErrorMessage() + "\n[ErrorCode]: " + (errorCode != null ? errorCode
: "") + "\n[RequestId]: " + (requestId != null ? requestId : "");
}
}
package com.wecloud.im.sdk.common.auth;
import lombok.Getter;
/**
* 证书
* @Author luozh
* @Date 2022年04月14日 11:17
* @Version 1.0
*/
@Getter
public class Credentials {
private String appKey;
private String appSecret;
public Credentials(String appKey, String appSecret) {
if (appKey == null || "".equals(appKey)) {
throw new InvalidCredentialsException("App key id should not be null or empty.");
}
if (appSecret == null || "".equals(appSecret)) {
throw new InvalidCredentialsException("Secret access key should not be null or empty.");
}
this.appKey = appKey;
this.appSecret = appSecret;
}
}
package com.wecloud.im.sdk.common.auth;
/**
*
* @Author luozh
* @Date 2022年04月14日 14:55
* @Version 1.0
*/
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.wecloud.im.sdk.utils.BinaryUtil;
/**
* Used for computing Hmac-SHA256 signature.
*/
public class HmacSHA256Signature {
/* The default encoding. */
private static final String DEFAULT_ENCODING = "UTF-8";
/* Signature method. */
private static final String ALGORITHM = "HmacSHA256";
/* Signature version. */
private static final String VERSION = "1";
private static final Object LOCK = new Object();
/* Prototype of the Mac instance. */
private static Mac macInstance;
public String getAlgorithm() {
return ALGORITHM;
}
public String getVersion() {
return VERSION;
}
public String computeSignature(String key, String data) {
try {
byte[] signData = sign(key.getBytes(DEFAULT_ENCODING), data.getBytes(DEFAULT_ENCODING), macInstance,
LOCK, ALGORITHM);
return BinaryUtil.toBase64String(signData);
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException("Unsupported algorithm: " + DEFAULT_ENCODING, ex);
}
}
protected byte[] sign(byte[] key, byte[] data, Mac macInstance, Object lock, String algorithm) {
try {
// Because Mac.getInstance(String) calls a synchronized method, it
// could block on
// invoked concurrently, so use prototype pattern to improve perf.
if (macInstance == null) {
synchronized (lock) {
if (macInstance == null) {
macInstance = Mac.getInstance(algorithm);
}
}
}
Mac mac;
try {
mac = (Mac) macInstance.clone();
} catch (CloneNotSupportedException e) {
// If it is not clonable, create a new one.
mac = Mac.getInstance(algorithm);
}
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data);
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException("Unsupported algorithm: " + algorithm, ex);
} catch (InvalidKeyException ex) {
throw new RuntimeException("Invalid key: " + key, ex);
}
}
}
package com.wecloud.im.sdk.common.auth;
public class InvalidCredentialsException extends RuntimeException {
private static final long serialVersionUID = 1L;
public InvalidCredentialsException() {
super();
}
public InvalidCredentialsException(String message) {
super(message);
}
public InvalidCredentialsException(Throwable cause) {
super(cause);
}
public InvalidCredentialsException(String message, Throwable cause) {
super(message, cause);
}
}
...@@ -8,7 +8,9 @@ import com.wecloud.im.sdk.utils.HttpHeaders; ...@@ -8,7 +8,9 @@ import com.wecloud.im.sdk.utils.HttpHeaders;
* @Date 2022年04月13日 14:39 * @Date 2022年04月13日 14:39
* @Version 1.0 * @Version 1.0
*/ */
public interface WecloudHeaders extends HttpHeaders { public interface ImHeaders extends HttpHeaders {
static final String WECLOUD_SIGN = "x-wecloud-sign"; static final String WECLOUD_SIGN = "x-wecloud-sign";
static final String IM_PREFIX = "x-im-";
} }
package com.wecloud.im.sdk.internal; package com.wecloud.im.sdk.internal;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
...@@ -55,6 +54,11 @@ public class OSSRequestMessageBuilder { ...@@ -55,6 +54,11 @@ public class OSSRequestMessageBuilder {
return this; return this;
} }
public OSSRequestMessageBuilder addHeaders(Map<String, String> headers) {
this.headers.putAll(headers);
return this;
}
public OSSRequestMessageBuilder addHeader(String key, String value) { public OSSRequestMessageBuilder addHeader(String key, String value) {
headers.put(key, value); headers.put(key, value);
return this; return this;
...@@ -86,9 +90,6 @@ public class OSSRequestMessageBuilder { ...@@ -86,9 +90,6 @@ public class OSSRequestMessageBuilder {
public RequestMessage build() { public RequestMessage build() {
Map<String, String> sentHeaders = new HashMap<String, String>(this.headers); Map<String, String> sentHeaders = new HashMap<String, String>(this.headers);
Map<String, String> sentParameters = new LinkedHashMap<String, String>(this.parameters); Map<String, String> sentParameters = new LinkedHashMap<String, String>(this.parameters);
Date now = new Date();
sentHeaders.put(WecloudHeaders.DATE, System.currentTimeMillis() + "");
RequestMessage request = new RequestMessage(this.originalRequest); RequestMessage request = new RequestMessage(this.originalRequest);
request.setEndpoint(endpoint); request.setEndpoint(endpoint);
request.setHeaders(sentHeaders); request.setHeaders(sentHeaders);
......
package com.wecloud.im.sdk.internal;
import com.wecloud.im.sdk.ClientException;
import com.wecloud.im.sdk.common.RequestMessage;
import com.wecloud.im.sdk.common.auth.Credentials;
/**
* 请求签名者
* @Author luozh
* @Date 2022年04月14日 11:25
* @Version 1.0
*/
public class RequestSigner {
private String httpMethod;
/* Note that resource path should not have been url-encoded. */
private String resourcePath;
private Credentials creds;
public RequestSigner(String httpMethod, String resourcePath, Credentials creds) {
this.httpMethod = httpMethod;
this.resourcePath = resourcePath;
this.creds = creds;
}
public void sign(RequestMessage request) throws ClientException {
String accessKeyId = creds.getAppKey();
String secretAccessKey = creds.getAppSecret();
if (accessKeyId.length() > 0 && secretAccessKey.length() > 0) {
String signature;
signature = SignUtils.buildSignature(secretAccessKey, httpMethod, resourcePath, request);
request.addHeader(ImHeaders.AUTHORIZATION, SignUtils.composeRequestAuthorization(accessKeyId, signature));
}
}
}
package com.wecloud.im.sdk.internal;
/**
* 签名参数
* @Author luozh
* @Date 2022年04月14日 11:33
* @Version 1.0
*/
public class SignParameters {
public static final String AUTHORIZATION_PREFIX = "WECLOUD-IM ";
public static final String AUTHORIZATION_ACCESS_KEY_ID = "AppKey";
public static final String AUTHORIZATION_ADDITIONAL_HEADERS = "AdditionalHeaders";
public static final String AUTHORIZATION_SIGNATURE = "Signature";
public static final String NEW_LINE = "\n";
}
package com.wecloud.im.sdk.internal;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.TreeMap;
import cn.hutool.core.lang.Assert;
import com.wecloud.im.sdk.ClientException;
import com.wecloud.im.sdk.common.RequestMessage;
import com.wecloud.im.sdk.common.auth.HmacSHA256Signature;
import com.wecloud.im.sdk.utils.HttpHeaders;
import static com.wecloud.im.sdk.internal.ImHeaders.IM_PREFIX;
import static com.wecloud.im.sdk.internal.SignParameters.AUTHORIZATION_ACCESS_KEY_ID;
import static com.wecloud.im.sdk.internal.SignParameters.AUTHORIZATION_PREFIX;
import static com.wecloud.im.sdk.internal.SignParameters.AUTHORIZATION_SIGNATURE;
/**
* 签名工具类
* @Author luozh
* @Date 2022年04月14日 11:32
* @Version 1.0
*/
public class SignUtils {
public static String composeRequestAuthorization(String accessKeyId, String signature) {
StringBuilder sb = new StringBuilder();
sb.append(AUTHORIZATION_PREFIX + AUTHORIZATION_ACCESS_KEY_ID).append(":").append(accessKeyId).append(", ");
sb.append(AUTHORIZATION_SIGNATURE).append(":").append(signature);
return sb.toString();
}
public static String buildCanonicalString(String method, String resourcePath, RequestMessage request) {
StringBuilder canonicalString = new StringBuilder();
canonicalString.append(method).append(SignParameters.NEW_LINE);
Map<String, String> headers = request.getHeaders();
TreeMap<String, String> fixedHeadersToSign = new TreeMap<>();
TreeMap<String, String> canonicalizedImHeadersToSign = new TreeMap<>();
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
if (header.getKey() != null) {
String lowerKey = header.getKey().toLowerCase();
if (lowerKey.equals(HttpHeaders.CONTENT_TYPE.toLowerCase())
|| lowerKey.equals(HttpHeaders.DATE.toLowerCase())) {
fixedHeadersToSign.put(lowerKey, header.getValue().trim());
} else if (lowerKey.startsWith(IM_PREFIX)) {
canonicalizedImHeadersToSign.put(lowerKey, header.getValue().trim());
}
}
}
}
if (!fixedHeadersToSign.containsKey(HttpHeaders.CONTENT_TYPE.toLowerCase())) {
fixedHeadersToSign.put(HttpHeaders.CONTENT_TYPE.toLowerCase(), "");
}
// Append fixed headers to sign to canonical string
for (Map.Entry<String, String> entry : fixedHeadersToSign.entrySet()) {
Object value = entry.getValue();
canonicalString.append(value);
canonicalString.append(SignParameters.NEW_LINE);
}
// Append canonicalized im headers to sign to canonical string
for (Map.Entry<String, String> entry : canonicalizedImHeadersToSign.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
canonicalString.append(key).append(':').append(value).append(SignParameters.NEW_LINE);
}
// Append canonical resource to canonical string
canonicalString.append(buildCanonicalizedResource(resourcePath, request.getParameters()));
return canonicalString.toString();
}
private static String buildCanonicalizedResource(String resourcePath, Map<String, String> parameters) {
Assert.isTrue(resourcePath.startsWith("/"), "Resource path should start with slash character");
StringBuilder builder = new StringBuilder();
builder.append(uriEncoding(resourcePath));
if (parameters != null) {
TreeMap<String, String> canonicalizedParams = new TreeMap<String, String>();
for (Map.Entry<String, String> param : parameters.entrySet()) {
if (param.getValue() != null) {
canonicalizedParams.put(uriEncoding(param.getKey()), uriEncoding(param.getValue()));
} else {
canonicalizedParams.put(uriEncoding(param.getKey()), null);
}
}
char separator = '?';
for (Map.Entry<String, String> entry : canonicalizedParams.entrySet()) {
builder.append(separator);
builder.append(entry.getKey());
if (entry.getValue() != null && !entry.getValue().isEmpty()) {
builder.append("=").append(entry.getValue());
}
separator = '&';
}
}
return builder.toString();
}
public static String uriEncoding(String uri) {
String result = "";
try {
for (char c : uri.toCharArray()) {
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
|| (c >= '0' && c <= '9') || c == '_' || c == '-'
|| c == '~' || c == '.') {
result += c;
} else if (c == '/') {
result += "%2F";
} else {
byte[] b;
b = Character.toString(c).getBytes("utf-8");
for (int i = 0; i < b.length; i++) {
int k = b[i];
if (k < 0) {
k += 256;
}
result += "%" + Integer.toHexString(k).toUpperCase();
}
}
}
} catch (UnsupportedEncodingException e) {
throw new ClientException(e);
}
return result;
}
public static String buildSignature(String secretAccessKey, String httpMethod, String resourcePath, RequestMessage request) {
String canonicalString = buildCanonicalString(httpMethod, resourcePath, request);
return new HmacSHA256Signature().computeSignature(secretAccessKey, canonicalString);
}
}
...@@ -15,6 +15,7 @@ import com.wecloud.im.sdk.common.HttpClient; ...@@ -15,6 +15,7 @@ import com.wecloud.im.sdk.common.HttpClient;
import com.wecloud.im.sdk.common.HttpMethod; import com.wecloud.im.sdk.common.HttpMethod;
import com.wecloud.im.sdk.common.RequestMessage; import com.wecloud.im.sdk.common.RequestMessage;
import com.wecloud.im.sdk.common.RequestParamSigner; import com.wecloud.im.sdk.common.RequestParamSigner;
import com.wecloud.im.sdk.common.auth.Credentials;
import com.wecloud.im.sdk.exception.WecloudException; import com.wecloud.im.sdk.exception.WecloudException;
/** /**
...@@ -41,7 +42,7 @@ public abstract class WecloudImOperation { ...@@ -41,7 +42,7 @@ public abstract class WecloudImOperation {
// 请求头签名 // 请求头签名
String signature = RequestParamSigner.sign(request.getClientId(), request.getClientId(), this.appKey, String signature = RequestParamSigner.sign(request.getClientId(), request.getClientId(), this.appKey,
this.appSecret, request.getPlatform()); this.appSecret, request.getPlatform());
request.addHeader(WecloudHeaders.WECLOUD_SIGN, signature); request.addHeader(ImHeaders.WECLOUD_SIGN, signature);
return send(request); return send(request);
} }
...@@ -80,8 +81,12 @@ public abstract class WecloudImOperation { ...@@ -80,8 +81,12 @@ public abstract class WecloudImOperation {
} catch (Exception e) { } catch (Exception e) {
throw new WecloudException("发送请求报错: " + e.getMessage()); throw new WecloudException("发送请求报错: " + e.getMessage());
} }
}
private static RequestSigner createSigner(HttpMethod method, String bucketName, String key, Credentials creds) {
String resourcePath = "/" + ((bucketName != null) ? bucketName + "/" : "") + ((key != null ? key : ""));
return new RequestSigner(method.toString(), resourcePath, creds);
} }
......
...@@ -18,4 +18,6 @@ public abstract class WebServiceRequest { ...@@ -18,4 +18,6 @@ public abstract class WebServiceRequest {
* 预留版本号id * 预留版本号id
*/ */
private String versionId; private String versionId;
} }
package com.wecloud.im.sdk.sample;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import cn.hutool.core.date.DateUtil;
import com.wecloud.im.sdk.common.HttpMethod;
import com.wecloud.im.sdk.common.RequestMessage;
import com.wecloud.im.sdk.common.auth.HmacSHA256Signature;
import com.wecloud.im.sdk.internal.ImHeaders;
import com.wecloud.im.sdk.internal.OSSRequestMessageBuilder;
import com.wecloud.im.sdk.internal.SignUtils;
/**
*
* @Author luozh
* @Date 2022年04月14日 15:37
* @Version 1.0
*/
public class SignTest {
public static void main(String[] args) {
String date = DateUtil.formatHttpDate(new Date());
// 资源地址
String resourcePath = "/api/imClient/registerClient";
// 请求头
Map<String, String> headers = new HashMap<>();
headers.put(ImHeaders.DATE, date);
headers.put(ImHeaders.CONTENT_TYPE, "application/json");
// 请求参数
Map<String, String> param = new HashMap<>();
param.put("appKey", "appKey");
param.put("userId", "1111222333");
param.put("headPortrait", "");
param.put("nickname", "罗罗啦");
param.put("deviceType", "1");
// 发送请求
RequestMessage request = new OSSRequestMessageBuilder().setEndpoint("/api/imClient/registerClient")
.setMethod(HttpMethod.POST).setParameters(param).addHeaders(headers)
.setOriginalRequest(null).build();
String canonicalString = SignUtils.buildCanonicalString("POST", resourcePath, request);
String signature = new HmacSHA256Signature().computeSignature("secretAccessKey", canonicalString);
String authorization = SignUtils.composeRequestAuthorization("accessKeyId", signature);
System.out.println("canonicalString: " + canonicalString);
System.out.println("signature: " + signature);
System.out.println("Authorization header: " + authorization);
}
}
package com.wecloud.im.sdk.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Base64;
public class BinaryUtil {
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
'E', 'F'};
public static String toBase64String(byte[] binaryData) {
return new String(Base64.encodeBase64(binaryData));
}
public static byte[] fromBase64String(String base64String) {
return Base64.decodeBase64(base64String);
}
public static byte[] calculateMd5(byte[] binaryData) {
MessageDigest messageDigest = null;
try {
messageDigest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not found.");
}
messageDigest.update(binaryData);
return messageDigest.digest();
}
public static String encodeMD5(byte[] binaryData) {
byte[] md5Bytes = calculateMd5(binaryData);
int len = md5Bytes.length;
char buf[] = new char[len * 2];
for (int i = 0; i < len; i++) {
buf[i * 2] = HEX_DIGITS[(md5Bytes[i] >>> 4) & 0x0f];
buf[i * 2 + 1] = HEX_DIGITS[md5Bytes[i] & 0x0f];
}
return new String(buf);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment