This commit is contained in:
王贵源 2019-09-29 09:33:56 +08:00
commit 69d2d09f96
34 changed files with 1870 additions and 0 deletions

20
.gitignore vendored Normal file
View File

@ -0,0 +1,20 @@
*.class
*.iml
.idea/
.DS_Store
# Package Files #
*.jar
*.war
*.ear
#workspace Files
node_modules/
dist/
.factorypath
target/
.settings/
.factorypath
.classpath
.project

147
pom.xml Normal file
View File

@ -0,0 +1,147 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<name>rop</name>
<description>restful open platform</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop-core</artifactId>
<version>${axe-version}</version>
</dependency>
<dependency>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop-server</artifactId>
<version>${axe-version}</version>
</dependency>
<dependency>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop-client</artifactId>
<version>${axe-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<properties>
<axe-version>1.0</axe-version>
</properties>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<developers>
<developer>
<name>kerbores</name>
<email>kerbores@gmail.com</email>
</developer>
</developers>
<scm>
<connection>scm:https://github.com/ZHCS-CLUB/AXE.git</connection>
<developerConnection>scm:https://github.com/ZHCS-CLUB/AXE.git</developerConnection>
<url>https://github.com/ZHCS-CLUB/AXE.git</url>
</scm>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<executions>
<execution>
<id>attach-javadocs</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
<configuration>
<additionalparam>-Xdoclint:none</additionalparam>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
<configuration>
<additionalparam>${javadoc.opts}</additionalparam>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.3</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>nutz</id>
<url>http://jfrog.nutz.cn/artifactory/snapshots/</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>http://jfrog.nutz.cn/artifactory/soho-project/</url>
</repository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutz</artifactId>
<version>[1.r.68,)</version>
</dependency>
</dependencies>
<modules>
<module>rop-core</module>
<module>rop-server</module>
<module>rop-client</module>
</modules>
</project>

31
rop-client/pom.xml Normal file
View File

@ -0,0 +1,31 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop</artifactId>
<version>1.0</version>
</parent>
<artifactId>rop-client</artifactId>
<name>rop-client</name>
<description>rop invoke client integration</description>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,25 @@
package cn.com.chinarecrm.rop.client;
import cn.com.chinarecrm.rop.core.signer.Signer;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public interface ClientSigner extends Signer {
/**
* 客户端签名
*
* @param appSecret
* 应用密钥
* @param timestamp
* 时间戳
* @param gateway
* 方法/路由
* @param nonce
* 随机串
* @param request
* 请求
* @return 签名字符串
*/
public String sign(String appSecret, String timestamp, String gateway, String nonce, ROPRequest request);
}

View File

@ -0,0 +1,115 @@
package cn.com.chinarecrm.rop.client;
import org.nutz.http.Header;
import org.nutz.http.Request;
import org.nutz.http.Request.METHOD;
import org.nutz.http.Response;
import org.nutz.http.Sender;
import org.nutz.lang.Strings;
import org.nutz.lang.Times;
import org.nutz.lang.random.R;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import cn.com.chinarecrm.rop.ROPConfig;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class ROPClient {
public static ROPClient create(String appKey, String appSecret, String endpoint, String digestName) {
ROPClient client = new ROPClient();
client.setAppKey(appKey);
client.setAppSecret(appSecret);
client.setEndpoint(endpoint);
client.setDigestName(digestName);
return client;
}
private String appKey;
private String appSecret;
private String digestName;
private String endpoint;// 调用点
Log log = Logs.get();
private ROPClient() {}
public String getAppKey() {
return appKey;
}
public String getAppSecret() {
return appSecret;
}
public String getDigestName() {
return digestName;
}
public String getEndpoint() {
return endpoint;
}
public Response send(ROPRequest request) {
return Sender.create(toRequest(request)).send();
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public void setDigestName(String digestName) {
this.digestName = digestName;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
/**
* 处理header
*
* @param request
* @return
*/
private Header signHeader(ROPRequest request) {
String nonce = R.UU16();
String ts = Times.now().getTime() + "";
Header header = null;
if (request.getMethod() == METHOD.GET) {
String query = request.getURLEncodedParams();
String method = Strings.isBlank(query) ? request.getGateway() : request.getGateway() + "?" + query;
header = request.getHeader()
.set(ROPConfig.APP_KEY_KEY, appKey)
.set(ROPConfig.METHOD_KEY, method)
.set(ROPConfig.NONCE_KEY, nonce)
.set(ROPConfig.TS_KEY, ts)
.set(ROPConfig.SIGN_KEY, new ROPClientDigestSigner(digestName).sign(appSecret, ts, method, nonce, request));
} else {
header = request.getHeader()
.set(ROPConfig.APP_KEY_KEY, appKey)
.set(ROPConfig.METHOD_KEY, request.getGateway())
.set(ROPConfig.NONCE_KEY, nonce)
.set(ROPConfig.TS_KEY, ts)
.set(ROPConfig.SIGN_KEY,
new ROPClientDigestSigner(digestName).sign(appSecret, ts, request.getGateway(), nonce, request));
}
return request.getData() == null || request.getData().length == 0 ? header.asFormContentType() : header.asJsonContentType();
}
public Request toRequest(ROPRequest request) {
Request req = Request.create(endpoint, request.getMethod());
req.setParams(request.getParams());
req.setData(request.getData());
req.setHeader(signHeader(request));
Header header = req.getHeader();
log.debugf("send headers %s", header);
return req;
}
}

View File

@ -0,0 +1,43 @@
package cn.com.chinarecrm.rop.client;
import java.io.IOException;
import java.io.InputStreamReader;
import org.nutz.lang.Lang;
import org.nutz.lang.Streams;
import org.nutz.lang.Strings;
import cn.com.chinarecrm.rop.core.signer.DigestSigner;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class ROPClientDigestSigner extends DigestSigner implements ClientSigner {
public ROPClientDigestSigner(String name) {
super(name);
}
protected String getDataMate(ROPRequest request) {
if (request.isGet()) {
String query = request.getURLEncodedParams();
String method = Strings.isBlank(query) ? request.getGateway() : request.getGateway() + "?" + query;
query = method.indexOf('?') >= 0 ? method.substring(method.indexOf('?') + 1) : "";
return Lang.md5(query);
}
StringBuilder info;
try {
info = Streams.read(new InputStreamReader(request.getInputStream()));
}
catch (IOException e) {
throw Lang.wrapThrow(e);
}
return Lang.md5(info);
}
@Override
public String sign(String appSecret, String timestamp, String gateway, String nonce, ROPRequest request) {
return sign(appSecret, timestamp, gateway, nonce, getDataMate(request));
}
}

View File

@ -0,0 +1,259 @@
package cn.com.chinarecrm.rop.client;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.nutz.http.Cookie;
import org.nutz.http.Header;
import org.nutz.http.Http;
import org.nutz.http.Request.METHOD;
import org.nutz.json.Json;
import org.nutz.lang.ContinueLoop;
import org.nutz.lang.Each;
import org.nutz.lang.Encoding;
import org.nutz.lang.ExitLoop;
import org.nutz.lang.Lang;
import org.nutz.lang.LoopException;
import org.nutz.lang.util.NutMap;
import cn.com.chinarecrm.rop.core.signer.SignerHelper;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class ROPRequest {
public static ROPRequest create(String gateway, METHOD method) {
return create(gateway, method, new HashMap<String, Object>());
}
public static ROPRequest create(String gateway, METHOD method, Map<String, Object> params) {
return ROPRequest.create(gateway, method, params, Header.create());
}
public static ROPRequest create(String gateway, METHOD method, Map<String, Object> params, Header header) {
return new ROPRequest().setMethod(method).setParams(params).setGateway(gateway).setHeader(header);
}
public static ROPRequest create(String gateway, METHOD method, String paramsAsJson) {
return create(gateway, method, (Map<String, Object>) Json.fromJson(paramsAsJson));
}
public static ROPRequest create(String gateway, METHOD method, String paramsAsJson, Header header) {
return create(gateway, method, (Map<String, Object>) Json.fromJson(paramsAsJson), header);
}
public static ROPRequest get(String gateway) {
return create(gateway, METHOD.GET, new HashMap<String, Object>());
}
public static ROPRequest get(String gateway, Header header) {
return ROPRequest.create(gateway, METHOD.GET, new HashMap<String, Object>(), header);
}
public static ROPRequest post(String gateway) {
return create(gateway, METHOD.POST, new HashMap<String, Object>());
}
public static ROPRequest post(String gateway, Header header) {
return ROPRequest.create(gateway, METHOD.POST, new HashMap<String, Object>(), header);
}
private byte[] data;
private String enc = Encoding.UTF8;
private String gateway;
private Header header;
private InputStream inputStream;
private METHOD method;
private Map<String, Object> params;
private ROPRequest() {}
protected void fileUpload(final StringBuilder sb) {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
for (final String key : keys) {
Object val = params.get(key);
if (val == null)
val = "";
Lang.each(val, new Each<Object>() {
@Override
public void invoke(int index, Object ele, int length)
throws ExitLoop, ContinueLoop, LoopException {
if (ele instanceof File) {
sb.append(Http.encode(key, enc))
.append('=')
.append(Http.encode(Lang.md5((File) ele), enc))
.append('&');
} else {
sb.append(Http.encode(key, enc))
.append('=')
.append(Http.encode(ele, enc))
.append('&');
}
}
});
}
}
public Cookie getCookie() {
String s = header.get("Cookie");
if (null == s)
return new Cookie();
return new Cookie(s);
}
public byte[] getData() {
return data;
}
public String getEnc() {
return enc;
}
/**
* @return the gateway
*/
public String getGateway() {
return gateway;
}
public Header getHeader() {
return header;
}
public InputStream getInputStream() {
if (inputStream != null) {
return inputStream;
} else {
if (header.get("Content-Type") == null)
header.asFormContentType(enc);
if (null == data) {
try {
return new ByteArrayInputStream(getURLEncodedParams().getBytes(enc));
}
catch (UnsupportedEncodingException e) {
throw Lang.wrapThrow(e);
}
}
return new ByteArrayInputStream(data);
}
}
public METHOD getMethod() {
return method;
}
public Map<String, Object> getParams() {
return params;
}
public String getURLEncodedParams() {
// 此处不影响发送的数据,只影响签名,需要按照rop的规则进行签名串的获取即可
final StringBuilder sb = new StringBuilder();
if (isFileUpload()) {// 文件上传的签名流
fileUpload(sb);
} else if (params != null) {
return SignerHelper.mapAsUrlParams(params, enc);
}
if (sb.length() > 0)
sb.setLength(sb.length() - 1);
return sb.toString();
}
public ROPRequest header(String key, String value) {
getHeader().set(key, value);
return this;
}
public boolean isDelete() {
return METHOD.DELETE == method;
}
public boolean isFileUpload() {
final NutMap t = NutMap.NEW().addv("target", false);
if ((isPost() || isPut()) && getParams() != null) {
for (Object val : getParams().values()) {
Lang.each(val, new Each<Object>() {
@Override
public void invoke(int index, Object ele, int length) throws ExitLoop, ContinueLoop, LoopException {
if (ele instanceof File) {
t.put("target", true);
throw new ExitLoop();
}
}
});
}
}
return t.getBoolean("target");
}
public boolean isGet() {
return METHOD.GET == method;
}
public boolean isPost() {
return METHOD.POST == method;
}
public boolean isPut() {
return METHOD.PUT == method;
}
public ROPRequest setCookie(Cookie cookie) {
header.set("Cookie", cookie.toString());
return this;
}
public ROPRequest setData(byte[] data) {
this.data = data;
return this;
}
public ROPRequest setData(String data) {
this.data = data.getBytes(StandardCharsets.UTF_8);
return this;
}
public ROPRequest setEnc(String reqEnc) {
if (reqEnc != null)
this.enc = reqEnc;
return this;
}
public ROPRequest setGateway(String gateway) {
this.gateway = gateway;
return this;
}
public ROPRequest setHeader(Header header) {
if (header == null)
header = Header.create();
this.header = header;
return this;
}
public ROPRequest setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
return this;
}
public ROPRequest setMethod(METHOD method) {
this.method = method;
return this;
}
public ROPRequest setParams(Map<String, Object> params) {
this.params = params;
return this;
}
}

View File

@ -0,0 +1,23 @@
package cn.com.chinarecrm.rop.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import cn.com.chinarecrm.rop.client.ROPClient;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
@Configuration
@EnableConfigurationProperties(ROPClientConfigurationProperties.class)
public class ROPClientAutoConfiguration {
@Bean
public ROPClient ropClient(ROPClientConfigurationProperties configProperties) {
return ROPClient.create(configProperties.getAppKey(),
configProperties.getAppSecret(),
configProperties.getEndpoint(),
configProperties.getDigestName());
}
}

View File

@ -0,0 +1,59 @@
package cn.com.chinarecrm.rop.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
@ConfigurationProperties(prefix = "rop.client")
public class ROPClientConfigurationProperties {
/**
* appKey
*/
String appKey;
/**
* appSecret
*/
String appSecret;
/**
* 签名算法
*/
String digestName;
/**
* 对方接口地址
*/
String endpoint;
public String getAppKey() {
return appKey;
}
public String getAppSecret() {
return appSecret;
}
public String getDigestName() {
return digestName;
}
public String getEndpoint() {
return endpoint;
}
public void setAppKey(String appKey) {
this.appKey = appKey;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
public void setDigestName(String digestName) {
this.digestName = digestName;
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
}

View File

@ -0,0 +1,2 @@
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.com.chinarecrm.rop.config.ROPClientAutoConfiguration

View File

@ -0,0 +1 @@
provides: chinare-rop-client

20
rop-core/pom.xml Normal file
View File

@ -0,0 +1,20 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop</artifactId>
<version>1.0</version>
</parent>
<artifactId>rop-core</artifactId>
<name>rop-core</name>
<description>rop core for sign and check</description>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,38 @@
package cn.com.chinarecrm.rop;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class ROP {
public static final String DESCRIPTION = "chinare rest open platform";
public static final int MAJOR_VERSION = 1;
public static final int MINOR_VERSION = 0;
public static final String NAME = "rop";
public static final String RELEASE_LEVEL = "b";
public static final boolean SNAPSHOT = false;
public static int majorVersion() {
return MAJOR_VERSION;
}
public static int minorVersion() {
return MINOR_VERSION;
}
public static String releaseLevel() {
return RELEASE_LEVEL;
}
public static String v() {
return String.format("%d.%s.%d%s",
majorVersion(),
releaseLevel(),
minorVersion(),
SNAPSHOT ? ".SNAPSHOT" : "");
}
/**
*
*/
private ROP() {}
}

View File

@ -0,0 +1,21 @@
package cn.com.chinarecrm.rop;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class ROPConfig {
public static final String APP_KEY_KEY = "rop-sign-appkey";
public static final String APP_SECRET_KEY = "rop-sign-appsecret";
public static final String METHOD_KEY = "rop-service-method";
public static final String NONCE_KEY = "rop-nonce";
public static final String PARAS_KEY = "rop-paras";
public static final String SIGNER_KEY = "rop-signer-name";
public static final String SIGN_KEY = "rop-sign";
public static final String TS_KEY = "rop-ts";
/**
*
*/
private ROPConfig() {}
}

View File

@ -0,0 +1,27 @@
package cn.com.chinarecrm.rop.core;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public enum OperationState {
/**
* 默认
*/
DEFAULT,
/**
* 异常
*/
EXCEPTION,
/**
* 失败
*/
FAIL,
/**
* 成功
*/
SUCCESS,
/**
* 未登录
*/
UNLOGINED
}

View File

@ -0,0 +1,146 @@
package cn.com.chinarecrm.rop.core;
import org.nutz.json.Json;
import org.nutz.json.JsonFormat;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class ROPData<T> {
/**
* 创建一个异常结果
*
* @return 一个异常结果实例,不携带异常信息
*/
public static ROPData exception() {
return ROPData.me().setOperationState(OperationState.EXCEPTION);
}
/**
* 创建一个异常结果
*
* @param e
* 异常
* @return 一个异常结果实例,包含参数异常的信息
*/
public static ROPData exception(Exception e) {
return ROPData.exception(e.getMessage());
}
/**
* 创建一个异常结果
*
* @param msg
* 异常信息
* @return 一个异常结果实例,不携带异常信息
*/
public static ROPData exception(String msg) {
return ROPData.exception().setMsg(msg);
}
/**
* 创建一个带失败信息的ROPData
*
* @param reason
* 失败原因
* @return ROPData实例
*/
public static ROPData fail(String reason) {
return ROPData.me().setOperationState(OperationState.FAIL).setMsg(reason);
}
/**
* 获取一个ROPData实例
*
* @return 一个不携带任何信息的ROPData实例
*/
public static ROPData me() {
return new ROPData();
}
/**
* 创建一个成功结果
*
* @return ROPData实例状态为成功无数据携带
*/
public static ROPData success() {
return ROPData.me().setOperationState(OperationState.SUCCESS);
}
/**
* 未登录
*
* @return 未登录
*/
public static ROPData unlogin() {
return ROPData.me().setOperationState(OperationState.UNLOGINED);
}
/**
* 操作结果数据 假设一个操作要返回很多的数据 一个用户名 一个产品 一个相关产品列表 一个产品的评论信息列表 我们以key
* value形式进行保存页面获取data对象读取其对于的value即可
*/
private T data;
private String msg;
/**
* 带状态的操作 比如登录有成功和失败
*/
private OperationState operationState = OperationState.DEFAULT;
public ROPData() {
super();
}
public T getData() {
return data;
}
public String getMsg() {
return msg;
}
public OperationState getOperationState() {
return operationState;
}
/**
* 是否成功
*
* @return 是否成功
*/
public boolean isSuccess() {
return getOperationState() == OperationState.SUCCESS;
}
public ROPData<T> setData(T data) {
this.data = data;
return this;
}
public ROPData setMsg(String msg) {
this.msg = msg;
return this;
}
public ROPData setOperationState(OperationState operationState) {
this.operationState = operationState;
return this;
}
public ROPData<T> success(T t) {
return success().setData(t);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return Json.toJson(this, JsonFormat.forLook());
}
}

View File

@ -0,0 +1,171 @@
package cn.com.chinarecrm.rop.core.signer;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Collections;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import org.nutz.http.Http;
import org.nutz.lang.Lang;
import org.nutz.lang.Streams;
import org.nutz.lang.Strings;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.upload.UploadingContext;
import cn.com.chinarecrm.rop.ROPConfig;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public abstract class AbstractSinger implements Signer {
UploadingContext context = new UploadingContext(System.getProperty("java.io.tmpdir"));
Log log = Logs.get();
@Override
public boolean check(HttpServletRequest request, AppsecretFetcher fetcher) {
if (Strings.isBlank(request.getHeader(ROPConfig.APP_KEY_KEY))
|| Strings.isBlank(request.getHeader(ROPConfig.TS_KEY))
|| Strings.isBlank(request.getHeader(ROPConfig.METHOD_KEY))
|| Strings.isBlank(request.getHeader(ROPConfig.NONCE_KEY))
|| Strings.isBlank(request.getHeader(ROPConfig.SIGN_KEY))) {
return false;
}
String sign = request.getHeader(ROPConfig.SIGN_KEY);
log.debugf("Expected sign is %s", sign);
return Strings.equalsIgnoreCase(sign(request, fetcher), sign);
}
public String contentType(HttpServletRequest request) {
return request.getHeader("Content-Type");
}
protected String getDataMate(HttpServletRequest request) {
if (Strings.equalsIgnoreCase(request.getMethod(), "GET")) {// GET请求需要处理一下
return Lang.md5(request.getQueryString());
}
// 文件上传
if (isFileUpload(request)) {
try {
return Lang.md5(new ByteArrayInputStream(
getURLEncodedParams(request).getBytes(request.getCharacterEncoding())));
}
catch (IOException | ServletException e) {
log.debug("不支持的编码!");
throw Lang.wrapThrow(e);
}
}
try {
StringBuilder info = Streams.read(new InputStreamReader(request.getInputStream()));
if (info.length() == 0) {
return Lang.md5(
SignerHelper.paramMapAsUrlString(request.getParameterMap(), request.getCharacterEncoding()));
}
return Lang.md5(info);
}
catch (IOException e) {
throw Lang.wrapThrow(e);
}
}
public String getURLEncodedParams(final HttpServletRequest request) throws IOException, ServletException {
final StringBuilder sb = new StringBuilder();
List<Part> parts = Lang.collection2list(request.getParts());
Collections.sort(parts, (part1, part2) -> part1.getName().compareTo(part2.getName()));
parts.stream().forEach(part -> {
String key = part.getName();
if (Strings.isBlank(part.getContentType())) {
// 参数
sb.append(Http.encode(key, request.getCharacterEncoding()))
.append('=')
.append(Http.encode(request.getParameter(key), request.getCharacterEncoding()))
.append('&');
} else {
// 文件
try {
sb.append(Http.encode(key, request.getCharacterEncoding()))
.append('=')
.append(Http.encode(Lang.md5(part.getInputStream()), request.getCharacterEncoding()))
.append('&');
}
catch (IOException e) {
throw Lang.wrapThrow(e);
}
}
});
if (sb.length() > 0)
sb.setLength(sb.length() - 1);
return sb.toString();
}
private boolean isCommonFileUpload(HttpServletRequest request) {
return contentType(request) != null
&& contentType(request).startsWith("multipart/form-data");
}
/**
* @param request
* @return
*/
private boolean isFileUpload(HttpServletRequest request) {
return isCommonFileUpload(request) || isHtml5FileUpload(request);
}
private boolean isHtml5FileUpload(HttpServletRequest request) {
return contentType(request) != null
&& contentType(request).startsWith("application/octet-stream");
}
/**
* 根据appSecret获取器签名
*
* @param fetcher
* appSecret获取器
* @param appKey
* 应用key
* @param timestamp
* 时间戳
* @param gateway
* 方法/路由
* @param nonce
* 随机串
* @param dataMate
* 数据元数据
* @return 签名字符串
*/
public String sign(AppsecretFetcher fetcher,
String appKey,
String timestamp,
String gateway,
String nonce,
String dataMate) {
return sign(fetcher.fetch(appKey), timestamp, gateway, nonce, dataMate);
}
/**
* 服务器端签名
*
* @param request
* 请求
* @param fetcher
* appSecret获取器
* @return 签名字符串
*/
public String sign(HttpServletRequest request, AppsecretFetcher fetcher) {
return sign(fetcher,
request.getHeader(ROPConfig.APP_KEY_KEY),
request.getHeader(ROPConfig.TS_KEY),
request.getHeader(ROPConfig.METHOD_KEY),
request.getHeader(ROPConfig.NONCE_KEY),
getDataMate(request));
}
}

View File

@ -0,0 +1,9 @@
package cn.com.chinarecrm.rop.core.signer;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public interface AppsecretFetcher {
public String fetch(String key);
}

View File

@ -0,0 +1,15 @@
package cn.com.chinarecrm.rop.core.signer;
import org.nutz.lang.Lang;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class DefaultMD5Fetcher implements AppsecretFetcher {
@Override
public String fetch(String key) {
return Lang.md5(key);
}
}

View File

@ -0,0 +1,38 @@
package cn.com.chinarecrm.rop.core.signer;
import java.util.Arrays;
import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class DigestSigner extends AbstractSinger {
private String name;
/**
*
*/
public DigestSigner() {
this.name = "MD5";
}
public DigestSigner(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public String sign(String appSecret, String timestamp, String gateway, String nonce, String dataMate) {
String[] temp = Lang.array(appSecret, timestamp, gateway, nonce, dataMate);
Arrays.sort(temp);
log.debugf("sign with %s args %s", name(), temp);
return Lang.digest(name(), Strings.join("", temp));
}
}

View File

@ -0,0 +1,43 @@
package cn.com.chinarecrm.rop.core.signer;
import javax.servlet.http.HttpServletRequest;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public interface Signer {
/**
* 签名检查
*
* @param request
* 请求
* @param fetcher
* 密钥获取器
* @return 是否检查通过
*/
public boolean check(HttpServletRequest request, AppsecretFetcher fetcher);
/**
* 名称
*
* @return 签名器名称
*/
public String name();
/**
*
* @param appSecret
* 密钥
* @param timestamp
* 时间戳
* @param gateway
* 网关/方法名称
* @param nonce
* 随机串
* @param dataMate
* 数据元数据
* @return 签名字符串
*/
public String sign(String appSecret, String timestamp, String gateway, String nonce, String dataMate);
}

View File

@ -0,0 +1,99 @@
package cn.com.chinarecrm.rop.core.signer;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.nutz.http.Http;
import org.nutz.lang.ContinueLoop;
import org.nutz.lang.Each;
import org.nutz.lang.ExitLoop;
import org.nutz.lang.Lang;
import org.nutz.lang.LoopException;
import org.nutz.lang.Strings;
/**
* @author 王贵源(wangguiyuan@chinarecrm.com.cn)
*/
public class SignerHelper {
public static String mapAsUrlParams(Map<String, Object> map, String enc) {
if (map == null || map.size() == 0) {
return "";
}
String[] keys = Lang.collection2array(map.keySet());
Arrays.sort(keys);
StringBuilder sb = new StringBuilder();
Lang.each(keys, new Each<String>() {
@Override
public void invoke(int index, String key, int length) throws ExitLoop, ContinueLoop, LoopException {
Object val = map.get(key);
if (val instanceof Collection || val.getClass().isArray()) {
// 数组情况的处理
sb.append(Http.encode(key, enc))
.append('=')
.append(Http.encode(toStringInfo(val), enc))
.append('&');
} else {
sb.append(Http.encode(key, enc))
.append('=')
.append(Http.encode(val, enc))
.append('&');
}
}
});
if (sb.length() > 0)
sb.setLength(sb.length() - 1);
return sb.toString();
}
public static String paramMapAsUrlString(Map<String, String[]> map, String enc) {
if (map == null || map.size() == 0) {
return "";
}
String[] keys = Lang.collection2array(map.keySet());
Arrays.sort(keys);
StringBuilder sb = new StringBuilder();
Lang.each(keys, new Each<String>() {
@Override
public void invoke(int index, String key, int length) throws ExitLoop, ContinueLoop, LoopException {
String[] val = map.get(key);
if (val.length == 1) {
// 单值的情况
sb.append(Http.encode(key, enc))
.append('=')
.append(Http.encode(val[0], enc))
.append('&');
} else {
sb.append(Http.encode(key, enc))
.append('=')
.append(Http.encode(toStringInfo(val), enc))
.append('&');
}
}
});
if (sb.length() > 0)
sb.setLength(sb.length() - 1);
return sb.toString();
}
protected static String toStringInfo(Object val) {
List<Object> info = Lang.list();
Lang.each(val, new Each<Object>() {
@Override
public void invoke(int index, Object ele, int length) throws ExitLoop, ContinueLoop, LoopException {
info.add(ele);
}
});
return Strings.join(",", info);
}
private SignerHelper() {}
}

35
rop-server/pom.xml Normal file
View File

@ -0,0 +1,35 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop</artifactId>
<version>1.0</version>
</parent>
<artifactId>rop-server</artifactId>
<name>rop-server</name>
<description>rop server integration</description>
<dependencies>
<dependency>
<groupId>cn.com.chinarecrm</groupId>
<artifactId>rop-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>