Merge branch '5.0'

# Conflicts:
#	sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/entity/ConfigServiceRoute.java
#	sop-gateway/src/main/java/com/gitee/sop/gateway/entity/ConfigServiceRoute.java
This commit is contained in:
六如
2024-12-23 00:18:44 +08:00
2528 changed files with 92851 additions and 70184 deletions

18
.gitignore vendored Normal file → Executable file
View File

@@ -1,4 +1,4 @@
/target/
target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
@@ -8,20 +8,18 @@
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
*.png
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
nbproject/private/
build/
nbbuild/
dist/
nbdist/
.nb-gradle/
/local-config/

6
Dockerfile Normal file → Executable file
View File

@@ -4,10 +4,8 @@ VOLUME /sop
# 将所有应用放到一个镜像当中
ADD sop-gateway/target/*.jar sop/sop-gateway/sop-gateway.jar
ADD sop-admin/sop-admin-server/target/*.jar sop/sop-admin/sop-admin.jar
ADD sop-website/sop-website-server/target/*.jar sop/sop-website/sop-website.jar
ADD sop-auth/target/*.jar sop/sop-auth/sop-auth.jar
ADD sop-example/sop-story/target/*.jar sop/sop-story/sop-story.jar
ADD sop-admin/sop-admin-backend/backend-boot/target/*.jar sop/sop-admin/sop-admin.jar
ADD sop-example/example-story/target/*.jar sop/sop-story/sop-story.jar
# 拷贝启动脚本
COPY docker-entrypoint.sh /usr/local/bin/

0
LICENSE Normal file → Executable file
View File

227
README.md Normal file → Executable file
View File

@@ -1,144 +1,137 @@
# SOP(Simple Open Platform)
一个开放平台解决方案项目基于Spring Cloud实现目标让用户快速搭建自己的开放平台。
一个开放平台解决方案项目基于dubbo实现目标让用户快速搭建自己的开放平台。
通过简单的配置后,你的项目就具备了和支付宝开放平台的一样的接口提供能力。
SOP封装了开放平台大部分功能包括签名验证、统一异常处理、统一返回内容 、业务参数验证JSR-303、秘钥管理等未来还会实现更多功能。
## 项目特点
- 接入方式简单,与老项目不冲突,老项目注册到注册中心,然后在方法上加上注解即可。
- 架构松耦合业务代码实现在各自微服务上SOP不参与业务实现这也是Spring Cloud微服务体系带来的好处
- 扩展简单,开放平台对应的功能各自独立,可以自定义实现自己的需求,如:更改参数,更改签名规则等
## 项目特点
+ 接入方式简单,与老项目不冲突,老项目注册到注册中心,然后在方法上加上注解即可。
+ 架构松耦合业务代码实现在各自微服务上SOP不参与业务实现这也是dubbo微服务体系带来的好处
+ 扩展简单,开放平台对应的功能各自独立,可以自定义实现自己的需求,如:更改参数,更改签名规则等。
## 谁可以使用这个项目
- 有现成的项目,想改造成开放平台供他人调用
- 有现成的项目,想暴露其中几个接口并通过开放平台供他人调用
- 想搭一个开放平台新项目,并结合微服务的方式去维护
- 对开放平台感兴趣的朋友
+ 有现成的项目,想改造成开放平台供他人调用
+ 有现成的项目,想暴露其中几个接口并通过开放平台供他人调用
+ 想搭一个开放平台新项目,并结合微服务的方式去维护
+ 对开放平台感兴趣的朋友
以上情况都可以考虑使用SOP
## 例子
开放接口定义
```java
// 加一个注解即可
@Open("story.get")
@RequestMapping("/get")
public StoryResult get() {
StoryResult result = new StoryResult();
result.setId(1L);
result.setName("海底小纵队");
return result;
/**
* 支付接口
*
* @author 六如
*/
@Api("支付接口")
public interface OpenPayment {
@ApiOperation(
value = "手机网站支付接口",
notes = "该接口是页面跳转接口,用于生成用户访问跳转链接。" +
"请在服务端执行SDK中pageExecute方法读取响应中的body()结果。" +
"该结果用于跳转到页面,返回到用户浏览器渲染或重定向跳转到页面。" +
"具体使用方法请参考 <a href=\"https://torna.cn\" target=\"_blank\">接入指南</a>"
)
@Open("pay.trade.wap.pay")
PayTradeWapPayResponse tradeWapPay(PayTradeWapPayRequest request);
}
```
接口实现
```java
/**
* 开放接口实现
*
* @author 六如
*/
@DubboService(validation = "true")
public class OpenPaymentImpl implements OpenPayment {
@Override
public PayTradeWapPayResponse tradeWapPay(PayTradeWapPayRequest request) {
PayTradeWapPayResponse payTradeWapPayResponse = new PayTradeWapPayResponse();
payTradeWapPayResponse.setPageRedirectionData(UUID.randomUUID().toString());
return payTradeWapPayResponse;
}
}
```
调用:
```java
// 公共请求参数
Map<String, String> params = new HashMap<String, String>();
params.put("app_id", appId);
params.put("method", "story.get");
params.put("format", "json");
params.put("charset", "utf-8");
params.put("sign_type", "RSA2");
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
params.put("version", "1.0");
// 业务参数
Map<String, String> bizContent = new HashMap<>();
bizContent.put("id", "1");
bizContent.put("name", "葫芦娃");
params.put("biz_content", JSON.toJSONString(bizContent));
System.out.println("----------- 请求信息 -----------");
System.out.println("请求参数:" + buildParamQuery(params));
System.out.println("商户秘钥:" + privateKey);
String content = AlipaySignature.getSignContent(params);
System.out.println("待签名内容:" + content);
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
System.out.println("签名(sign)" + sign);
params.put("sign", sign);
System.out.println("URL参数" + buildUrlQuery(params));
System.out.println("----------- 返回结果 -----------");
String responseData = get(url, params);// 发送请求
System.out.println(responseData);
@Test
public void testGet() throws Exception {
// 公共请求参数
Map<String, String> params = new HashMap<String, String>();
params.put("app_id", appId);
params.put("method", "pay.trade.wap.pay");
params.put("format", "json");
params.put("charset", "utf-8");
params.put("sign_type", "RSA2");
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
params.put("version", "1.0");
// 业务参数
Map<String, Object> bizContent = new HashMap<>();
bizContent.put("outTradeNo", "70501111111S001111119");
bizContent.put("totalAmount", "9.00");
bizContent.put("subject", "衣服");
bizContent.put("productCode", "QUICK_WAP_WAY");
params.put("biz_content", JSON.toJSONString(bizContent));
String content = AlipaySignature.getSignContent(params);
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
params.put("sign", sign);
System.out.println("----------- 请求信息 -----------");
System.out.println("请求参数:" + buildParamQuery(params));
System.out.println("商户秘钥" + privateKey);
System.out.println("待签名内容:" + content);
System.out.println("签名(sign)" + sign);
System.out.println("URL参数" + buildUrlQuery(params));
System.out.println("----------- 返回结果 -----------");
String responseData = postJson(url, params);// 发送请求
System.out.println(responseData);
}
```
## 架构图
SDK调用
![架构图](https://images.gitee.com/uploads/images/2019/1227/145216_c9b45109_332975.png "sop3.png")
```java
@Test
public void test() {
PayTradeWapPayRequest request = new PayTradeWapPayRequest();
PayTradeWapPayModel model = new PayTradeWapPayModel();
model.setOutTradeNo("70501111111S001111119");
model.setTotalAmount(new BigDecimal("1000"));
model.setSubject("衣服");
model.setProductCode("QUICK_WAP_WAY");
request.setBizModel(model);
> 如上图所示,整个系统运行后,开发者只需关注微服务中的业务代码,接口变更后重新部署微服务应用即可
Result<PayTradeWapPayResponse> result = client.execute(request);
if (result.isSuccess()) {
PayTradeWapPayResponse response = result.getData();
System.out.println(response);
} else {
System.out.println(result);
}
}
```
## 整体架构
![整体架构](./asset/arc.jpg)
## 已完成列表
- 签名验证
- 统一异常处理
- 统一返回内容
- session管理
- 秘钥管理
- 微服务端自动验证JSR-303
- Admin管理平台统一管理微服务配置管理路由管理微服务上下线
- 门户网站,提供用户注册账号
- 接入方管理+秘钥管理
- 接口权限分配
- 文件上传/下载
- 提供基础SDKJava,C++,C#,Python,Go,Rust,Nodejs
- 接口限流
- 文档整合
- 应用授权
- 监控日志
- 注册中心支持nacos/eureka
- 网关动态修改参数
- 预发布/灰度环境切换
## 界面预览
![服务列表](https://images.gitee.com/uploads/images/2020/1016/134354_c1915902_332975.png "service.png")
![路由管理](https://images.gitee.com/uploads/images/2020/1016/134039_bed1608d_332975.png "route.png")
![限流管理](https://images.gitee.com/uploads/images/2020/1016/134102_f2dcfb25_332975.png "limit.png")
![秘钥信息](https://images.gitee.com/uploads/images/2019/0711/174921_bd817533_332975.png "秘钥信息")
- 门户网站
![首页2](https://images.gitee.com/uploads/images/2021/0318/195935_1d610da8_332975.png "portal-vue.png")
![文档页](https://images.gitee.com/uploads/images/2020/1107/104342_d44849a9_332975.png "portal1.png")
## 工程说明
> 运行环境JDK8Maven3[Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html)Mysql
- doc开发文档
- sop-common公共模块封装常用功能包含签名校验、错误处理、限流等功能
- sop-gateway网关统一访问入口`Spring Cloud Zuul``Spring Cloud Gateway`实现
- sop-example微服务示例含springboot,springmvc示例
- sop-website开放平台对应网站提供文档API、沙箱测试等内容
- sop-auth应用授权服务示例
- sop-admin后台管理
- sop-sdk基础sdk含Java、C#版本
- sop-test接口调用测试用例
## 分支说明
- master发版分支
- develop日常开发分支
- eureka使用eureka注册中心
- pr接受PR的分支提交PR请提交到此分支
[更新说明](./changelog.md)
## 相关文档
[开发文档](http://durcframework.gitee.io/sop)

BIN
asset/arc.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

49
build-admin.sh Executable file
View File

@@ -0,0 +1,49 @@
# 构建admin, 结果输出在dist/sop-admin目录
# 获取当前路径并赋值给变量 current_path
current_path=$(pwd)
# 打印变量的值,以验证赋值是否成功
echo "当前路径是: $current_path"
# 构建目录
dist_dir="dist"
# 服务端文件夹名称
# 执行文件名称
app_name="sop-admin"
version="5.0"
build_folder="${app_name}-${version}"
# 输出目录
target_dir="$dist_dir/${build_folder}"
server_source=sop-admin/sop-admin-backend/admin-boot
# admin前端路径
front_source=sop-admin/sop-admin-frontend
# ------ 构建前端 ------
echo "开始构建sop-admin前端..."
cd $front_source
sh build.sh
cd $current_path
# ------ 构建后端 ------
echo "开始构建sop-admin服务端..."
mvn clean package -pl $server_source -am -DskipTests
# ------ 复制文件 ------
if [ ! -d "$target_dir" ]; then
mkdir -p $target_dir
fi
rm -rf ${target_dir}/*
# 复制前端资源
echo "复制前端文件到$target_dir"
cp -r ${front_source}/dist ./$target_dir
# 复制服务端资源
cp -r ${server_source}/target/*.jar $target_dir
echo "服务端构建完毕,构建结果在${target_dir}文件夹下"

37
build-gateway.sh Normal file
View File

@@ -0,0 +1,37 @@
# 构建网关
# 获取当前路径并赋值给变量 current_path
current_path=$(pwd)
# 打印变量的值,以验证赋值是否成功
echo "当前路径是: $current_path"
# 构建目录
dist_dir="dist"
# 服务端文件夹名称
# 执行文件名称
app_name="sop-gateway"
version="5.0"
build_folder="${app_name}-${version}"
# 输出目录
target_dir="$dist_dir/${build_folder}"
server_source=sop-gateway
# ------ 构建后端 ------
echo "开始构建sop-gateway..."
mvn clean package -pl $server_source -am -DskipTests
# ------ 复制文件 ------
if [ ! -d "$target_dir" ]; then
mkdir -p $target_dir
fi
rm -rf ${target_dir}/*
# 复制服务端资源
cp -r ${server_source}/target/*.jar $target_dir
echo "服务端构建完毕,构建结果在${target_dir}文件夹下"

49
build-website.sh Executable file
View File

@@ -0,0 +1,49 @@
# 构建admin, 结果输出在dist/sop-admin目录
# 获取当前路径并赋值给变量 current_path
current_path=$(pwd)
# 打印变量的值,以验证赋值是否成功
echo "当前路径是: $current_path"
# 构建目录
dist_dir="dist"
# 服务端文件夹名称
# 执行文件名称
app_name="sop-website"
version="5.0"
build_folder="${app_name}-${version}"
# 输出目录
target_dir="$dist_dir/${build_folder}"
server_source=sop-website/sop-website-backend/website-boot
# admin前端路径
front_source=sop-website/sop-website-frontend
# ------ 构建前端 ------
echo "开始构建sop-website前端..."
cd $front_source
sh build.sh
cd $current_path
# ------ 构建后端 ------
echo "开始构建sop-website服务端..."
mvn clean package -pl $server_source -am -DskipTests
# ------ 复制文件 ------
if [ ! -d "$target_dir" ]; then
mkdir -p $target_dir
fi
rm -rf ${target_dir}/*
# 复制前端资源
echo "复制前端文件到$target_dir"
cp -r ${front_source}/dist ./$target_dir
# 复制服务端资源
cp -r ${server_source}/target/*.jar $target_dir
echo "服务端构建完毕,构建结果在${target_dir}文件夹下"

418
changelog.md Normal file → Executable file
View File

@@ -1,419 +1,5 @@
# changelog
## 4.4.2
## 5.0
- 优化参数绑定
## 4.4.1
- 修复单值参数绑定问题
## 4.4.0
**【重要】:升级前请阅读 [升级到4.4.0注意事项](./升级到4.4.0注意事项.md)**
- 优化异常处理
- 优化网关多实例数据库重复保存问题
- 修复`IP``IP+路由ID``IP+APP_ID`限流不生效问题
## 4.3.4
- 修复Request参数在第一位导致绑定失败问题
## 4.3.3
- 修复获取eureka地址问题
## 4.3.2
- 修复微服务方法获取不到OpenContext问题
## 4.3.1
- 修复serviceId有大小写出现404问题
- 修复路有监控错误面板分页问题
## 4.3.0
- 升级`spring-boot/spring-cloud/spring-cloud-alibaba`版本
- 修复`DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144`问题
- 修复压测出现`ClosedChannelException``Connection has been closed BEFORE response`问题
## 4.2.7
- 修复两个微服务相同path问题
- 修复Python-SDK下参数传递问题
## 4.2.6
- 优化网关超时处理
## 4.2.5
- 修复restful负载均衡问题
## 4.2.4
- 修复sop-auth模块启动失败问题
## 4.2.3
- 修复nacos的group不生效问题
## 4.2.2
- 优化版本号
## 4.2.1
- 可指定nacos的group`spring.cloud.nacos.discovery.group=xx`
- 修复spring循环依赖问题
## 4.2.0
需要执行`sop-upgrade-4.2.0.sql`
- 新增ISV用户平台
- 新增门户网站portal
- 新增`C++`,`Rust`语言SDK
## 4.1.0
需要执行`sop-upgrade-4.1.0.sql`
- 重构路由监控功能
- 升级SpringBootSpringCloudSpringCloudAlibaba版本
## 4.0.3
- 可定义业务错误码(见`@Open`注解中的`bizCode`属性)
- 文档参数可指定最大长度(使用`@Length(max = xx)`
- 修复返回大文本导致的错误Exceeded limit on max bytes to buffer : 262144
- 增强参数绑定
## 4.0.2
- 支持swagger排序position属性
## 4.0.1
- 修复超大文本无法请求BUG设置`spring.codec.max-in-memory-size`无效)
## 4.0.0(不兼容3.x)
- 新增@Open注解代替ApiMapping和ApiAbility
- service接入减少代码入侵
- 修改admin密码存储规则
- 完善example
### 不兼容部分
- 移除ApiMapping和ApiAbility改为Open注解
- 移除OpenContext
- 移除zuul
- 移除对easyopen支持
## 3.2.1
- 强化RouteInterceptorContext可获取微服务信息
## 3.2.0
- 使用alibaba cloud
- 新增PythonGo版本SDK
- 返回结果新增全局request_id
- 沙箱环境可填写token
Hoxton.SR3Spring Cloud Version 2.2.1.RELEASESpring Cloud Alibaba Version 2.2.5.RELEASESpring Boot Version
- 优化pom文件
- 优化灰度发布
## 3.1.5
修复文件上传大小不一致问题
## 3.1.4
- 优化跨域
## 3.1.3
- 修复监控日志乱码问题
## 3.1.2
- 修复图片上传保存后图片破损问题
## 3.1.1
- 修复继承WebMvcConfigurationSupport导致的jackson序列化时间问题
- 修复微服务接口返回void网关不会返回code和msg问题
## 3.1.0
- 新增路由监控功能
- 新增路由拦截器
- 优化负载均衡策略
## 3.0.1
- 增强国际化消息现SpringCouldGateway支持英文国际化
- 优化限流配置页
## 3.0.0
- 重构spring cloud gateway网关
- 重构`预发布/灰度发布环境选择`
- zuul和gateway网关二合一可随意切换
- 精简配置文件
- 优化文档中心页面
- 优化接口限流
## 2.5.10
- 优化签名认证,优化校验日志打印
## 2.5.9
- 优化获取context-path
- 修复结果返回产生的NPE问题
## 2.5.8
- 优化参数绑定
## 2.5.7
- 优化restful接口调用
## 2.5.6
- 优化文档显示
- 修复路由拉取接口重复BUG
## 2.5.5
- 优化文档显示
## 2.5.4(不建议使用)
- 可排除其它服务(`sop.service.exclude=your-serviceId1,your-serviceId2`
## 2.5.3(不建议使用)
- 修复JSR-303校验问题
## 2.5.2(不建议使用)
- 修复JSR-303校验枚举对象问题
## 2.5.1
- JSR-303支持嵌套校验
## 2.5.0
**(需要执行`sop-2.5.0.sql`升级文件)**
- 网关可校验token [doc](https://durcframework.gitee.io/sop/#/files/10089_%E8%87%AA%E5%AE%9A%E4%B9%89%E6%A0%A1%E9%AA%8Ctoken?t=1572076365259)
## 2.4.1
- 优化restful接口调用如果正在使用此功能必看 [doc](https://durcframework.gitee.io/sop/#/files/10100_%E6%8F%90%E4%BE%9Brestful%E6%8E%A5%E5%8F%A3?t=1571107529449)
## 2.4.0
- 支持自定义限流持续时间每n秒允许m个请求需要执行`sop-2.4.0.sql`升级脚本)
## 2.3.2
- 支持spring cloud gateway下restful接口调用
## 2.3.1
- 修复restful接口调用通配符问题
## 2.3.0
- 支持请求restful接口设置`sop.restful.enable=true`
## 2.2.0(需要执行`sop-2.2.0.sql`升级文件)
- 支持eureka注册中心`eureka`分支
- 签名内容支持urlencode设置`sign.urlencode=true`
- 可扩展其它注册中心
## 2.1.3
- 优化文件上传校验
## 2.1.2
- 优化获取路由配置
## 2.1.1
- 修复springmvc获取路由问题
## 2.1.0
- 支持分布式限流redis实现
- 可调整JSR-303校验顺序
- 修复springmvc工程注册到nacos无法读取路由配置问题
## 2.0.0
- 全面使用nacos舍弃zookeeper1.x版本见1.x分支
- 可自定义文档模块显示顺序
## 1.15.2
- 优化SpringCloudGateway上传文件功能
- 优化SpringCloudGateway动态修改参数功能
## 1.15.1
- 修复未配置正确MessageConverter导致的异常
## 1.15.0
- 优化预发布、灰度
- 网关动态修改请求参数
- 支持swagger-bootstrap插件
- 优化admin服务列表显示
- 优化文档刷新逻辑
- 新增测试all in one
- 修复中文乱码问题
## 1.14.0
- 支持预发布、灰度发布环境
## 1.13.7
- 修复修复context-path识别问题
## 1.13.6
- 修复@RequestBody不能绑定问题
## 1.13.5
- 修复postJson下version获取不到问题
## 1.13.4
- 修复admin服务列表最后更新时间不显示问题
- 优化上传路由配置逻辑
- 微服务可获得access_token, notify_url参数
## 1.13.3
- 优化参数绑定
## 1.13.2
- 修复json方式请求获取不到参数问题
- 微服务端新增获取开放平台请求参数
## 1.13.1
- 支持json方式请求application/json
- 支持传统web服务开发见文档`传统web开发`
## 1.13.0
- 新增IP黑名单
## 1.12.4
- 优化属性文件配置
- 新增sleuth接入文档
- admin的isv列表新增备注字段
## 1.12.3
- 修复删除zk节点导致的BUG
## 1.12.2
- 沙盒支持文件上传
## 1.12.1
- 修复重启网关路由状态重置BUG
- 优化SpringCloudGateway
## 1.12.0
- admin后台新增角色管理
- 支持nacos作为注册中心
## 1.11.0
- 秘钥管理改造
- 服务端返回sign
- 新增SDK返回sign处理
- 新增沙箱环境
## 1.10.0
- 新增监控日志
## 1.9.0
- 改造限流
- 增强参数绑定
## 1.8.0
- 支持文件上传
## 1.7.2
- 修复微服务参数绑定BUG
- Admin新增vue界面
## 1.7.1
- 支持接口名版本号放在url后面
## 1.7.0
- 可自定义数据节点名称
## 1.6.0
- 新增应用授权
## 1.5.0
- admin新增signType字段
- 修复easyopen接入无法访问BUG
## 1.4.0
- 新增文档分组显示
- 支持easyopen文档注解
- BUG修复
## 1.3.0
- 新增接口限流功能 [doc](http://durcframework.gitee.io/sop/#/files/10092_%E6%8E%A5%E5%8F%A3%E9%99%90%E6%B5%81?t=1555378655699)
- 新增文档整合功能 [doc](http://durcframework.gitee.io/sop/#/files/10041_%E7%BC%96%E5%86%99%E6%96%87%E6%A1%A3?t=1555378655698)
- 新增springmvc项目接入demo
## 1.2.0
- SOP Admin新增用户登录
- 新增基础SDK(Java,C#) [doc](http://durcframework.gitee.io/sop/#/files/10095_SDK%E5%BC%80%E5%8F%91?t=1554693919597)
## 1.1.0
- 新增ISV管理 [doc](http://durcframework.gitee.io/sop/#/files/10085_ISV%E7%AE%A1%E7%90%86?t=1554123435621)
- 新增接口授权 [doc](http://durcframework.gitee.io/sop/#/files/10090_%E8%B7%AF%E7%94%B1%E6%8E%88%E6%9D%83?t=1554123435621)
## 1.0.0
- 第一次发布
全面重构,欢迎体验:[文档](https://www.yuque.com/u1604442/sop)

176
checkstyle.xml Executable file
View File

@@ -0,0 +1,176 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<!-- 文件长度不超过1500行 -->
<module name="FileLength">
<property name="max" value="2500"/>
</module>
<!-- 长度检查 -->
<!-- 每行不超过200个字符 -->
<module name="LineLength">
<property name="max" value="400"/>
</module>
<module name="SuppressWarningsFilter" />
<!-- 每个java文件一个语法树 -->
<module name="TreeWalker">
<module name="SuppressWarningsHolder" />
<!-- import检查-->
<!-- 检查是否从非法的包中导入了类 -->
<module name="IllegalImport"/>
<!-- 检查是否导入了多余的包 -->
<module name="RedundantImport"/>
<!-- 没用的import检查比如1.没有被用到2.重复的3.import java.lang的4.import 与该类在同一个package的 -->
<module name="UnusedImports"/>
<!-- 注释检查 -->
<!-- 检查构造函数的javadoc -->
<module name="JavadocType">
<property name="allowUnknownTags" value="true"/>
<message key="javadoc.missing" value="类注释缺少Javadoc注释。"/>
</module>
<!-- 命名检查 -->
<!-- 局部的final变量包括catch中的参数的检查 -->
<module name="LocalFinalVariableName"/>
<!-- 局部的非final型的变量包括catch中的参数的检查 -->
<module name="LocalVariableName"/>
<!-- 包名的检查(只允许小写字母),默认^[a-z]+(\.[a-zA-Z_][a-zA-Z_0-9_]*)*$ -->
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
<message key="name.invalidPattern" value="包名 ''{0}'' 要符合 ''{1}''格式."/>
</module>
<!-- 仅仅是static型的变量不包括static final型的检查 -->
<module name="StaticVariableName"/>
<!-- Class或Interface名检查默认^[A-Z][a-zA-Z0-9]*$-->
<module name="TypeName">
<property name="severity" value="warning"/>
<message key="name.invalidPattern" value="名称 ''{0}'' 要符合 ''{1}''格式."/>
</module>
<!-- 非static型变量的检查
<module name="MemberName"/>
-->
<!-- 方法名的检查 -->
<module name="MethodName"/>
<!-- 方法的参数名
<module name="ParameterName "/>
-->
<!-- 常量名的检查(只允许大写),默认^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$ -->
<module name="ConstantName"/>
<!-- 定义检查 -->
<!-- 检查数组类型定义的样式 -->
<module name="ArrayTypeStyle"/>
<!-- 检查long型定义是否有大写的“L” -->
<module name="UpperEll"/>
<!-- 方法不超过100行 -->
<module name="MethodLength">
<property name="tokens" value="METHOD_DEF"/>
<property name="max" value="300"/>
</module>
<!-- 方法的参数个数不超过8个。 并且不对构造方法进行检查-->
<module name="ParameterNumber">
<property name="max" value="8"/>
<property name="ignoreOverriddenMethods" value="true"/>
<property name="tokens" value="METHOD_DEF"/>
</module>
<!-- 空格检查-->
<!-- 方法名后跟左圆括号"(" -->
<module name="MethodParamPad"/>
<!-- 在类型转换时,不允许左圆括号右边有空格,也不允许与右圆括号左边有空格 -->
<module name="TypecastParenPad"/>
<!-- 检查在某个特定关键字之后应保留空格 -->
<module name="NoWhitespaceAfter"/>
<!-- 检查在某个特定关键字之前应保留空格 -->
<module name="NoWhitespaceBefore"/>
<!-- 圆括号空白 -->
<module name="ParenPad"/>
<!-- 检查分隔符是否在空白之后 -->
<module name="WhitespaceAfter"/>
<!-- 检查分隔符周围是否有空白 -->
<module name="WhitespaceAround"/>
<!-- 修饰符检查 -->
<!-- 检查修饰符的顺序是否遵照java语言规范默认public、protected、private、abstract、static、final、transient、volatile、synchronized、native、strictfp -->
<module name="ModifierOrder"/>
<!-- 检查接口和annotation中是否有多余修饰符如接口方法不必使用public -->
<module name="RedundantModifier"/>
<!-- 代码块检查 -->
<!-- 检查是否有嵌套代码块 -->
<module name="AvoidNestedBlocks"/>
<!-- 检查是否有空代码块 -->
<module name="EmptyBlock"/>
<!-- 检查左大括号位置 -->
<module name="LeftCurly"/>
<!-- 检查代码块是否缺失{} -->
<module name="NeedBraces"/>
<!-- 检查右大括号位置 -->
<module name="RightCurly"/>
<!-- 代码检查 -->
<!-- 检查空的代码段 -->
<module name="EmptyStatement"/>
<!-- 检查在重写了equals方法后是否重写了hashCode方法 -->
<module name="EqualsHashCode"/>
<!-- 检查局部变量或参数是否隐藏了类中的变量 -->
<module name="HiddenField">
<property name="tokens" value="VARIABLE_DEF"/>
</module>
<!-- 检查子表达式中是否有赋值操作 -->
<module name="InnerAssignment"/>
<!-- 检查switch语句是否有default -->
<module name="MissingSwitchDefault"/>
<!-- 检查是否有过度复杂的布尔表达式 -->
<module name="SimplifyBooleanExpression"/>
<!-- 检查是否有过于复杂的布尔返回代码段 -->
<module name="SimplifyBooleanReturn"/>
<!-- 类设计检查 -->
<!-- 检查类是否为扩展设计l -->
<!-- 检查只有private构造函数的类是否声明为final
<module name="FinalClass"/>
-->
<!-- 检查接口是否仅定义类型 -->
<module name="InterfaceIsType"/>
<!-- 检查类成员的可见度 检查类成员的可见性。只有static final 成员是public的
除非在本检查的protectedAllowed和packagedAllowed属性中进行了设置-->
<module name="VisibilityModifier">
<property name="packageAllowed" value="true"/>
<property name="protectedAllowed" value="true"/>
</module>
<!-- 语法 -->
<!-- String的比较不能用!= 和 == -->
<module name="StringLiteralEquality"/>
<!-- 限制for循环最多嵌套2层 -->
<module name="NestedForDepth">
<property name="max" value="2"/>
</module>
<!-- if最多嵌套3层 -->
<module name="NestedIfDepth">
<property name="max" value="10"/>
</module>
<!-- 检查未被注释的main方法,排除以Appllication结尾命名的类 -->
<module name="UncommentedMain">
<property name="excludedClasses" value=".*[Application,Test]$"/>
</module>
<!-- 禁止使用System.out.println -->
<module name="Regexp">
<property name="format" value="System\.out\.println"/>
<property name="illegalPattern" value="true"/>
</module>
<!--try catch 异常处理数量 3-->
<module name="NestedTryDepth ">
<property name="max" value="3"/>
</module>
<!-- clone方法必须调用了super.clone() -->
<module name="SuperClone"/>
<!-- finalize 必须调用了super.finalize() -->
<module name="SuperFinalize"/>
</module>
</module>

27
doc/.gitignore vendored
View File

@@ -1,27 +0,0 @@
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
*.png
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/

View File

@@ -1,11 +0,0 @@
# 开发文档
文档放在docs/files下写完文档记得执行下`SidebarTest.main()`方法
配合gitee pages服务使用gitee pages服务指定docs目录。
## 本地查看开发文档
- 前提先安装好npm[npm安装教程](https://blog.csdn.net/zhangwenwu2/article/details/52778521)。建议使用淘宝镜像。
- 安装docsify执行npm命令`npm i docsify-cli -g --registry=https://registry.npm.taobao.org`
- cd到当前目录运行命令`docsify serve docs`,然后访问:`http://localhost:3000`即可查看。

View File

@@ -1,3 +0,0 @@
# SOP开发文档
Git地址[SOP](https://gitee.com/durcframework/SOP)

View File

@@ -1 +0,0 @@
include: [_navbar,_sidebar]

View File

@@ -1,12 +0,0 @@
![logo](_media/icon.svg)
# docsify <small>4.6.10</small>
> A magical documentation site generator.
* Simple and lightweight (~19kB gzipped)
* No statically built html files
* Multiple themes
[GitHub](https://github.com/QingWei-Li/docsify/)
[Get Started](#docsify)

View File

@@ -1,3 +0,0 @@
- 关于
- [帮助](/zh-cn/)
- [API](/)

View File

@@ -1,36 +0,0 @@
* [首页](/?t=1616211903021)
* 开发文档
* [快速体验](files/10010_快速体验.md?t=1616211903027)
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1616211903048)
* [新增接口](files/10020_新增接口.md?t=1616211903048)
* [开发流程](files/10021_开发流程.md?t=1616211903048)
* [用户注册](files/10022_用户注册.md?t=1616211903049)
* [业务参数校验](files/10030_业务参数校验.md?t=1616211903049)
* [错误处理](files/10040_错误处理.md?t=1616211903049)
* [编写文档](files/10041_编写文档.md?t=1616211903049)
* [接口交互详解](files/10050_接口交互详解.md?t=1616211903049)
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1616211903049)
* [ISV管理](files/10085_ISV管理.md?t=1616211903050)
* [自定义返回结果](files/10087_自定义返回结果.md?t=1616211903050)
* [自定义过滤器](files/10088_自定义过滤器.md?t=1616211903050)
* [自定义校验token](files/10089_自定义校验token.md?t=1616211903050)
* [网关拦截器](files/10090_网关拦截器.md?t=1616211903050)
* [路由授权](files/10091_路由授权.md?t=1616211903050)
* [接口限流](files/10092_接口限流.md?t=1616211903051)
* [路由监控](files/10093_路由监控.md?t=1616211903051)
* [SDK开发](files/10095_SDK开发.md?t=1616211903051)
* [应用授权](files/10097_应用授权.md?t=1616211903051)
* [提供restful接口](files/10100_提供restful接口.md?t=1616211903051)
* [文件上传](files/10104_文件上传.md?t=1616211903051)
* [配置Sleuth链路追踪](files/10109_配置Sleuth链路追踪.md?t=1616211903052)
* [预发布灰度发布](files/10110_预发布灰度发布.md?t=1616211903052)
* [动态修改请求参数](files/10111_动态修改请求参数.md?t=1616211903052)
* [使用eureka](files/10112_使用eureka.md?t=1616211903052)
* [超时设置](files/10113_超时设置.md?t=1616211903052)
* 原理分析
* [网关性能测试](files/90001_网关性能测试.md?t=1616211903052)
* [原理分析之如何存储路由](files/90011_原理分析之如何存储路由.md?t=1616211903052)
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1616211903052)
* [原理分析之文档归纳](files/90013_原理分析之文档归纳.md?t=1616211903052)
* [原理分析之预发布灰度发布](files/90014_原理分析之预发布灰度发布.md?t=1616211903052)
* [常见问题](files/90100_常见问题.md?t=1616211903053)

View File

@@ -1,52 +0,0 @@
# 快速体验
## 方式1
> 运行环境JDK8Maven3[Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html)Mysql
- 安装并启动Nacos[安装教程](https://nacos.io/zh-cn/docs/quick-start.html)
- 执行Mysql脚本`sop.sql`(Mysql版本5.6+)5.6以下运行`sop-mysql5.6以下版本.sql`
- IDE安装lombok插件然后打开项目(IDEA下可以打开根pom.xml然后open as project)
- 启动网关打开sop-gateway下的`application-dev.properties`
1. 修改数据库`username/password`
2. 指定nacos地址如果nacos安装在本机则不用改
3. 运行`SopGatewayApplication.java`
- 启动微服务:打开`sop-example/sop-story`下的`application-dev.properties`文件
1. 指定nacos地址如果nacos安装在本机则不用改
2. 运行`SopStoryApplication.java`
- 找到sop-test运行`com.gitee.sop.test.AlipayClientPostTest.testGet`进行接口调用测试
## 方式2docker
> 前提安装好docker
- 安装并启动Nacos[安装教程](https://nacos.io/zh-cn/docs/quick-start.html)
- 执行Mysql脚本`sop.sql`(Mysql版本5.6+)5.6以下运行`sop-mysql5.6以下版本.sql`
- 打开`docker-entrypoint.sh`修改mysqlnacos配置
- 执行`docker-build.sh`
- 找到sop-test运行`com.gitee.sop.test.AlipayClientPostTest.testGet`进行接口调用测试
> - admin地址http://ip:8082 登录账号admin/123456
> - 文档地址http://ip:8083/
## 使用admin
- 找到`sop-admin/sop-admin-server`工程打开sop-admin-server下的`application-dev.properties`,修改相关配置
- 运行`SopAdminServerApplication.java`
- 访问:`http://localhost:8082`
登录账号admin/123456
## 启动门户网站
见:`sop-website/sop-portal/README.md`
## 启动文档中心/ISV后台
文档中心代码在sop-website工程中
- 确保注册中心、网关、微服务正常启动
- 修改sop-website-server下的application-dev.properties相关配置
- 运行WebsiteServerApplication.java
- 访问http://localhost:8083

View File

@@ -1,217 +0,0 @@
# 项目接入到SOP
以springboot项目为例完整项目可参考sop-example下的sop-story
- pom.xml添加版本配置
```xml
<!-- springboot 版本-->
<spring-boot.version>2.6.15</spring-boot.version>
<!-- spring cloud 版本 -->
<spring-cloud.version>2021.0.5</spring-cloud.version>
<!-- spring cloud alibaba 版本 -->
<!-- 具体版本对应关系见https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
```
- pom.xml添加`<dependencyManagement>`控制版本
```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
- pom.xml依赖sop-service-common和nacos服务发现
```xml
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-common</artifactId>
<version>最新版本</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
```
- application.properties配置文件添加
```properties
server.port=2222
spring.application.name=story-service
# nacos注册中心
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
```
- 在springboot启动类上添加`@EnableDiscoveryClient`
- 新增一个配置类,继承`AlipayServiceConfiguration.java`,内容为空
```java
@Configuration
public class OpenServiceConfig extends AlipayServiceConfiguration {
}
```
- 全局异常处理
在微服务项目的全局异常处理中添加一句:`ExceptionHolder.hold(request, response, exception);`
```java
@ExceptionHandler(Exception.class)
@ResponseBody
public Object exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
...
// 在返回前加这一句
ExceptionHolder.hold(request, response, exception);
...
return ..;
}
```
如果没有配置全局异常,可参考下面配置
```java
@ControllerAdvice
@Slf4j
public class StoryGlobalExceptionHandler {
/**
* 捕获手动抛出的异常
*
* @param request request
* @param response response
* @param exception 异常信息
* @return 返回提示信息
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Object exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception exception) {
// 在返回前加这一句
ExceptionHolder.hold(request, response, exception);
// 下面可以实现自己的全局异常处理
return new ErrorResult(500, exception.getMessage());
}
@Data
@AllArgsConstructor
public static class ErrorResult {
private int code;
private String msg;
}
}
```
到此准备工作就完成了,接下来可前往`新增接口`查看如何新增接口。
## 非Java项目接入
如果是非Java项目接入如php需要做到以下几点
> 1. 提供路由配置接口
> 2. 服务注册到nacos并在nacos的metadata中指定接口路径metadata的key为`sop.routes.path`
- 提供路由配置接口
php应用提供的接口需要返回如下json内容
假设请求的接口为:`http://open.xxx.com/get_routes`
```json
{
"serviceId": "goods-service",
"createTime": 1568603471646,
"updateTime": 1568603471646,
"description": null,
"routeDefinitionList": [
{
"id": "goods.list1.0",
"name": "goods.list",
"version": "1.0",
"uri": "lb://goods-service",
"path": "/goods/list_goods",
"order": 0,
"ignoreValidate": 0,
"status": 1,
"mergeResult": 1,
"permission": 0
},
...
]
}
```
json参数说明
|参数名|是否必填|说明|
|:----|:----|:----|
|serviceId |是|serviceId服务id |
|createTime |是|创建时间Unix timestamp毫秒 |
|updateTime |是|修改时间Unix timestamp毫秒 |
|description|否|描述|
|routeDefinitionList元素参数说明|是|路由配置routeDefinitionList元素参数说明|
routeDefinitionList元素参数说明
|参数名|是否必填|说明|
|:----|:----|:----|
|id |是|路由id全局唯一格式接口名+版本号 |
|name|是|接口名称|
|version|是|版本号|
|uri|是|格式lb:// + serviceIdlb://goods-service|
|path|是|接口path填端口号后面的path如你的接口为`http://open.domain.com:8080/goods/list_goods`,填:`/goods/list_goods`|
|order|是|固定填0|
|ignoreValidate|是|忽略签名验证10否|
|status|是|启用状态1启用2禁用|
|mergeResult|是|是否统一返回结果10否|
|permission|是|是否需要权限访问10否|
- 服务注册到nacos
可前往nacos官网参考[open-api](https://nacos.io/zh-cn/docs/open-api.html)使用nacos提供的接口完成服务注册
- 在nacos的metadata中指定接口路径
伪代码如下:
```java
Instance instance = new Instance();
instance.setServiceName("goods-service");
instance.setIp("192.168.0.11");
instance.setPort(8080);
// 在nacos的metadata中指定接口路径
instance.getMetadata().put("sop.routes.path", "http://open.xxx.com/get_routes");
namingService.registerInstance(serviceId, instance);
```
完成以上步骤后php服务注册到nacos网关会触发监听事件获取新注册的服务然后会向你的服务拉取路由配置。

View File

@@ -1,147 +0,0 @@
# 新增接口
假设要对下面这个接口提供开放能力。
```java
@RestController
public class StoryDemoController {
@RequestMapping("/story/get")
public StoryResult getStory() {
StoryResult result = new StoryResult();
result.setId(1L);
result.setName("海底小纵队");
return result;
}
}
```
只需要在方法上新增一个`@Open`注解,指定接口名即可
```java
// 添加一个@Open注解
@Open("story.demo.get")
@RequestMapping("/story/get")
public StoryResult getStory() {
StoryResult result = new StoryResult();
result.setId(1L);
result.setName("海底小纵队");
return result;
}
```
如果要加上版本号,指定`version`参数:`@Open(value = "story.demo.get", version = "2.0")`
- 重启服务,这样接口就可以使用了。
## 绑定业务参数
网关校验通过后,请求参数会传递到微服务上来,完整的参数如下所示:
```
请求参数charset=utf-8&biz_content={"goods_remark":"iphone6"}&method=goods.add&format=json&app_id=2019032617262200001&sign_type=RSA2&version=1.0&timestamp=2019-04-29 19:18:38
```
其中biz_content部分是我们想要的在方法上申明一个对象对应biz_content中的内容即可完成参数绑定并且对参数进行JSR-303校验。
**注意:接口方法必须有且只有一个对象参数,如果申明多个会出现参数绑定失败**
```java
@Open("goods.add")
@RequestMapping("/goods/add")
public Object addGoods(GoodsParam param) {
return param;
}
@Data
public class GoodsParam {
@NotEmpty(message = "不能为空") // 支持JSR-303校验
private String goods_remark;
}
```
一般情况下,只需要获取业务参数即可,如果想要获取更多的参数,可在后面跟一个`HttpServletRequest`对象。
```java
@Open("goods.add")
@RequestMapping("/goods/add")
public Object addGoods(GoodsParam param, HttpServletRequest request) {
System.out.println(request.getParameter("method"));
return param;
}
```
## 接口命名
接口命名没有做强制要求,但我们还是推荐按照下面的方式进行命名:
接口名的命名规则为:`服务模块.业务模块.功能模块.行为`,如:
- mini.user.userinfo.get 小程序服务.用户模块.用户信息.获取
- member.register.total.get 会员服务.注册模块.注册总数.获取
如果觉得命名规则有点长可以精简为:`服务模块.功能模块.行为`,如`member.usercount.get`,前提是确保前缀要有所区分,不和其它服务冲突。
## 测试接口
- 在sop-test工程下新建一个测试用例`StoryDemoTest`继承TestBase
```java
public class StoryDemoTest extends TestBase {
String url = "http://localhost:8081";
String appId = "2019032617262200001";
// 私钥
String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=";
@Test
public void testDemo() throws Exception {
// 公共请求参数
Map<String, String> params = new HashMap<String, String>();
params.put("app_id", appId);
// 这里对应@Open.value属性
params.put("method", "story.demo.get");
params.put("format", "json");
params.put("charset", "utf-8");
params.put("sign_type", "RSA2");
params.put("timestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
// 这里对应@Open.version属性
params.put("version", "1.0");
// 业务参数
Map<String, String> bizContent = new HashMap<>();
params.put("biz_content", JSON.toJSONString(bizContent));
System.out.println("----------- 请求信息 -----------");
System.out.println("请求参数:" + buildParamQuery(params));
System.out.println("商户秘钥:" + privateKey);
String content = AlipaySignature.getSignContent(params);
System.out.println("待签名内容:" + content);
String sign = AlipaySignature.rsa256Sign(content, privateKey, "utf-8");
System.out.println("签名(sign)" + sign);
params.put("sign", sign);
System.out.println("----------- 返回结果 -----------");
String responseData = post(url, params);// 发送请求
System.out.println(responseData);
}
}
```
- 请求成功后,控制台会打印:
```
----------- 请求信息 -----------
请求参数charset=utf-8&biz_content={}&method=story.demo.get&format=json&app_id=alipay_test&sign_type=RSA2&version=1.0&timestamp=2019-03-23 15:41:22
商户秘钥MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=
待签名内容app_id=alipay_test&biz_content={}&charset=utf-8&format=json&method=story.demo.get&sign_type=RSA2&timestamp=2019-03-23 15:41:22&version=1.0
签名(sign)YMbxTPdovi6htcn1K3USTS6/Tbg6MOAMigG6x/kG0kQFCYH8ljvxXzcY86UT056nUG3OXxnj0xkw07eV6E03HMlu7bn3/jrT3PCcV3YguhA92aWz720x2xJWdfXY13OUPS9VOCC9zIVxu6EBD+PoZ7ojYChYvOfCR5I8bR/oOc0ZLjK63PWTBdf0eFS4sybXzRf81uNLMROsMhmBDDy0Fhml3ml77qzWBIpsmq5ECZ+89rMPbkNhAUcnFAe7ik7xZIL6WcUhAOhKVa8ZQK1GMjoGnAbGRed1FbuOHZGubgffg4/vMqrY10Bcy6h9jt/zK5w9L3HVgK3aPgQlfP16Gg==
----------- 返回结果 -----------
{"story_demo_get_response":{"msg":"Success","code":"10000","name":"白雪公主","id":1},"sign":"YMbxTPdovi6htcn1K3USTS6/Tbg6MOAMigG6x/kG0kQFCYH8ljvxXzcY86UT056nUG3OXxnj0xkw07eV6E03HMlu7bn3/jrT3PCcV3YguhA92aWz720x2xJWdfXY13OUPS9VOCC9zIVxu6EBD+PoZ7ojYChYvOfCR5I8bR/oOc0ZLjK63PWTBdf0eFS4sybXzRf81uNLMROsMhmBDDy0Fhml3ml77qzWBIpsmq5ECZ+89rMPbkNhAUcnFAe7ik7xZIL6WcUhAOhKVa8ZQK1GMjoGnAbGRed1FbuOHZGubgffg4/vMqrY10Bcy6h9jt/zK5w9L3HVgK3aPgQlfP16Gg=="}
```

View File

@@ -1,20 +0,0 @@
# 开发流程
如果您打算使用SOP做开放平台开发流程大致如下
- cd到/SOP/sop-common目录执行命令`mvn clean deploy`把jar上传到maven私服如果没有maven私服可以打包到本地`mvn clean install`
- 打包`sop-gateway`(网关)、`sop-admin`(后台管理)、`sop-website`(文档),部署到服务器上
以上服务是固定的,启动一次即可,后续不用做改动。
- 你的项目接入到SOP参考[项目接入到SOP](10011_项目接入到SOP.md)在微服务端开发接口编写swagger注解文档
- 接口开发完成,启动微服务,注册到注册中心。
- 【可选】编写sdk`SDK管理`页发布SDK提供下载地址
## ISV对接流程
假设接口开发完毕开始对接ISV大致步骤如下
1. ISV访问门户网站注册账号并登陆
2. 上传应用公钥
3. 如果有SDK下载SDK进行接口调用

View File

@@ -1,11 +0,0 @@
# 用户ISV注册
新增ISV有两种方式
- 方式1
启动sop-admin在admin后台`ISV管理添加`
- 方式2
启动`sop-website-server`,用户访问自主注册

View File

@@ -1,88 +0,0 @@
# 业务参数校验
业务参数校验采用JSR-303方式关于JSR-303介绍可以参考这篇博文[JSR 303 - Bean Validation 介绍及最佳实践](https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/)
在参数中使用注解即可框架会自动进行验证。如下面一个添加商品接口它的参数是GoodsParam
```java
@Open("goods.add")
@RequestMapping("/goods/add")
public void addGoods(GoodsParam param) {
...
}
```
在GoodsParam中添加JSR-303注解:
```java
@Data
public class GoodsParam {
@NotEmpty(message = "商品名称不能为空")
private String goods_name;
}
```
如果不传商品名称则返回
```
{"goods_add_response":{"msg":"Success","code":"10000","sub_msg":"商品名称不能为空","sub_code":"isv.invalid-parameter"},"sign":"Eh3Z5CxDCHsb4MyYFVxsPSmBpwVi1LISJdOkrzglxXoqG7RVyEOt4ef1kNpznUvMI3FDQU1suR7Rsmx6NjGdEVS6NSH2Kt0d8TFBRpLhWz8hApnxOtgzqMqbYeMuJie7X5gF6m8hTnvuuxF21IrkixMe+lyBcXw7dk0C3w1SwdEZkHQ+xC+M4bLqAZt5/3kl79/FWSMFJWHiZmg5YeEi8e8XhYCNcz+xlJRJL0x2Y87fFxqSY0UYWNxbQHgdVI8xRfn1n31nzkcLxiAtTh4LPtNRrG7w7absK/C1Oi/vczuBlFeq2EWUsYVWOVpKiJifUwvYVUUsztSLElzplzOjbg=="}
```
- 校验顺序
如果存在多个注解可以指定groups来控制校验顺序如下代码所示
```java
@NotBlank(message = "NotBlank", groups = Group1.class)
// 优先校验Group2
// 可交换下面Group2,Group3看下校验顺序
@Length(min = 2, max = 20, message = "length must 10~20", groups = Group2.class)
@Pattern(regexp = "[a-zA-Z]*", message = "name must letters", groups = Group3.class)
private String name;
```
优先校验`@Length`,通过后再校验`@Pattern`
## 参数校验国际化
国际化的配置方式如下:
```java
@NotEmpty(message = "{goods.remark.notNull}")
private String goods_remark;
```
国际化资源文件`bizerror_en.properties`中添加:
```
goods.remark.notNull=The goods_remark can not be null
```
bizerror_zh_CN.properties中添加
```
# 商品备注不能为空
goods.remark.notNull=\u5546\u54c1\u5907\u6ce8\u4e0d\u80fd\u4e3a\u7a7a
```
## 参数校验国际化传参
下面校验商品评论的长度要求大于等于3且小于等于20。数字3和20要填充到国际化资源中去。
```
// 传参的格式:{xxx}=value1,value2...
@Length(min = 3, max = 20, message = "{goods.comment.length}=3,20")
private String goods_comment;
```
bizerror_en.properties:
```
goods.comment.length=The goods_comment length must >= {0} and <= {1}
```
bizerror_zh_CN.properties中添加
```
# 商品评论长度必须在{0}和{1}之间
goods.comment.length=\u5546\u54c1\u8bc4\u8bba\u957f\u5ea6\u5fc5\u987b\u5728{0}\u548c{1}\u4e4b\u95f4
```
这样value1value2会分别填充到{0},{1}中

View File

@@ -1,91 +0,0 @@
# 错误处理
SOP对错误处理已经封装好了简单做法是`throw ServiceException`在最顶层的Controller会做统一处理。例如
```java
if(StringUtils.isEmpty(param.getGoods_name())) {
throw new ServiceException("goods_name不能为null");
}
```
为了保证编码风格的一致性推荐统一使用ServiceException
## i18n国际化
SOP支持国际化消息。通过Request对象中的getLocale()来决定具体返回那种语言客户端通过设置Accept-Language头部来决定返回哪种语言中文是zh英文是en。
SOP通过模块化来管理国际化消息这样做的好处结构清晰维护方便。下面就来讲解如何设置国际化消息。
以story服务为例假设我们要对商品模块进行设置步骤如下
-`resource/i18n/isp`目录下新建goods_error_zh_CN.properties属性文件
属性文件的文件名有规律, **i18n/isp/goods_error** 表示模块路径, **_zh_CN.properties** 表示中文错误消息。如果要使用英文错误,则新建一个`goods_error_en.properties`即可。
- 在goods_error_zh_CN.properties中配置错误信息
```
# 商品名字不能为空
isp.goods_error_100=\u5546\u54C1\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
```
isp.goods_error_为固定前缀100为错误码这两个值后续会用到。
接下来是把属性文件加载到国际化容器当中。
- 添加国际化配置在OpenServiceConfig中的static块中添加代码如下
```java
@Configuration
public class OpenServiceConfig extends AlipayServiceConfiguration {
static {
ServiceConfig.getInstance().getI18nModules().add("i18n/isp/goods_error");
}
}
```
- 新建一个枚举用来定义错误
```java
// 按模块来定义异常消息,团队开发可以分开进行
public enum GoodsErrorEnum {
/** 参数错误 */
NO_GOODS_NAME("100"),
;
private ServiceErrorMeta errorMeta;
StoryErrorEnum(String subCode) {
this.errorMeta = new ServiceErrorMeta("isp.goods_error_", subCode);
}
public ServiceErrorMeta getErrorMeta() {
return errorMeta;
}
}
```
接下来就可以使用了
```java
if (StringUtils.isEmpty(param.getGoods_name())) {
throw GoodsErrorEnum.NO_GOODS_NAME.getErrorMeta().getException();
}
```
### 国际化消息传参
即代码中变量传入到properties文件中去做法是采用{0},{1}占位符。0代表第一个参数1表示第二个参数。
```
# 商品名称太短,不能小于{0}个字
isp.goods_error_101=\u5546\u54C1\u540D\u79F0\u592A\u77ED\uFF0C\u4E0D\u80FD\u5C0F\u4E8E{0}\u4E2A\u5B57
```
```java
if (param.getGoods_name().length() <= 3) {
throw GoodsErrorEnum.LESS_GOODS_NAME_LEN.getErrorMeta().getException(3);
}
```
直接放进getException(Object... params)方法参数中,因为是可变参数,可随意放。

View File

@@ -1,126 +0,0 @@
# 编写文档
作为开放平台必须要提供API文档。
SOP采用微服务架构实现因此文档应该由各个微服务各自实现。难点就是如何统一归纳各个微服务端提供的文档信息并且统一展示。
写完接口后使用swagger注解来定义自己的文档信息。步骤如下
- maven添加swagger
```xml
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.5</version>
</dependency>
```
- 在config中添加swagger配置
```java
@Configuration
public class OpenServiceConfig extends AlipayServiceConfiguration {
/**
* 开启文档本地微服务文档地址http://localhost:2222/doc.html
* http://ip:port/v2/api-docs
*/
@Configuration
@EnableSwagger2
public static class Swagger2 extends SwaggerSupport {
@Override
protected String getDocTitle() {
return "故事API";
}
}
}
```
其中`getDocTitle()`返回文档模块名,不能和其它微服务重复。比如订单服务返回:`订单API`;库存服务返回:`库存API`
- 编写swagger注解
分别在请求参数和返回结果类中编写`@ApiModelProperty`
```java
// 请求参数
@Data
public class StoryParam {
@ApiModelProperty(value = "故事ID", example = "111")
private int id;
@ApiModelProperty(value = "故事名称", required = true, example = "白雪公主")
private String name;
}
// 返回结果
@Data
public class StoryResult {
@ApiModelProperty(value = "故事ID", example = "1")
private Long id;
@ApiModelProperty(value = "故事名称", example = "海底小纵队")
private String name;
@ApiModelProperty(value = "创建时间", example = "2019-04-14 19:02:12")
private Date gmt_create;
}
```
- 在接口方法上编写`@ApiOperation`注解
```java
/**
* 参数绑定
*
* @param story 对应biz_content中的内容并自动JSR-303校验
* @return
*/
@ApiOperation(value = "获取故事信息", notes = "说明接口的详细信息,介绍,用途,注意事项等。")
@Open(value = "alipay.story.find", bizCode = {
// 定义业务错误码,用于文档显示
@BizCode(code = "100001", msg = "姓名错误", solution = "填写正确的姓名"),
@BizCode(code = "100002", msg = "备注错误", solution = "填写正确备注"),
})
public StoryResult getStory2(StoryParam story) {
log.info("获取故事信息参数, story: {}", story);
// 获取其它参数
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
String app_id = openContext.getAppId();
StoryResult result = new StoryResult();
result.setName("白雪公主, app_id:" + app_id);
result.setGmt_create(new Date());
return result;
}
```
其中`value`属性填接口名称,简明扼要。`notes`填写接口的详细信息,介绍,用途,注意事项等。
## 查看文档
- 确保注册中心、网关、微服务正常启动
- 运行WebsiteServerApplication.java
- 访问http://localhost:8083
效果图如下
![预览](images/10041_1.png "10041_1.png")
## 注解对应关系
swagger注解和文档界面显示关系如下图所示
![预览](images/10041_2.png "10041_2.png")
![预览](images/10041_3.png "10041_3.png")
![预览](images/10041_4.png "10041_4.png")

View File

@@ -1,61 +0,0 @@
# 接口交互详解
开放平台所提供的接口有几十个到几百个不等,同样支持的服务也是多个的。就拿[支付宝开放平台](https://docs.open.alipay.com/api)来说
它所提供的服务有支付服务、会员服务、店铺服务、芝麻信用服务等。相信这些服务接口肯定不是写在同一个项目中但是它的接口地址只有一个https://openapi.alipay.com/gateway.do
从地址信息中可以看到,这是一个网关服务。也就是说,网关是所有请求的入口,然后通过请求分发的方式,把请求路由到具体某个服务中去。
虽然支付宝开放平台的实现方式我们不得而知,但是这种思路是可行的。
SOP也是采用这种方式实现大致步骤如下
- 每个服务注册到nacos
- 网关启动时同样注册到nacos然后从各服务中拉取路由信息
- 网关收到客户端请求后,先进行签名校验,通过之后根据接口信息找到对应的服务,然后进行路由
- 网关对返回结果进行处理(或不处理),返回给客户端。
如何通过接口参数找到对应的服务呢?
在网关定义一个`Map<String, RouteInfo> routeMap = ...`key为接口名+版本号。
网关启动时从各微服务中获取路由信息并保存到routeMap中
```java
routeMap = requestFormServices();
```
接口请求进来后,根据`方法名+版本号`获取路由信息,然后进行路由转发。
```java
String method = request.getParameter("method");
String version = request.getParameter("version");
RouteInfo routeInfo = routeMap.get(method + version);
doRoute(routeInfo);
```
因为nacos需要拉取各个微服务的路由信息接口名有可能会冲突因此需要确保接口名唯一`method`全局唯一。
我们推荐接口名的命名规则应该是:`服务模块.业务模块.功能模块.行为`,如:
mini.user.userinfo.get 小程序服务.用户模块.用户信息.获取
member.register.total.get 会员服务.注册模块.注册总数.获取
如果觉得命名规则有点长可以精简为:`服务模块.功能模块.行为`,如`member.usercount.get`,前提是确保前缀要有所区分,不和其它服务冲突。
得益于Spring Cloud的注册中心和和网关功能我们能很方便的进行接口路由并且还能实现LoadBalance不需要自己再去实现。
整个SOP的架构如下图所示
![架构图](https://images.gitee.com/uploads/images/2019/1227/145216_c9b45109_332975.png "sop2.png")
- 完整请求路线
```
客户端生成签名串 → 客户端发送请求 →【网关签名校验 → 权限校验 → 限流处理 → 路由转发】→ {微服务端业务参数校验 → 处理业务逻辑 → 微服务端返回结果}
客户端业务处理 ← 客户端验证服务端签名 ← 客户端收到结果 ← -------------【网关返回最终结果 ← 生成服务端签名 ← 网关处理结果】← 结果返回到网关
【】:表示网关处理
{}:表示微服务端处理
```

View File

@@ -1,41 +0,0 @@
# 使用签名校验工具
## 生成公私钥
SOP默认签名算法仿照的是支付宝开放平台因此我们可以使用支付宝开放平台提供的密钥生成工具[下载地址](https://docs.open.alipay.com/291/105971/)
工具下载完后,运行工具
- 秘钥格式选择PKCS8(JAVA适用)
- 秘钥长度2048
然后点击`生成秘钥`,下面文本框会生成,公私钥,如下图所示:
![示例图](https://gw.alipayobjects.com/zos/skylark/6dbc42cc-6b9b-4691-83f1-e7b875e1a602/2018/png/e6b725d0-8257-4a71-b7a0-f5479c9d43d0.png)
sop-admin创建一个新ISV将公私钥放入对应文本框中保存。
接着私钥放入客户端进行调用。参见AlipayClientPostTest类
## 签名校验
验证工具切换到`签名`tab页
例如执行com.gitee.sop.AlipayClientPostTest.testPost()方法,控制台会打印如下信息:
```
----------- 请求信息 -----------
请求参数charset=utf-8&biz_content={"name":"葫芦娃","id":"1"}&method=alipay.story.get&format=json&app_id=2019032617262200001&sign_type=RSA2&version=1.0&timestamp=2019-03-26 17:37:41
商户秘钥MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=
待签名内容app_id=2019032617262200001&biz_content={"name":"葫芦娃","id":"1"}&charset=utf-8&format=json&method=alipay.story.get&sign_type=RSA2&timestamp=2019-03-26 17:37:41&version=1.0
签名(sign)JCZMSFkXSjw/4TokyM9/9shyrMl7KxQGIZDHIm7+Bvl49Z816/iF/xXLYjUiPXWAXYfp+HlEs3VVQp1Kjh4tIKuKX/i1+exNVs+ICcqVGBewPSZwiWHGpZTfEUiYOoPyUL/eoRIj7Mvlaow0sI9uP7NXNo0kxEFjUOMCzZA7eKm/pu2FHRXt4OhgXq2Go30K5a9oCbbMc/2xcQCc2+zwvOgV3o0A6eMyeAXDJW+eQ2KLhtlqPQvbRV+xyfSut7TkwYSEuNXVVQAfN2lwAS3ru9CQIs8Uz7lK1ITkLu80yLapZVL7tS1PdxK0e3QYToCWD43Wtuoow4ZdDwwzir90HQ==
----------- 返回结果 -----------
{"alipay_story_get_response":{"msg":"Success","code":"10000","name":"海底小纵队(alipay.story.get)","id":1},"sign":"JCZMSFkXSjw/4TokyM9/9shyrMl7KxQGIZDHIm7+Bvl49Z816/iF/xXLYjUiPXWAXYfp+HlEs3VVQp1Kjh4tIKuKX/i1+exNVs+ICcqVGBewPSZwiWHGpZTfEUiYOoPyUL/eoRIj7Mvlaow0sI9uP7NXNo0kxEFjUOMCzZA7eKm/pu2FHRXt4OhgXq2Go30K5a9oCbbMc/2xcQCc2+zwvOgV3o0A6eMyeAXDJW+eQ2KLhtlqPQvbRV+xyfSut7TkwYSEuNXVVQAfN2lwAS3ru9CQIs8Uz7lK1ITkLu80yLapZVL7tS1PdxK0e3QYToCWD43Wtuoow4ZdDwwzir90HQ=="}
```
字符集选UTF-8签名方式RSA2
把控制台中的`请求参数``商户秘钥`填入文本框中,然后点击`开始签名`下方会出现待签名内容和sign。
通过比对判断签名过程是否正确。

View File

@@ -1,24 +0,0 @@
# ISV管理
ISV独立软体开发商independent software vendor即接入方或者说接口调用者在SOP中称为ISV。
---
在1.1.0版本中新增了ISV管理功能在sop-admin中ISV管理模块下。功能如下
- 基本信息的增查改
- 设置对应角色
界面如下图所示:
![admin预览](images/10085_1.png "10085_1.png")
## 秘钥管理
点击操作列的`秘钥管理`可对ISV的秘钥进行设置。
- 如果采用淘宝开放平台签名方式,签名方式选择`MD5`,如果采用支付宝开放平台签名方式,选择`RSA`
- 如果对接的开发者使用非Java语言秘钥格式选择`PKCS1`
- 带 ★ 的分配给开发者
![admin预览](images/10085_2.png "10085_2.png")

View File

@@ -1,110 +0,0 @@
# 自定义返回结果
网关默认对业务结果进行合并,然后返回统一的格式。
针对`alipay.story.find`接口,微服务端返回结果如下:
```json
{
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
}
```
网关合并后,最终结果如下
```json
{
"alipay_story_find_response": {
"msg": "Success",
"code": "10000",
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
},
"sign": "xxxxx"
}
```
其中`alipay_story_find_response`是它的数据节点。规则是:
> 将接口名中的点`.`转换成下划线`_`,后面加上`_response`
代码实现如下:
```java
String method = "alipay.story.find";
return method.replace('.', '_') + "_response";
```
详见`DefaultDataNameBuilder.java`
如果要更改数据节点,比如`result`,可使用`CustomDataNameBuilder.java`
```java
@Configuration
public class MyConfig {
static {
...
ApiConfig.getInstance().setDataNameBuilder(new CustomDataNameBuilder());
...
}
}
```
设置后,网关统一的返回结果如下:
```json
{
"result": {
...
},
"sign": "xxxxx"
}
```
此外,构造方法可指定自定义字段名称:`new CustomDataNameBuilder("data");`
设置后,数据节点将变成`data`
```json
{
"data": {
...
},
"sign": "xxxxx"
}
```
**注**网关设置了CustomDataNameBuilder后SDK也要做相应的更改`OpenConfig.dataNameBuilder = new CustomDataNameBuilder();`
## 自定义结果处理
如果想要对微服务结果做更深一步处理,步骤如下:
1. 新增一个类,继承`GatewayResultExecutor.java`,并重写`String mergeResult(T request, String serviceResult)`方法
2. 配置自定义类
```java
@Configuration
public class MyConfig {
static {
...
ApiConfig.getInstance().setGatewayResultExecutor(new MyGatewayResultExecutor());
...
}
}
```
## 不合并结果
如果不希望对结果进行合并可在application.properties中设置`sop.api-config.merge-result=false`
这样,网关最终返回结果即为微服务端的返回结果。

View File

@@ -1,40 +0,0 @@
# 自定义过滤器
演示在网关追加一个header
```java
public class CustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 演示在网关追加header
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange);
String token = apiParam.fetchAccessToken();
ServerWebExchange serverWebExchange = ServerWebExchangeUtil.addHeaders(exchange, httpHeaders -> {
httpHeaders.add("token", token);
});
return chain.filter(serverWebExchange);
}
@Override
public int getOrder() {
// 自定义过滤器可以从0开始
return 0;
}
}
```
使用过滤器在sop-gateway中找到MyConfig添加
```java
@Configuration
public class MyConfig {
...
@Bean
CustomFilter customFilter() {
return new CustomFilter();
}
...
}
```

View File

@@ -1,52 +0,0 @@
# 自定义校验token
`@Open`注解中有一个属性`needToken`用来告诉网关是否校验token
```java
/**
* 是否需要appAuthToken设置为true网关端会校验token是否存在
*/
boolean needToken() default false;
```
使用方式:
```java
@ApiOperation(value="传递token", notes = "传递token")
@Open(value = "story.get.token", needToken = true/* 设置true网关会校验token是否存在 */)
@RequestMapping("token")
public StoryResult token(StoryParam story, HttpServletRequest request) {
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
StoryResult result = new StoryResult();
result.setName("appAuthToken:" + openContext.getAppAuthToken());
return result;
}
```
指定了needToken=true后网关会判断客户端是否传了`app_auth_token`参数,没有传则返回错误信息。
网关默认简单校验参数值是否存在,如果要校验有效性,需要自己实现。
自己实现步骤:
- 设置`ApiConfig中的tokenValidator属性`
`TokenValidator`是一个函数式接口可以直接使用Lambda表达式示例代码如下
```java
@Component
public class MyConfig {
@PostConstruct
public void after() {
ApiConfig.getInstance().setTokenValidator(apiParam -> {
// 获取客户端传递过来的token
String token = apiParam.fetchAccessToken();
return !StringUtils.isBlank(token);
// TODO: 校验token有效性可以从redis中读取
// 返回true表示这个token真实、有效
});
}
}
```

View File

@@ -1,57 +0,0 @@
# 网关拦截器
从3.1.0开始新增了网关拦截器,使用该拦截器可做一些数据统计,日志记录等工作。
使用方法如下:
- 在sop-gateway工程下新增一个类实现`RouteInterceptor`接口,实现接口中的方法。别忘了加`@Component`
```java
@Component
public class MyRouteInterceptor implements RouteInterceptor {
@Override
public void preRoute(RouteInterceptorContext context) {
ApiParam apiParam = context.getApiParam();
System.out.println("请求接口:" + apiParam.fetchNameVersion());
}
@Override
public void afterRoute(RouteInterceptorContext context) {
System.out.println("请求成功,微服务返回结果:" + context.getServiceResult());
}
@Override
public int getOrder() {
return 0;
}
}
```
RouteInterceptor接口方法说明
- `public void preRoute(RouteInterceptorContext context)`
路由转发前执行,在签名验证通过之后会立即执行这个方法。
- `public void afterRoute(RouteInterceptorContext context)`
路由转发完成后,即拿到微服务返回结果后执行这个方法
- `public int getOrder()`
指定拦截执行顺序数字小的优先执行建议从0开始。
- `default boolean match(RouteInterceptorContext context)`
是否匹配返回true执行拦截器默认true
RouteInterceptorContext参数存放了各类参数信息。
参考类:
- `com.gitee.sop.gatewaycommon.interceptor.RouteInterceptor` 拦截器接口
- `com.gitee.sop.gatewaycommon.interceptor.RouteInterceptorContext` 拦截器上下文
- `com.gitee.sop.gatewaycommon.interceptor.MonitorRouteInterceptor` 默认实现的拦截器,用于收集监控数据

View File

@@ -1,25 +0,0 @@
# 路由授权
1.1.0版本新增了路由授权功能采用RBAC权限管理方式实现。
- 每个ISVappKey对应一个或多个角色
- 每个角色分配多个路由权限
接口跟角色相关联ISV拥有哪些角色就具有角色对应的接口访问权限。
假设把路由a,b,c分配给了`VIP角色`那么具有VIP角色的ISV可以访问a,b,c三个路由。
默认情况下接口访问时公开的ISV都能访问。如果要设置某个接口访问权限`@Open`注解中指定permission=true。
如:`@Open(value = "permission.story.get", permission = true)`。这样该接口是需要经过授权给ISV才能访问的。
重启服务后登录admin服务管理-路由列表界面中,`访问权限`列会出现一个点击授权,点击出现授权窗口,勾选对应的角色即可完成授权。
- `点击授权`,进行角色授权
![admin预览](images/10090_1.png "10090_1.png")
- 勾选对应角色,点击保存
![admin预览](images/10090_2.png "10090_2.png")
这里演示的是具有普通权限的ISV能够访问`permission.story.get`接口,运行`PermissionDemoPostTest`测试用例进行验证

View File

@@ -1,80 +0,0 @@
# 接口限流
SOP提供了简单的接口限流策略
- 窗口策略:每秒处理固定数量的请求,超出请求返回错误信息。
- 令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。
如果一个接口设置了窗口策略假设接口每秒可处理5个请求一秒内同时有6个请求进来前5个接口是能够访问的第六个请求将返回错误信息。
如果设置了令牌桶策略桶的容量是5那么每秒中生成5个令牌同一时间有6个请求进来那么前5个能成功拿到令牌继续第六个则等待令牌重新生成了再拿。
默认情况下接口的限流功能是关闭的可在sop admin中配置并开启。功能在`路由管理-->限流管理`下。
## 多维度限流
- 可针对接口进行限流,所有访问该接口的请求都被限流
- 可针对appKey进行限流某个appKey请求过来后对他限流
- 可针对IP进行限流某个IP请求过来后对他限流
此外还可以进行组合
- 可针对接口+appKey进行限流这个appKey调用某个接口比较频繁可以将它限制住
- 可针对接口+IP进行限流某个ip在频繁调用接口可以将它限制住
由于存在组合情况,一个接口可能会配置多个限流规则。在这种情况下会优先取排序值小的那一条,如果排序值一样,则默认取第一条。
假设有下面几个限流规则:
- 接口:`goods.get` 排序值1 每秒可处理请求数10
- 接口:`goods.get` appKeyxxxx 排序值0 每秒可处理请求数5
- 接口:`goods.get` ip172.1.2.2 排序值2 每秒可处理请求数6
客户端调用接口:`http://open.domain.com?method=goods.get&app_id=xxxx`客户端IP为`172.1.2.2`
这种情况下上面三条限流规则都命中了,由于排序值小优先执行,因此第二条规则命中.
具体设置方式可在sop admin中配置功能在`服务管理-->限流管理`下。执行`com.gitee.sop.test.LimitTest`测试用例验证限流情况
![限流配置](images/10092_1.png "10092_1.png")
![限流配置](images/10092_2.png "10092_2.png")
## 分布式限流
默认的限流方式是单机的,如果要部署多台网关实例,需要使用分布式限流
SOP使用redis进行分布式限流只支持窗口策略操作步骤如下
- sop-gateway/pom.xml添加redis依赖
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
```
- sop-gateway下的application-dev.properties文件添加redis配置
```properties
# redis
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
```
-`com.gitee.sop.gateway.config.MyConfig`中添加如下代码:
```java
@Autowired
private RedisTemplate redisTemplate;
@PostConstruct
public void after() {
ApiConfig.getInstance().setLimitManager(new RedisLimitManager(redisTemplate));
...
}
```

View File

@@ -1,27 +0,0 @@
# 路由监控
路由监控功能可以查看各个接口的调用情况,监控信息收集采用拦截器实现,前往【服务管理】-【路由监控】查看
- 后台预览
![监控日志](images/10093_1.png "10093_1.png")
![监控日志](images/10093_2.png "10093_2.png")
- 注意事项
处理完错误后,请及时`标记解决`一个接口默认保存50条错误信息采用LRU机制淘汰老的。标记解决后则会空出一个位置存放新的错误信息。
重复的错误只会存放一条记录,然后累加错误次数,重复错误定义如下:
`instanceId + routeId + errorMsg`,即一个实例 + 路由id + 错误信息确定一个错误
可在网关设置`sop.monitor.error-count-capacity=50`参数调整错误容量
考虑到数据库压力网关收到错误信息后并不会立即保存到数据库而是先保存到内容中然后定时保存到时间默认时间隔为30秒
可通过`sop.monitor.flush-period-seconds=30`调整间隔时间。
相关类:
- com.gitee.sop.gateway.interceptor.MonitorRouteInterceptor

View File

@@ -1,343 +0,0 @@
# 开发SDK
开放平台把接口开发完毕后一般需要开发对应的SDK提供给ISV。SOP提供了一个基础的SDK开发包
开发者可以在此基础上做开发就拿sdk-java来说具体步骤如下
## sdk-java
SDK依赖了三个jar包
- okhttp.jar 用于网络请求
- fastjson.jar 用于json处理
- commons-logging.jar 日志处理
### 接口封装步骤
比如获取故事信息接口
- 接口名alipay.story.find
- 版本号1.0
- 参数name 故事名称
- 返回信息
```json
{
"alipay_story_find_response": {
"msg": "Success",
"code": "10000",
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
},
"sign": "xxxxx"
}
```
针对这个接口,封装步骤如下:
1.在`model`包下新建一个类,定义业务参数
```java
@Data
public class GetStoryModel {
@JSONField(name = "name")
private String name;
}
```
2.在`response`包下新建一个返回类GetStoryResponse继承`BaseResponse`
里面填写返回的字段
```
@Data
public class GetStoryResponse extends BaseResponse {
private Long id;
private String name;
private Date gmtCreate;
}
```
3.在`request`包下新建一个请求类,继承`BaseRequest`
BaseRequest中有个泛型参数`GetStoryResponse`类,表示这个请求对应的返回类。
重写`method()`方法,填接口名。
如果要指定版本号,可重写`version()`方法,或者后续使用`request.setVersion(version)`进行设置
```java
public class GetStoryRequest extends BaseRequest<GetStoryResponse> {
@Override
protected String method() {
return "alipay.story.find";
}
}
```
可重写`getRequestMethod()`方法指定HTTP请求method默认是POST。
```java
@Override
protected RequestMethod getRequestMethod() {
return RequestMethod.GET;
}
```
**建议读请求用GET写请求用POST**
### 使用方式
```java
String url = "http://localhost:8081";
String appId = "2019032617262200001";
String privateKey = "你的私钥";
// 声明一个就行
OpenClient client = new OpenClient(url, appId, privateKey);
// 标准用法
@Test
public void testGet() {
// 创建请求对象
GetStoryRequest request = new GetStoryRequest();
// 请求参数
GetStoryModel model = new GetStoryModel();
model.setName("白雪公主");
request.setBizModel(model);
// 发送请求
GetStoryResponse response = client.execute(request);
if (response.isSuccess()) {
// 返回结果
System.out.println(String.format("成功response:%s\n响应原始内容:%s",
JSON.toJSONString(response), response.getBody()));
} else {
System.out.println("错误subCode:" + response.getSubCode() + ", subMsg:" + response.getSubMsg());
}
}
```
### 使用方式2(懒人版)
如果不想添加Request,Response,Model。可以用这种方式返回body部分是字符串后续自己处理
body对应的是alipay_story_find_response部分
```java
@Test
public void testLazy() {
// 创建请求对象
CommonRequest request = new CommonRequest("alipay.story.find");
// 请求参数
Map<String, Object> bizModel = new HashMap<>();
bizModel.put("name", "白雪公主");
request.setBizModel(bizModel);
// 发送请求
CommonResponse response = client.execute(request);
if (response.isSuccess()) {
// 返回结果body对应的是alipay_story_find_response部分
String body = response.getBody();
JSONObject jsonObject = JSON.parseObject(body);
System.out.println(jsonObject);
} else {
System.out.println(response);
}
}
```
## sdk-.net
### 接口封装步骤
比如获取故事信息接口
- 接口名alipay.story.find
- 版本号1.0
- 参数name 故事名称
- 返回信息
```
{
"alipay_story_find_response": {
"msg": "Success",
"code": "10000",
"name": "白雪公主",
"id": 1,
"gmtCreate": 1554193987378
},
"sign": "xxxxx"
}
```
针对这个接口,封装步骤如下:
1.在`Model`包下新建一个类,定义业务参数
```
namespace SDKCSharp.Model
{
public class GetStoryModel
{
/// <summary>
/// 故事名称
/// </summary>
/// <value>The name.</value>
[JsonProperty("name")]
public string Name { get; set; }
}
}
```
`[JsonProperty("name")]`是Newtonsoft.Json组件中的类用于Json序列化括号中的是参数名称。
类似于Java中的注解`@JSONField(name = "xx")`
2.在`Response`包下新建一个返回类GetStoryResponse继承`BaseResponse`
里面填写返回的字段
```
namespace SDKCSharp.Response
{
public class GetStoryResponse: BaseResponse
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("gmt_create")]
public string GmtCreate { get; set; }
}
}
```
3.在`Request`文件夹下新建一个请求类,继承`BaseRequest`
BaseRequest中有个泛型参数`GetStoryResponse`类,表示这个请求对应的返回类。
重写`GetMethod()`方法,填接口名。
如果要指定版本号,可重写`GetVersion()`方法,或者后续使用`request.Version = version`进行设置
```
namespace SDKCSharp.Request
{
public class GetStoryRequest : BaseRequest<GetStoryResponse>
{
public override string GetMethod()
{
return "alipay.story.find";
}
}
}
```
### 使用方式
```
class MainClass
{
static string url = "http://localhost:8081";
static string appId = "2019032617262200001";
// 平台提供的私钥
static string privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXJv1pQFqWNA/++OYEV7WYXwexZK/J8LY1OWlP9X0T6wHFOvxNKRvMkJ5544SbgsJpVcvRDPrcxmhPbi/sAhdO4x2PiPKIz9Yni2OtYCCeaiE056B+e1O2jXoLeXbfi9fPivJZkxH/tb4xfLkH3bA8ZAQnQsoXA0SguykMRZntF0TndUfvDrLqwhlR8r5iRdZLB6F8o8qXH6UPDfNEnf/K8wX5T4EB1b8x8QJ7Ua4GcIUqeUxGHdQpzNbJdaQvoi06lgccmL+PHzminkFYON7alj1CjDN833j7QMHdPtS9l7B67fOU/p2LAAkPMtoVBfxQt9aFj7B8rEhGCz02iJIBAgMBAAECggEARqOuIpY0v6WtJBfmR3lGIOOokLrhfJrGTLF8CiZMQha+SRJ7/wOLPlsH9SbjPlopyViTXCuYwbzn2tdABigkBHYXxpDV6CJZjzmRZ+FY3S/0POlTFElGojYUJ3CooWiVfyUMhdg5vSuOq0oCny53woFrf32zPHYGiKdvU5Djku1onbDU0Lw8w+5tguuEZ76kZ/lUcccGy5978FFmYpzY/65RHCpvLiLqYyWTtaNT1aQ/9pw4jX9HO9NfdJ9gYFK8r/2f36ZE4hxluAfeOXQfRC/WhPmiw/ReUhxPznG/WgKaa/OaRtAx3inbQ+JuCND7uuKeRe4osP2jLPHPP6AUwQKBgQDUNu3BkLoKaimjGOjCTAwtp71g1oo+k5/uEInAo7lyEwpV0EuUMwLA/HCqUgR4K9pyYV+Oyb8d6f0+Hz0BMD92I2pqlXrD7xV2WzDvyXM3s63NvorRooKcyfd9i6ccMjAyTR2qfLkxv0hlbBbsPHz4BbU63xhTJp3Ghi0/ey/1HQKBgQC2VsgqC6ykfSidZUNLmQZe3J0p/Qf9VLkfrQ+xaHapOs6AzDU2H2osuysqXTLJHsGfrwVaTs00ER2z8ljTJPBUtNtOLrwNRlvgdnzyVAKHfOgDBGwJgiwpeE9voB1oAV/mXqSaUWNnuwlOIhvQEBwekqNyWvhLqC7nCAIhj3yvNQKBgQCqYbeec56LAhWP903Zwcj9VvG7sESqXUhIkUqoOkuIBTWFFIm54QLTA1tJxDQGb98heoCIWf5x/A3xNI98RsqNBX5JON6qNWjb7/dobitti3t99v/ptDp9u8JTMC7penoryLKK0Ty3bkan95Kn9SC42YxaSghzqkt+uvfVQgiNGQKBgGxU6P2aDAt6VNwWosHSe+d2WWXt8IZBhO9d6dn0f7ORvcjmCqNKTNGgrkewMZEuVcliueJquR47IROdY8qmwqcBAN7Vg2K7r7CPlTKAWTRYMJxCT1Hi5gwJb+CZF3+IeYqsJk2NF2s0w5WJTE70k1BSvQsfIzAIDz2yE1oPHvwVAoGAA6e+xQkVH4fMEph55RJIZ5goI4Y76BSvt2N5OKZKd4HtaV+eIhM3SDsVYRLIm9ZquJHMiZQGyUGnsvrKL6AAVNK7eQZCRDk9KQz+0GKOGqku0nOZjUbAu6A2/vtXAaAuFSFx1rUQVVjFulLexkXR3KcztL1Qu2k5pB6Si0K/uwQ=";
// 声明一个就行
static OpenClient client = new OpenClient(url, appId, privateKey);
public static void Main(string[] args)
{
TestGet();
}
// 标准用法
private static void TestGet()
{
// 创建请求对象
GetStoryRequest request = new GetStoryRequest();
// 请求参数
GetStoryModel model = new GetStoryModel();
model.Name = "白雪公主";
request.BizModel = model;
// 发送请求
GetStoryResponse response = client.Execute(request);
if (response.IsSuccess())
{
// 返回结果
Console.WriteLine("故事名称:{0}", response.Name);
}
else
{
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
response.Code, response.Msg, response.SubCode, response.SubMsg);
}
}
}
```
### 使用方式2(懒人版)
如果不想添加Request,Response,Model。可以用这种方式返回data部分是Dictionary<string, object>,后续自己处理
```
// 懒人版如果不想添加Request,Response,Model。可以用这种方式返回Dictionary<string, object>,后续自己处理
private static void TestCommon()
{
// 创建请求对象
CommonRequest request = new CommonRequest("alipay.story.find");
// 请求参数
Dictionary<string, string> bizModel = new Dictionary<string, string>
{
["name"] = "白雪公主"
};
request.BizModel = bizModel;
// 发送请求
CommonResponse response = client.Execute(request);
if (response.IsSuccess())
{
// 返回结果
string body = response.Body;
Dictionary<string, object> dict = JsonUtil.ParseToDictionary(body);
Console.WriteLine("Dictionary内容:");
foreach (var item in dict)
{
Console.WriteLine("{0}:{1}", item.Key, item.Value);
}
}
else
{
Console.WriteLine("错误, code:{0}, msg:{1}, subCode:{2}, subMsg:{3}",
response.Code, response.Msg, response.SubCode, response.SubMsg);
}
}
```
## 发布SDK
将编写好的SDK打包后上传到云服务器如百度网盘
前往sop-admin点击`SDK管理`菜单,点击`发布SDK`填写SDK语言、版本、下载地址网盘地址、调用示例
保存后ISV端会看到发布的SDK

View File

@@ -1,117 +0,0 @@
# 应用授权
## 概述
- 1、用户对开发者进行应用授权后开发者可以帮助用户完成相应的业务逻辑。
- 2、授权采用标准的OAuth 2.0流程。
## 授权流程
![授权流程](images/10097_1.png "10097_1.png")
## 快速接入
- 第一步应用授权URL拼装
拼接规则:
http://openauth.yourdomain.com/oauth2/appToAppAuth?app_id=2019032617262200001&redirect_uri=http%3a%2f%2flocalhost%3a8087%2foauth2callback
参数说明:
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|--------------|-------------|--------|----|---------------|--------------------------|
| app_id | 开发者应用的AppId | String | 是 | 开发者应用的AppId | 2015101400446982 |
| redirect_uri | 回调页面 | String | 是 | 参数需要UrlEncode | http%3A%2F%2Fexample.com |
- 第二步获取code
授权成功后会跳转至开发者定义的回调页面即redirect_uri参数对应的url在回调页面请求中会带上当次授权的授权码code和开发者的app_id示例如下
http://www.xxx.com/oauth2callback?app_id=2015101400446982&code=ca34ea491e7146cc87d25fca24c4cD11
- 第三步使用code换取app_auth_token
接口名称open.auth.token.app
开发者通过code可以换取app_auth_token、授权用户的userId。
**注意**应用授权的code唯一code使用一次后失效有效期24小时 app_auth_token永久有效。
**请求参数说明**
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|---------------|------|--------|----|---------------------------------------------------------------------------------|------------------------------------------|
| grant_type | 授权类型 | String | 是 | 如果使用code换取token则为authorization_code如果使用refresh_token换取新的token则为refresh_token | authorization_code |
| code | 授权码 | String | 否 | 与refresh_token二选一用户对应用授权后得到即第一步中开发者获取到的code值 | bf67d8d5ed754af297f72cc482287X62 |
| refresh_token | 刷新令牌 | String | 否 | 与code二选一可为空刷新令牌时使用 | 201510BB0c409dd5758b4d939d4008a525463X62 |
**接口请求SDK示例**
```java
@GetMapping("oauth2callback")
@ResponseBody
public String callback(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
String app_id = servletRequest.getParameter("app_id");
String code = servletRequest.getParameter("code");
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest();
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel();
model.setCode(code);
model.setGrant_type("authorization_code");
request.setBizModel(model);
// 根据code获取token
OpenAuthTokenAppResponse response = openClient.execute(request);
if (response.isSuccess()) {
// 成功拿到token开发者在这里保存token信息
// 后续使用token进行接口访问
log.info("授权成功body:{}", response.getBody());
}
return response.getBody();
}
```
**同步响应参数说明**
| 参数 | 参数名称 | 类型 | 必填 | 描述 | 范例 |
|-------------------|---------|--------|----|----------------------------------------------------------|----------------------------------|
| app_auth_token | 用户授权令牌 | String | 是 | 通过该令牌来帮助用户发起请求,完成业务 | 856faf8d77d3b985c1073557ce6ea724 |
| user_id | 授权用户的ID | String | 是 | 授权者id | 1 |
| expires_in | 令牌有效期 | Long | 是 | 负值表示永久有效 | -1 |
| re_expires_in | 刷新令牌有效期 | Long | 是 | 负值表示永久有效 | -3 |
| app_refresh_token | 刷新令牌时使用 | String | 是 | 刷新令牌后我们会保证老的app_auth_token从刷新开始10分钟内可继续使用请及时替换为最新token | 88e68196d2359667f0dc8136e6c86803 |
**同步响应结果示例**
```json
{
"open_auth_token_app_response": {
"code": "10000",
"msg": "Success",
"app_auth_token": "88e68196d2359667f0dc8136e6c86803",
"app_refresh_token": "856faf8d77d3b985c1073557ce6ea724",
"expires_in": -1,
"re_expires_in": -3,
"user_id": "1"
},
"sign": "xxx"
}
```
**刷新token**
```java
OpenAuthTokenAppRequest request = new OpenAuthTokenAppRequest();
OpenAuthTokenAppModel model = new OpenAuthTokenAppModel();
model.setGrant_type("refresh_token");
model.setRefresh_token("856faf8d77d3b985c1073557ce6ea724");
request.setBizModel(model);
OpenAuthTokenAppResponse response = openClient.execute(request);
if (response.isSuccess()) {
// 成功拿到token开发者在这里保存token信息
// 后续使用token进行接口访问
log.info("换取token成功body:{}", response.getBody());
}
```

View File

@@ -1,147 +0,0 @@
# 提供restful接口
有些接口没有被开放但是也想要通过网关来访问SOP提供一个固定的请求格式来访问。
请求格式:
`http://ip:port/rest/服务id/your_path`,其中`http://ip:port/rest/`为固定部分,后面跟微服务请求路径。
下面是一个微服务的接口例子
```java
@RestController
@RequestMapping("food")
public class TraditionalWebappController {
@RequestMapping(value = "getFoodById", method = RequestMethod.GET)
public Food getFoodById(Integer id) {
Food food = new Food();
food.setId(id);
food.setName("香蕉");
food.setPrice(new BigDecimal(20.00));
return food;
}
}
```
这是一个`食品服务`例子serviceId为`food-service`假设网关ip为10.0.1.11端口8081食品服务ip为10.0.1.22端口2222
1. 网关访问:`http://10.0.1.11:8081/rest/food-service/food/getFoodById?id=2`
2. 本地访问:`http://10.0.1.22:2222/food/getFoodById/?id=2`
由此可见,对于前端调用者来说,它把网关看做一个大服务,只访问网关提供的请求,不需要关心网关后面的路由转发。网关后面各个微服务独自管理,
微服务之间的调用可以使用dubbo或feign有了版本号的管理可以做到服务的平滑升级对用户来说都是无感知的。结合SOP-Admin提供的上下线功能
可实现预发布环境功能。
默认情况下,`http://ip:port/rest/`为固定部分,如果想要更改`rest`,可在网关配置文件指定:`sop.restful.path=/aaa`
请求前缀将变成:`http://ip:port/aaa/`
- 关闭restful功能
如果不想用该功能,在配置文件指定:
```properties
# 开启restful请求默认开启
sop.restful.enable=false
```
- 封装请求工具【可选】
封装请求方便调用针对vue的封装如下
```js
import axios from 'axios'
// 创建axios实例
const client = axios.create({
baseURL: process.env.BASE_API, // api 的 base_url
timeout: 5000, // 请求超时时间
headers: { 'Content-Type': 'application/json' }
})
const RequestUtil = {
/**
* 请求接口
* @param url 请求路径如http://localhost:8081/rest/food-service/food/getFoodById
* @param data 请求数据json格式
* @param callback 成功回调
* @param errorCallback 失败回调
*/
post: function(url, data, callback, errorCallback) {
client.post(url, data)
.then(function(response) {
const resp = response.result
const code = resp.code
// 成功,网关正常且业务正常
if (code === '10000' && !resp.sub_code) {
callback(resp)
} else {
// 报错
Message({
message: resp.msg,
type: 'error',
duration: 5 * 1000
})
}
})
.catch(function(error) {
console.log('err' + error) // for debug
errorCallback && errorCallback(error)
})
}
}
export default RequestUtil
```
jQuery版本如下
```js
var RequestUtil = {
/**
* 请求接口
* @param url 请求路径如http://localhost:8081/rest/food-service/food/getFoodById
* @param data 请求数据json格式
* @param callback 成功回调
* @param errorCallback 失败回调
*/
post: function(url, data, callback, errorCallback) {
$.ajax({
url: 'http://localhost:8081' // 网关url
, type: 'post'
, headers: { 'Content-Type': 'application/json' }
, data: data
,success:function(response) {
var resp = response.result
var code = resp.code
// 成功,网关正常且业务正常
if (code === '10000' && !resp.sub_code) {
callback(resp)
} else {
// 报错
alert(resp.msg);
}
}
, error: function(error) {
errorCallback && errorCallback(error)
}
});
}
}
```
jQuery调用示例
```js
$(function () {
var data = {
id: 1
,name: '葫芦娃'
}
RequestUtil.post('http://localhost:8081/rest/food-service/food/getFoodById', data, function (result) {
console.log(result)
});
})
```

View File

@@ -1,111 +0,0 @@
# 文件上传
请求接口时带上文件
## 客户端调用
```java
DemoFileUploadRequest request = new DemoFileUploadRequest();
DemoFileUploadModel model = new DemoFileUploadModel();
model.setRemark("上传文件参数");
request.setBizModel(model);
List<UploadFile> files = new ArrayList<>();
String root = System.getProperty("user.dir");
System.out.println(root);
// 这里演示将resources下的两个文件上传到服务器
files.add(new UploadFile("file1", new File(root + "/src/main/resources/file1.txt")));
files.add(new UploadFile("file2", new File(root + "/src/main/resources/file2.txt")));
request.setFiles(files);
DemoFileUploadResponse response = client.execute(request);
System.out.println("--------------------");
if (response.isSuccess()) {
List<DemoFileUploadResponse.FileMeta> responseFiles = response.getFiles();
System.out.println("您上传的文件信息:");
responseFiles.stream().forEach(file->{
System.out.println(file);
});
} else {
System.out.println("errorCode:" + response.getCode() + ",errorMsg:" + response.getMsg());
}
System.out.println("--------------------");
```
客户端使用`UploadFile`添加上传文件
```java
/**
* @param name 表单名称,不能重复
* @param file 文件
* @throws IOException
*/
public UploadFile(String name, File file) throws IOException {
this(name, file.getName(), FileUtil.toBytes(file));
}
```
其中`name`表示字段名称,相当于`<input type="file" name="file1"/>`中的name
源码详见sop-sdk
## 服务端接收文件
- 方式1
```java
/**
* 方式1将文件写在参数中可直接获取。好处是可以校验是否上传
* @param param
* @return
*/
@Open("file.upload")
@RequestMapping("file1")
public FileUploadVO file1(FileUploadParam param) {
System.out.println(param.getRemark());
// 获取上传的文件
MultipartFile file1 = param.getFile1();
MultipartFile file2 = param.getFile2();
...
return vo;
}
@Data
public class FileUploadParam {
private String remark;
// 上传文件字段名称对应表单中的name属性值
@NotNull(message = "文件1不能为空")
private MultipartFile file1;
@NotNull(message = "文件2不能为空")
private MultipartFile file2;
}
```
方式1的好处是可以使用`JSR-303`校验文件是否上传
- 方式2
```java
/**
* 方式2从request中获取上传文件
*
* @param param
* @return
*/
@Open("file.upload2")
@RequestMapping("file2")
public FileUploadVO file2(FileUploadParam2 param, HttpServletRequest request) {
System.out.println(param.getRemark());
FileUploadVO vo = new FileUploadVO();
// 获取上传的文件
Collection<MultipartFile> uploadFiles = UploadUtil.getUploadFiles(request);
...
return vo;
}
```
微服务端源码参考`FileUploadDemoController.java`

View File

@@ -1,104 +0,0 @@
# 配置Sleuth链路追踪
配置了Sleuth可以很方便查看微服务的调用路线图可快速定位问题。
SOP基于SpringCloud因此只要整合[Spring Cloud Sleuth](https://spring.io/projects/spring-cloud-sleuth)即可。
除此之外还需要支持dubbo的链路的跟踪Sleuth在2.0已经对dubbo做了支持详见[brave-instrumentation-dubbo-rpc](https://github.com/openzipkin/brave/tree/master/instrumentation/dubbo-rpc)
接入Spring Cloud Sleuth步骤如下
- 下载zipkin服务器
以mac环境为例执行下面命令下载jar并启动zipkin服务
```
curl -sSL https://zipkin.io/quickstart.sh | bash -s
java -jar zipkin.jar
```
默认端口是9411更多安装方式详见[quickstart](https://zipkin.io/pages/quickstart.html)
- sop-gateway/pom.xml添加依赖
```xml
<!--开启zipkin服务链路跟踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
```
配置文件新增
```properties
# zipkin服务跟踪
spring.zipkin.base-url=http://127.0.0.1:9411/
# 设置sleuth收集信息的比率默认0.1最大是1数字越大越耗性能
spring.sleuth.sampler.probability=1
```
重启sop-gateway
- 打开sop-story-web/pom.xml
添加依赖:
```xml
<!--开启zipkin服务链路跟踪-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<!-- zipkin支持dubbo -->
<dependency>
<groupId>io.zipkin.brave</groupId>
<artifactId>brave-instrumentation-dubbo-rpc</artifactId>
<version>5.6.6</version>
</dependency>
```
配置文件新增:
```properties
# zipkin服务跟踪
spring.zipkin.base-url=http://127.0.0.1:9411/
# 设置sleuth收集信息的比率默认0.1最大是1数字越大越耗性能
spring.sleuth.sampler.probability=1
# dubbo使用zipkin过滤器
dubbo.provider.filter=tracing
dubbo.consumer.filter=tracing
```
重启服务
- 打开sop-book/sop-book-web/pom.xml
步骤同上
- 运行DubboDemoTest.java单元测试
运行完毕看控制台,找到日志信息
```text
2019-07-18 16:22:04.438 INFO [story-service,59dae98250b276bd,60828035658f175f,true] 90553 --- [:12345-thread-2] c.g.s.s.service.DefaultDemoService : dubbo provider, param: DemoParam(id=222)
```
日志内容多了`[story-service,59dae98250b276bd,60828035658f175f,true]`部分这些是zipkin加进去的说明如下
```text
story-service服务名称
59dae98250b276bdtraceId
60828035658f175fspanId
true是否上传到zipkin服务器
```
查看各个服务的控制台可以发现traceId是一致的。
- 浏览器打开http://127.0.0.1:9411/
将traceId复制黏贴到右上角文本框进行查询可看到服务调用链。
![预览](images/10109_1.png "10109_1.png")

View File

@@ -1,71 +0,0 @@
# 预发布灰度发布
从1.14.0开始支持预发布、灰度发布,可登陆`SOP-Admin`,然后选择`服务列表`进行操作。
## 使用预发布
SOP中预发布的思路如下
假设网关工程sop-gateway在阿里云负载均衡有两台服务器域名分别为
|域名|说明|
|:---- |:---- |
|open1.domain.com |网关服务器1 |
|openpre.domain.com | 网关服务器2作为预发布请求入口|
SLB对外域名为`open.domain.com`,即开放平台入口为:`http://open.domain.com`
访问`open.domain.com`会负载均衡到`open1.domain.com``openpre.domain.com`这两台实例
如果单独从`openpre.domain.com`访问,则会访问到预发布微服务。
SOP开启预发布步骤如下
修改网关工程配置文件,指定预发布域名
```properties
# 预发布网关域名,多个用英文逗号(,)隔开
pre.domain=openpre.domain.com
```
重启网关
微服务启动参数添加:`--spring.cloud.nacos.discovery.metadata.env=pre`eureka下是`--eureka.instance.metadata-map.env=pre`)。
建议线上配两套启动脚本,其中预发布启动脚本添加启动参数`--spring.cloud.nacos.discovery.metadata.env=pre`
登录SOP-Admin在服务列表中点击预发布。
`openpre.domain.com`请求进来的用户都会进预发布服务器从SLB域名进来请求路由到非预发服务器
## 使用灰度发布
- 灰度接口定义
接口名相同,版本号不同。比如接口`user.get`version:1.0
新建一个接口`user.get`version:2.0
那么这个2.0接口就是灰度接口
灰度发布可允许指定的用户访问灰度服务器,其它用户还是走正常流程。
登录SOP-Admin前往`服务列表`
- 先设置灰度参数指定灰度appId和灰度接口
- 服务器实例开启灰度
参考类:
- LoadBalanceServerChooser.java 预发布/灰度发布服务实例选择
### 自定义判断灰度用户
默认根据`appId``IP`来判断灰度用户如果要通过其它维度来判断是否是灰度用户可实现GrayUserBuilder接口
然后在springboot main方法中调用如下方法
```java
ApiConfig.getInstance().addGrayUserBuilder(new XXGrayUserBuilder());
```
参考com.gitee.sop.gatewaycommon.loadbalancer.builder.AppIdGrayUserBuilder.java

View File

@@ -1,33 +0,0 @@
# 动态修改请求参数
自1.14.0开始zuul网关支持动态修改请求参数。即在网关修改客户端传递过来的参数然后发送到微服务端。
```
客户端参数{"name": "jim"} --> zuul中修改为{"name": "Lucy"} --> 微服务端将收到{"name": "Lucy"}
```
使用场景:客户端请求参数经过加密,在网关解密后,再次发送明文参数给微服务端
- 如何使用
在网关springboot启动函数中添加如下代码
```java
public static void main(String[] args) {
ApiConfig.getInstance().setParameterFormatter(requestParams -> {
// 获取biz_content
JSONObject jsonObject = requestParams.getJSONObject(ParamNames.BIZ_CONTENT_NAME);
// 修改biz_content中的值
jsonObject.put("name", "name修改了111");
jsonObject.put("remark", "remark修改了222");
// 重新设置biz_content
requestParams.put(ParamNames.BIZ_CONTENT_NAME, jsonObject);
});
SpringApplication.run(SopGatewayApplication.class, args);
}
```
其中requestParams是客户端传递过来的参数可在此基础上添加修改参数。
更多参考com.gitee.sop.gatewaycommon.zuul.filter.PreParameterFormatterFilter.java

View File

@@ -1,3 +0,0 @@
# 使用eureka
切换到`eureka`分支

View File

@@ -1,14 +0,0 @@
# 超时设置
当微服务处理业务逻辑时间过长网关会报超时错误默认等待时间是5秒。
可在网关指定`spring.cloud.gateway.httpclient.response-timeout`参数设置超时时间,单位毫秒
```properties
# 设置响应超时10秒
spring.cloud.gateway.httpclient.response-timeout=10000
```
更多配置参见:`org.springframework.cloud.gateway.config.HttpClientProperties`
测试用例参见:`com.gitee.sop.test.TimeoutTest`

View File

@@ -1,149 +0,0 @@
# 网关性能测试
> 注意:记得关闭限流功能
**测试环境**
- 测试工具:[wrk](https://github.com/wg/wrk)[安装教程](https://www.cnblogs.com/quanxiaoha/p/10661650.html)
- 服务器CentOS7虚拟机宿主机macbookpro内存2GCPU:1核数2核
- 运行环境Java8、Mysql-5.7、Nacos-1.1.3
- 网关启动参数:
```
-verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC -Xloggc:gc-zuul.log \
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn512m -Xss256k -XX:SurvivorRatio=8 \
-XX:+UseConcMarkSweepGC
```
- zuul配置仅针对zuulSpring Cloud Gateway没有做优化配置
```properties
# 不校验时间,这样一个链接可以一直进行测试
sop.api-config.timeout-seconds=0
sop.restful.enable=true
logging.level.com.gitee=info
# zuul调优
zuul.host.max-per-route-connections=5000
zuul.host.max-total-connections=5000
zuul.semaphore.max-semaphores=5000
ribbon.ReadTimeout=5000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=13000
logging.file=sop-gateway.log
```
CentOS允许最大连接数
```
$ ulimit -n
65535
```
## 调用开放接口
- wrk命令
```
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/?charset=utf-8&biz_content=%7B%22name%22%3A%22%E8%91%AB%E8%8A%A6%E5%A8%83%22%2C%22id%22%3A%221%22%7D&method=alipay.story.get&format=json&sign=RjK%2FThnzAJQOw%2BfoVLS18PgWZAR%2B25SI2XdONFhS%2BmS8vsv2jNT3rygFoh%2ByX1AJbMgIEfcBzkQyqrs29jjd5dcwHVkcf7vxXshyfcEgl0fbMF6Ihycnz7rqSqkW3lzAWx4NuWUfkPnTX8Ffuf%2BhYRaI0NCpNv%2FV300HvsvmUjS6ZzS4YHaT1peSq0agfUhwRPd97aYMnUwRZDzxNfc5wuXA7OQ1o%2FPYIuIb%2FajVfwNP5ysitc%2FKtYEqt9rNAuzkcFmsw71d2RRnrPLsDN%2BuBXnIEh482f%2FbMj2Rj4%2FMq%2B0PEtlTRbg3rYnxyfowymfX%2BNmI4gNRUt70D4a%2FL3Qiug%3D%3D&app_id=2019032617262200001&sign_type=RSA2&version=1.0&timestamp=2020-01-19+13%3A34%3A12"
```
- Spring Cloud Gateway测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 139.74ms 69.39ms 617.14ms 69.82%
Req/Sec 182.12 55.74 343.00 66.24%
43391 requests in 30.09s, 11.96MB read
Requests/sec: 1441.96
Transfer/sec: 406.96KB
```
结果说明,下同:
```
8 threads and 200 connections 共8个测试线程200个连接
(平均值) (标准差) (最大值)(正负一个标准差所占比例)
Thread Stats Avg Stdev Max +/- Stdev
(延迟)
Latency 139.74ms 69.39ms 617.14ms 69.82%
(每秒请求数)
Req/Sec 182.12 55.74 343.00 66.24%
43391 requests in 30.09s, 11.96MB read (30.09秒内处理了43391个请求耗费流量11.96MB)
Requests/sec: 1441.96 (QPS 1441.96,即平均每秒处理请求数为1441.96)
Transfer/sec: 406.96KB (平均每秒流量406.96KB)
```
- Spring Cloud Zuul测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 230.14ms 331.27ms 2.00s 86.98%
Req/Sec 141.69 51.04 323.00 66.99%
33945 requests in 30.09s, 9.88MB read
Socket errors: connect 0, read 0, write 0, timeout 385
Requests/sec: 1128.05
Transfer/sec: 336.15KB
```
## 调用restful请求
- wrk命令
```
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/rest/story-service/food/getFoodById?id=2"
```
线程数为 8模拟 200 个并发请求,持续 30 秒
- Spring Cloud Gateway测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 120.14ms 58.30ms 513.85ms 67.47%
Req/Sec 210.47 54.26 770.00 69.37%
50301 requests in 30.10s, 7.53MB read
Requests/sec: 1670.97
Transfer/sec: 256.21KB
```
- Spring Cloud Zuul测试结果
```
8 threads and 200 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 185.86ms 285.65ms 1.99s 88.55%
Req/Sec 167.75 55.60 460.00 68.05%
40070 requests in 30.09s, 6.65MB read
Socket errors: connect 0, read 0, write 0, timeout 466
Requests/sec: 1331.81
Transfer/sec: 226.50KB
```
综上所述Spring Cloud Gateway在没有优化的情况下压测表现比zuul好但zuul的数据表现也不差但是出现超时现象总的来说还是Spring Cloud Gateway具有优势。
附启动脚本:
`restart.sh`
```bash
echo "Stopping sop-gateway-4.2.4-SNAPSHOT.jar"
pid=`ps -ef | grep sop-gateway-4.2.4-SNAPSHOT.jar | grep -v grep | awk '{print $2}'`
if [ -n "$pid" ]
then
echo "kill -9 的id:" $pid
kill -9 $pid
fi
nohup java -jar -verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC -Xloggc:gc.log \
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn256m -Xss256k -XX:SurvivorRatio=8\
-XX:+UseConcMarkSweepGC sop-gateway-4.2.4-SNAPSHOT.jar\
--spring.profiles.active=dev --server.port=8081 &
tail -f nohup.out
```

View File

@@ -1,46 +0,0 @@
# 原理分析之如何存储路由
SOP基于spring cloud因此会涉及到网关路由。但是开发者不用去配置文件定义路由的隐射关系SOP帮你解决了这个问题。
## 获取路由信息
网关启动成后会触发一个事件,代码见:`com.gitee.sop.gatewaycommon.config.AbstractConfiguration.listenEvent`
这个事件会取拉取微服务中提供的路由信息
下面以nacos为例介绍拉取路由过程
1.从nacos中获取微服务实例
入口代码:`com.gitee.sop.bridge.route.NacosRegistryListener.onEvent`
2.拿到微服务信息,调用微服务提供的接口拉取路由数据
入口代码:`com.gitee.sop.gatewaycommon.route.BaseRegistryListener.pullRoutes`
最终落实到:`com.gitee.sop.gatewaycommon.route.ServiceRouteListener.onAddInstance`
微服务提供一个url`http://ip:port/sop/routes`对应Controller在`com.gitee.sop.servercommon.route.ServiceRouteController.listRoutes`
微服务找到被`@Open`注解的方法然后封装成一个路由对象放到List中最后返回给网关。
3.网关拿到路由信息,经过处理,转化成网关路由配置
关联方法:
`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.add`
`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.refresh`
路由的存储方式是一个Mapkey为路由id即接口名+版本号。
```java
/**
* keynameVersion
*/
private static final Map<String, GatewayTargetRoute> routes = synchronizedMap(new LinkedHashMap<>());
```
因为客户端调用接口都会传递一个接口名和版本号,因此通过这两个字段能够很快查询出路由信息,然后进行路由转发操作。

View File

@@ -1,33 +0,0 @@
# 原理分析之如何路由
Spring Cloud Gateway通过一系列的Filter来进行数据的传输如下图所示
![流程图](images/90012_1.png)
SOP网关在此基础上新增了几个Filter用来处理自己的逻辑前置校验、结果返回。
| 过滤器 | 类型 | Order | 功能 |
| ----- | ---- | ----------------------- | ---------------------------- |
|IndexFilter| `自定义` | -2147483648 | 入口过滤器,获取参数、签名校验 |
|ParameterFormatterFilter | `自定义` | -2147482647 | 格式化参数 |
|LimitFilter|`自定义`|-2147482447|限流|
|ForwardPathFilter|系统自带|0 |设置转发的path|
|RouteToRequestUrlFilter|系统自带|10000|设置转发host|
|SopLoadBalancerClientFilter|`自定义`|10100|LoadBalance获取转发实例|
|NettyRoutingFilter|系统自带|2147483647|获取httpclient发送请求|
|ForwardRoutingFilter|系统自带|2147483647|请求分发|
|GatewayModifyResponseGatewayFilter|`自定义`|-2|处理响应结果|
一个完整的请求会自上而下经过这些Filter下面讲解如何动态设置路由。
## 动态设置路由
网关启动后会从注册中心拉取微服务实例,然后请求微服务提供的一个接口(`/sop/routes`),获取开放接口信息(被`@Open`注解的接口)。
监听处理类在:`com.gitee.sop.bridge.route.NacosRegistryListener`
获取到路由信息后,将路由信息缓存到本地,并保存到数据库,代码在:`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache.load`
然后动态设置Gateway路由代码在`com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteRepository.refresh`
当有微服务重新启动时,网关会监听到微服务实例有变更,会重复上述步骤,确保网关存有最新的路由。

View File

@@ -1,26 +0,0 @@
# 原理分析之文档归纳
作为开放平台必须要提供API文档。
SOP采用微服务架构实现因此文档应该由各个微服务各自实现。难点是如何归纳各个微服务端提供的文档信息并统一展示。
SOP的解决思路如下
- 各微服务使用swagger定义自己的接口信息
- sop-website项目在启动时向注册中心获取所有服务实例分别调用各个服务提供的swagger文档信息保存到本地
- sop-website前端页面负责展示swagger提供的文档信息
由于注册中心的存在可以很方便的获取每个微服务提供的接口因此可以获取到swagger提供的文档信息。
如此一来的好处是各微服务不用关心文档该怎么展示只需要写好swagger注解即可文档信息展示统一交给另外一个工程来维护各司其职。
SOP设计初衷亦是如此微服务只管写业务代码其它的都交给SOP来处理。
文档归纳原理图:
![文档归纳原理图](images/90013_1.png "10090_1.png")
- sop-website服务启动时向各微服务获取接口信息保存到本地
- 用户访问website页面website提供对应的接口文档并展示

View File

@@ -1,92 +0,0 @@
# 原理分析之预发布灰度发布
SOP网关采用`自定义负载均衡策略`来实现对预发布/灰度发布服务器实例的选择。
spring cloud gateway默认的负载均衡实现类在:`org.springframework.cloud.gateway.filter.LoadBalancerClientFilter.java`
这个类主要做了几件事情:
1. 解析出请求路径中的scheme
2. 如果scheme不是以`lb`协议开头直接跳过
3. 如果scheme以`lb`协议开头,则说明需要进行负载均衡,选出一台微服务实例
4.`lb`协议解析成`http://ip:port`,继续向下请求
其中第4步是由`org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient`来完成的,
我们只要分别继承`LoadBalancerClientFilter``RibbonLoadBalancerClient`,然后重写其中的方法就能完成自定义负载均衡。
SOP中的重写类是`SopLoadBalancerClientFilter``SopLoadBalancerClient`,核心代码委托给了`LoadBalanceServerChooser`处理,核心代码如下:
```java
/**
* 选择服务器
*
* @param serviceId serviceId仅gateway网关有作用
* @param exchange 请求上下文
* @param loadBalancer loadBalancer
* @param superChooser 父类默认的选择
* @param serverChooserFunction 执行选择操作
* @return 返回服务器实例没有选到则返回null
*/
public R choose(
String serviceId
, T exchange
, ILoadBalancer loadBalancer
, Supplier<R> superChooser
, Function<List<Server>, R> serverChooserFunction) {
// 获取所有服务实例
List<Server> servers = loadBalancer.getReachableServers();
// 存放预发服务器
List<Server> preServers = new ArrayList<>(4);
// 存放灰度发布服务器
List<Server> grayServers = new ArrayList<>(4);
// 存放非预发服务器
List<Server> notPreServers = new ArrayList<>(4);
for (Server server : servers) {
// 获取实例metadata
Map<String, String> metadata = getMetadata(serviceId, server);
// 是否开启了预发模式
if (this.isPreServer(metadata)) {
preServers.add(server);
} else if (this.isGrayServer(metadata)) {
grayServers.add(server);
} else {
notPreServers.add(server);
}
}
notPreServers.addAll(grayServers);
// 如果没有开启预发布服务和灰度发布,直接用默认的方式
if (preServers.isEmpty() && grayServers.isEmpty()) {
return superChooser.get();
}
// 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器
if (this.isRequestFromPreDomain(exchange)) {
return serverChooserFunction.apply(preServers);
}
// 如果是灰度请求,则认为是灰度用户,选出灰度服务器
if (this.isRequestGrayServer(exchange)) {
return serverChooserFunction.apply(grayServers);
}
// 到这里说明不能访问预发/灰度服务器,则需要路由到非预发服务器
// 注意这里允许走灰度服务器如果不允许走注释notPreServers.addAll(grayServers);这行
return serverChooserFunction.apply(notPreServers);
}
```
其业务逻辑如下:
1. 选出`serviceId`对应的所有服务器实例
2. 将服务器实例进行分类,分别放进`预发布List``灰度List``非预发布List`
3. 如果`预发布List``灰度List`都为空,表示没有开启任何预发/灰度服务,直接使用父类的负载均衡策略
4. 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器
5. 如果是灰度请求,则认为是灰度用户,选出灰度服务器
6. 最后剩下的是正常用户,正常用户不能走预发环境
## 参考类
- com.gitee.sop.gatewaycommon.gateway.filter.SopLoadBalancerClientFilter
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.SopLoadBalancerClient
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.GatewayLoadBalanceServerChooser

View File

@@ -1,125 +0,0 @@
# 常见问题
## Connection has been closed BEFORE send operation
压测时出现`Connection has been closed BEFORE send operation``java.nio.channels.ClosedChannelException`之类的错误
- 解决办法
尝试调大内存分配1G或2G`-Xms1g -Xmx1g`
## spring cloud gateway [DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144]
如果POST请求body内容太大可能会报这个错误
- 解决版办法
网关指定配置`spring.codec.max-in-memory-size=xx`xx默认是262144即256K262144=256*1024
## primordials is not defined
node版本太高导致与gulp版本不兼容解决方法node退回11版本。参考https://blog.csdn.net/zxxzxx23/article/details/103000393
## Nacos指定group
可在配置文件中添加:`spring.cloud.nacos.discovery.group=xxx`指定group不加默认是DEFAULT_GROUP
## 在SpringCloudGateway中获取请求参数
```java
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange);
```
## 微服务端如何获取appId等参数
```java
OpenContext openContext = ServiceContext.getCurrentContext().getOpenContext();
System.out.println("app_id:" + openContext.getAppId());
System.out.println("token:" + openContext.getAppAuthToken());
```
## 如何关闭签名验证
- 针对某一个接口关闭签名验证
`@Open(value = "alipay.story.get", ignoreValidate = true)`
## 注册到eureka显示hostname非ip
```properties
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
```
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
```
参考https://www.jianshu.com/p/5ad8317961b7
## 直接访问服务的swagger-ui.html 提示access forbidden
找到微服务的`OpenServiceConfig.java`重写内部类Swagger2中的swaggerAccessProtected()方法返回false。线上请设置成true
```java
// 开启文档
@Configuration
@EnableSwagger2
public static class Swagger2 extends SwaggerSupport {
@Override
protected String getDocTitle() {
return "故事API";
}
@Override
protected boolean swaggerAccessProtected() {
return false;
}
}
```
## 调试网关出现服务不可用
打断点调试网关出现Read Timeout
参考https://blog.csdn.net/qq_36872046/article/details/81058045
yml添加
```properties
# https://blog.csdn.net/qq_36872046/article/details/81058045
# 路由转发超时时间毫秒默认值1000详见RibbonClientConfiguration.DEFAULT_READ_TIMEOUT。
# 如果微服务端 处理时间过长会导致ribbon read超时解决办法将这个值调大一点
ribbon.ReadTimeout= 60000
```
## 其它微服务没有开放接口,需要排除
在sop-gateway项目中配置
```properties
# 排除服务,多个用,隔开
sop.service.exclude=your-serviceId1,your-serviceId2
```
或者使用正则:
```properties
# 排除以"test-"开头的
# 多个正则用英文分号(;)隔开
sop.service.exclude-regex=test\\-.*
```
## ISV公私钥 & 平台公私钥
```java
ISV私钥必须ISV保存用来生成签名 --> ISV公钥必须平台保存用来校验签名是否正确
平台私钥非必须平台保存对返回结果生成签名 --> 平台公钥非必须ISV保存用来校验签名是否正确
```
总结:私钥负责加密生成签名,公钥负责校验签名是否正确

Binary file not shown.

Before

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 853 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,68 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SOP开发文档</title>
<link rel="icon" href="_media/favicon.ico">
<meta name="google-site-verification" content="6t0LoIeFksrjF4c9sqUEsVXiQNxLp2hgoqo0KryT-sE" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="keywords" content="doc,docs,documentation,gitbook,creator,generator,github,jekyll,github-pages">
<meta name="description" content="A magical documentation generator.">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css" title="vue" >
<style>
nav.app-nav li ul {
min-width: 100px;
}
/* 显示区域宽度 */
.markdown-section{
max-width: 90%;
}
.sidebar li {
margin: 0px 0;
}
</style>
</head>
<body>
<div id="app">加载中 ...</div>
<script>
window.$docsify = {
alias: {
'/.*/_sidebar.md': '/_sidebar.md?t=' + new Date().getTime()
},
auto2top: true,
coverpage: false,
executeScript: true,
loadSidebar: true,
loadNavbar: false,
mergeNavbar: true,
maxLevel: 3,
subMaxLevel: 2,
ga: 'UA-106147152-1',
name: '',
search: {
noData: {
'/zh-cn/': '没有结果!',
'/': '没有结果!'
},
paths: 'auto',
placeholder: {
'/zh-cn/': '搜索',
'/': '搜索'
}
},
formatUpdated: '{MM}/{DD} {HH}:{mm}'
}
</script>
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/search.min.js"></script>
<script src="//unpkg.com/docsify/lib/plugins/ga.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-bash.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-markdown.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-java.min.js"></script>
<script src="//unpkg.com/prismjs/components/prism-json.min.js"></script>
</body>
</html>

View File

@@ -1,49 +0,0 @@
<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>com.gitee.sop</groupId>
<artifactId>doc</artifactId>
<version>4.4.2-SNAPSHOT</version>
<properties>
<!-- Generic properties -->
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1 +0,0 @@
docsify serve docs

View File

@@ -1,55 +0,0 @@
const liveServer = require('live-server')
const isSSR = !!process.env.SSR
const middleware = []
if (isSSR) {
const Renderer = require('./packages/docsify-server-renderer/build.js')
const renderer = new Renderer({
template: `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>docsify</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<link rel="stylesheet" href="/themes/vue.css" title="vue">
</head>
<body>
<!--inject-app-->
<!--inject-config-->
<script src="/lib/docsify.js"></script>
</body>
</html>`,
config: {
name: 'docsify',
repo: 'docsifyjs/docsify',
basePath: 'https://docsify.js.org/',
loadNavbar: true,
loadSidebar: true,
subMaxLevel: 3,
auto2top: true,
alias: {
'/de-de/changelog': '/changelog',
'/zh-cn/changelog': '/changelog',
'/changelog':
'https://raw.githubusercontent.com/docsifyjs/docsify/master/CHANGELOG'
}
},
path: './'
})
middleware.push(function(req, res, next) {
if (/\.(css|js)$/.test(req.url)) {
return next()
}
renderer.renderToString(req.url).then(html => res.end(html))
})
}
const params = {
port: 3000,
watch: ['lib', 'docs', 'themes'],
middleware
}
liveServer.start(params)

View File

@@ -1,99 +0,0 @@
package com.gitee.sop.doc;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 生成_sidebar.md文件直接运行即可
*
* @author tanghc
*/
public class SidebarTest {
static String format = " * [%s](files/%s?t=%s)\r\n";
static Map<String, Menu> levelMap = new HashMap<>(8);
static {
int i = 0;
levelMap.put("1", new Menu("* 开发文档\n", i++));
levelMap.put("9", new Menu("* 原理分析\n", i++));
}
public static void main(String[] args) throws Exception {
String path = SidebarTest.class.getClassLoader().getResource("").getPath();
String root = path.substring(0, path.indexOf("doc")) + "doc";
String fileDir = root + "/docs/files";
File dir = new File(fileDir);
File[] files = dir.listFiles();
Stream<File> filesStream = Stream.of(files);
Map<String, List<FileExt>> menuMap = filesStream
.sorted(Comparator.comparing(File::getName))
.map(file -> {
if (file.isDirectory()) {
return null;
}
FileExt fileExt = new FileExt();
fileExt.menu = file.getName().substring(0, 1);
fileExt.file = file;
return fileExt;
})
.filter(Objects::nonNull)
.collect(Collectors.groupingBy(FileExt::getMenu));
StringBuilder output = new StringBuilder();
output.append("* [首页](/?t=" + System.currentTimeMillis() + ")\n");
for (Map.Entry<String, List<FileExt>> entry : menuMap.entrySet()) {
Menu menu = levelMap.get(entry.getKey());
if (menu == null) {
continue;
}
output.append(menu.parentName);
for (FileExt fileExt : entry.getValue()) {
String filename = fileExt.file.getName();
String title = filename.substring(filename.indexOf("_") + 1, filename.length() - 3);
String line = String.format(format, title, filename, System.currentTimeMillis());
output.append(line);
}
}
System.out.println(output);
String sidebarFilepath = root + "/docs/_sidebar.md";
FileOutputStream out = new FileOutputStream(new File(sidebarFilepath));
out.write(output.toString().getBytes());
out.close();
}
static class Menu {
String parentName;
int order;
public Menu(String parentName, int order) {
this.parentName = parentName;
this.order = order;
}
}
static class FileExt {
File file;
String menu;
public File getFile() {
return file;
}
public String getMenu() {
return menu;
}
}
}

0
docker-build.sh Normal file → Executable file
View File

6
docker-entrypoint.sh Normal file → Executable file
View File

@@ -3,11 +3,9 @@
JAVA_OPTS="-Xms128m -Xmx128m"
# mysql, nacos配置
args="--mysql.host=10.1.30.110:3306 --mysql.username=root --mysql.password=root --register.url=10.1.30.110:8848"
args="--mysql.host=10.1.30.110:3306 --mysql.username=root --mysql.password=root --dubbo.registry.address=10.1.30.110:8848"
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-gateway/sop-gateway.jar $args --logging.file.path=/sop/sop-gateway/log &
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-admin/sop-admin.jar $args --logging.file.path=/sop/sop-admin/log &
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-website/sop-website.jar $args --logging.file.path=/sop/sop-website/log &
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-auth/sop-auth.jar $args --logging.file.path=/sop/sop-auth/log &
# 最后一条没有&
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-story/sop-story.jar $args --logging.file.path=/sop/sop-story/log
java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /sop/sop-story/sop-story.jar $args --logging.file.path=/sop/sop-story/log

153
pom.xml Normal file → Executable file
View File

@@ -11,28 +11,25 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-parent</artifactId>
<version>4.4.2-SNAPSHOT</version>
<version>5.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<description>一个开放平台解决方案项目,基于Spring Cloud实现,目标是能够让用户快速得搭建起自己的开放平台</description>
<description>一个开放平台解决方案项目,基于Dubbo实现,目标是能够让用户快速得搭建起自己的开放平台</description>
<modules>
<module>doc</module>
<module>sop-common</module>
<module>sop-auth</module>
<module>sop-example</module>
<module>sop-admin</module>
<module>sop-gateway</module>
<module>sop-website</module>
<module>sop-test</module>
<module>sop-sdk</module>
<module>sop-website</module>
<module>sop-gateway</module>
<module>sop-registry</module>
<module>sop-support</module>
</modules>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- springboot 版本-->
<spring-boot.version>2.6.15</spring-boot.version>
@@ -41,15 +38,10 @@
<!-- spring cloud alibaba 版本 -->
<!-- 具体版本对应关系见https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E -->
<spring-cloud-alibaba.version>2021.0.5.0</spring-cloud-alibaba.version>
<!-- dubbo版本 -->
<dubbo.version>3.2.10</dubbo.version>
<!-- Logging -->
<logback.version>1.2.3</logback.version>
<!-- Test -->
<junit.version>4.11</junit.version>
<fastjson.version>1.2.73</fastjson.version>
<commons-io.version>2.5</commons-io.version>
<commons-fileupload.version>1.3.3</commons-fileupload.version>
<commons-collection.version>3.2.2</commons-collection.version>
@@ -58,15 +50,7 @@
<commons-logging.version>1.2</commons-logging.version>
<validation-api.version>2.0.1.Final</validation-api.version>
<hibernate-validator.version>6.0.13.Final</hibernate-validator.version>
<fastmybatis.version>2.4.8</fastmybatis.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<guava.version>29.0-jre</guava.version>
<knife4j.version>3.0.2</knife4j.version>
<swagger.version>1.5.21</swagger.version>
<springfox.version>3.0.0</springfox.version>
<easyopen.version>1.16.9</easyopen.version>
<asm.version>6.2</asm.version>
<pagehelper.version>5.2.0</pagehelper.version>
<fastmybatis.version>3.0.12</fastmybatis.version>
</properties>
<dependencyManagement>
@@ -93,60 +77,30 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-bom</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>net.oschina.durcframework</groupId>
<groupId>io.gitee.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
<version>${fastmybatis.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-spring-web</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>net.oschina.durcframework</groupId>
<artifactId>easyopen</artifactId>
<version>${easyopen.version}</version>
<artifactId>http-helper</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
<dependency>
@@ -155,12 +109,6 @@
<version>3.14.7</version>
</dependency>
<dependency>
<groupId>net.oschina.durcframework</groupId>
<artifactId>easyopen-spring-boot-starter</artifactId>
<version>${easyopen.version}</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
@@ -171,11 +119,20 @@
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.5</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<!-- commons -->
@@ -210,22 +167,10 @@
<version>${commons-logging.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>${pagehelper.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<version>1.18.34</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
@@ -264,6 +209,26 @@
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<linkXRef>false</linkXRef>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

10
sop-admin/pom.xml Normal file → Executable file
View File

@@ -3,18 +3,16 @@
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>com.gitee.sop</groupId>
<artifactId>sop-parent</artifactId>
<version>4.4.2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
<version>5.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sop-admin</artifactId>
<packaging>pom</packaging>
<modules>
<module>sop-admin-server</module>
<module>sop-admin-backend</module>
</modules>
</project>
</project>

4
sop-admin/readme.md Normal file → Executable file
View File

@@ -1,5 +1,5 @@
# 后台admin
- sop-admin-server: admin服务端使用方式见readme.md
- sop-admin-vue: admin前端vue实现
- sop-admin-backend: admin服务端
- sop-admin-frontend: admin前端实现

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>com.gitee.sop</groupId>
<artifactId>sop-admin-backend</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>admin-boot</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>admin-web</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!-- nacos注册中心 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- provided -->
<!-- 仅在开发中使用 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper-curator5</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
</repository>
</repositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<linkXRef>false</linkXRef>
</configuration>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,15 @@
package com.gitee.sop.admin;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableDubbo
public class SopAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SopAdminApplication.class, args);
}
}

View File

@@ -0,0 +1,17 @@
package com.gitee.sop.admin.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* @author 六如
*/
@Configuration
@ConfigurationProperties(prefix = "admin")
@Data
public class AdminConfig {
private int jwtTimeoutDays;
}

View File

@@ -0,0 +1,106 @@
package com.gitee.sop.admin.config;
import com.gitee.sop.admin.common.context.SpringContext;
import com.gitee.sop.admin.common.util.SystemUtil;
import com.gitee.sop.admin.interceptor.LoginInterceptor;
import com.gitee.sop.admin.service.sys.UserCacheService;
import com.gitee.sop.admin.service.sys.impl.LocalUserCacheService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author 六如
*/
@Configuration
@Slf4j
public class SopAdminConfiguration implements ApplicationContextAware, WebMvcConfigurer {
@Value("${front-location:}")
private String frontLocation;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContext.setApplicationContext(applicationContext);
}
@Bean
@ConditionalOnProperty(value = "user.cache.type", havingValue = "local", matchIfMissing = true)
public UserCacheService userCacheService() {
return new LocalUserCacheService();
}
/**
* 配置拦截器
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
String[] excludes = {
"/", "/error",
// 排除前端资源
"/*.html", "/*.ico", "/*.png", "/*.json", "/static/**", "/assets/**"
};
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(excludes);
}
/**
* 跨域设置
*/
@Bean
public CorsFilter corsFilter(
@Value("${torna.cors.allowed-origin-pattern:*}") String allowedOriginPattern,
@Value("${torna.cors.allowed-header:*}") String allowedHeader
) {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// SpringBoot升级2.4.0之后,跨域配置中的.allowedOrigins不再可用,改成addAllowedOriginPattern
corsConfiguration.addAllowedOriginPattern(allowedOriginPattern);
corsConfiguration.addAllowedHeader(allowedHeader);
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
corsConfiguration.addExposedHeader("Content-Disposition");
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
/**
* 配置静态资源
*
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
String homeDir = SystemUtil.getBinPath();
String frontRoot;
if (StringUtils.hasText(frontLocation)) {
frontRoot = StringUtils.trimTrailingCharacter(frontLocation, '/');
} else {
frontRoot = homeDir + "/dist";
}
log.info("前端资源目录:{}", frontRoot);
String location = "file:" + frontRoot;
registry.addResourceHandler("/index.html").addResourceLocations(location + "/index.html");
registry.addResourceHandler("/favicon.ico").addResourceLocations(location + "/favicon.ico");
registry.addResourceHandler("/logo.png").addResourceLocations(location + "/logo.png");
registry.addResourceHandler("/platform-config.json").addResourceLocations(location + "/platform-config.json");
registry.addResourceHandler("/static/**").addResourceLocations(location + "/static/");
registry.addResourceHandler("/assets/**").addResourceLocations(location + "/assets/");
}
}

View File

@@ -0,0 +1,45 @@
package com.gitee.sop.admin.interceptor;
import com.gitee.sop.admin.common.annotation.NoToken;
import com.gitee.sop.admin.common.enums.StatusEnum;
import com.gitee.sop.admin.common.user.User;
import com.gitee.sop.admin.common.context.UserContext;
import com.gitee.sop.admin.common.exception.LoginFailureException;
import com.gitee.sop.admin.common.util.RequestUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author tanghc
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
NoToken noLogin = handlerMethod.getMethodAnnotation(NoToken.class);
if (noLogin != null) {
return true;
}
noLogin = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), NoToken.class);
if (noLogin != null) {
return true;
}
User user = UserContext.getUser(request);
if (user == null || StatusEnum.of(user.getStatus()) == StatusEnum.DISABLED) {
log.error("登录失败, 客户端ip:{}, uri:{}", RequestUtil.getIP(request), request.getRequestURI());
throw new LoginFailureException("登录失败");
}
return true;
}
}

View File

@@ -0,0 +1,8 @@
dubbo.registry.address=zookeeper://localhost:2181
mybatis.print-sql=true
# mysql config
mysql.host=127.0.0.1:3306
mysql.username=root
mysql.password=root

View File

@@ -0,0 +1,8 @@
dubbo.registry.address=nacos://localhost:8848
mybatis.print-sql=true
# mysql config
mysql.host=127.0.0.1:3306
mysql.username=root
mysql.password=root

View File

@@ -0,0 +1,48 @@
server.port=8082
spring.profiles.active=dev
spring.application.name=sop-admin
index.path=/
####### admin config #######
# user cache
user.cache.type=local
dubbo.protocol.name=dubbo
dubbo.protocol.port=-1
dubbo.application.qos-enable=false
dubbo.consumer.check=false
# ### register config see:https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/registry/overview/
# ------
# nacos://localhost:8848 Cluster config:nacos://localhost:8848?backup=localshot:8846,localshot:8847
# zookeeper://localhost:2181 Cluster config:zookeeper://10.20.153.10:2181?backup=10.20.153.11:2181,10.20.153.12:2181
# redis://localhost:6379 Cluster config:redis://10.20.153.10:6379?backup=10.20.153.11:6379,10.20.153.12:6379
# ------
dubbo.registry.address=zookeeper://localhost:2181
####### mysql config #######
mysql.host=127.0.0.1:3306
mysql.username=
mysql.password=
mysql.db=sop
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://${mysql.host}/${mysql.db}?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.username=${mysql.username}
spring.datasource.password=${mysql.password}
####### mybatis config #######
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillInsert=add_time
mybatis.fill.com.gitee.fastmybatis.core.support.LocalDateTimeFillUpdate=update_time
mybatis.fill.com.gitee.sop.admin.common.fill.AddByFill=
mybatis.fill.com.gitee.sop.admin.common.fill.UpdateByFill=
# mybatis config file
mybatis.config-location=classpath:mybatis/mybatisConfig.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# print SQL
logging.level.com.gitee.sop.admin.dao=error
logging.level.com.gitee.fastmybatis=info
mybatis.print-sql=false

View File

@@ -0,0 +1,13 @@
package com.gitee.sop.admin;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class BaseTest {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,44 @@
package com.gitee.sop.admin.service;
import com.alibaba.fastjson2.JSON;
import com.gitee.sop.admin.BaseTest;
import com.gitee.sop.admin.service.sys.login.LoginService;
import com.gitee.sop.admin.service.sys.login.dto.LoginDTO;
import com.gitee.sop.admin.service.sys.login.dto.LoginUser;
import com.gitee.sop.admin.service.sys.login.enums.RegTypeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.util.Assert;
/**
* @author 六如
*/
public class LoginServiceTest extends BaseTest {
@Autowired
LoginService loginService;
@Test
public void login() {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("admin");
loginDTO.setPassword("123456");
loginDTO.setRegType(RegTypeEnum.BACKEND);
LoginUser loginUser = loginService.login(loginDTO);
Assert.notNull(loginUser, "not null");
System.out.println(JSON.toJSONString(loginUser));
}
@Test
public void resetAdminPwd() {
// 初始密码
String defPassword = "123456";
defPassword = DigestUtils.sha256Hex(defPassword);
String encodedPassword = BCrypt.hashpw(defPassword, BCrypt.gensalt());
}
}

View File

@@ -0,0 +1,36 @@
package com.gitee.sop.admin.service;
import com.gitee.sop.admin.BaseTest;
import com.gitee.sop.admin.dao.entity.SysUser;
import com.gitee.sop.admin.service.sys.SysUserService;
import org.apache.commons.codec.digest.DigestUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCrypt;
/**
* @author 六如
*/
public class PasswordTest extends BaseTest {
@Autowired
SysUserService sysAdminUserService;
/**
* 重置admin密码
*/
@Test
public void resetAdminPwd() {
String username = "admin";
String defPassword = "123456";
defPassword = DigestUtils.sha256Hex(defPassword);
String encodedPassword = BCrypt.hashpw(defPassword, BCrypt.gensalt());
System.out.println("数据库保存:" + encodedPassword);
sysAdminUserService.query()
.eq(SysUser::getUsername, username)
.set(SysUser::getPassword, encodedPassword)
.update();
}
}

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>com.gitee.sop</groupId>
<artifactId>sop-admin-backend</artifactId>
<version>5.0.0-SNAPSHOT</version>
</parent>
<artifactId>admin-common</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-support</artifactId>
<version>5.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.gitee.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- json处理 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>net.oschina.durcframework</groupId>
<artifactId>http-helper</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,17 @@
package com.gitee.sop.admin.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 不需要登录验证
* @author tanghc
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoToken {
}

View File

@@ -0,0 +1,45 @@
package com.gitee.sop.admin.common.config;
import com.gitee.sop.admin.common.context.SpringContext;
import com.gitee.sop.admin.common.enums.ConfigKeyEnum;
import java.util.function.Supplier;
/**
* @author 六如
*/
public class Configs {
/**
* 获取配置参数
*
* @param keyGetter 配置key
* @return 返回配参数没有则返回null
*/
public static String getValue(ConfigKeyEnum keyGetter) {
return getValue(keyGetter, keyGetter.getDefaultValue());
}
/**
* 获取配置参数
*
* @param keyGetter 配置key
* @param defaultValue 默认值
* @return 返回配参数,没有则返回默认值
*/
public static String getValue(ConfigKeyEnum keyGetter, String defaultValue) {
return SpringContext.getBean(IConfig.class).getConfig(keyGetter.getKey(), defaultValue);
}
/**
* 获取配置参数
*
* @param keyGetter 配置key
* @param defaultValue 默认值
* @return 返回配参数,没有则返回默认值
*/
public static String getValue(ConfigKeyEnum keyGetter, Supplier<String> defaultValue) {
return getValue(keyGetter, defaultValue.get());
}
}

View File

@@ -0,0 +1,9 @@
package com.gitee.sop.admin.common.config;
public interface IConfig {
String getConfig(String key);
String getConfig(String key, String defaultValue);
}

View File

@@ -0,0 +1,20 @@
package com.gitee.sop.admin.common.constants;
import java.util.Objects;
/**
* @author 六如
*/
public class YesOrNo {
public static final int YES = 1;
public static final int NO = 0;
public static boolean yes(Number value) {
return value != null && value.intValue() == YES;
}
public static int of(Boolean b) {
return Objects.equals(b, true) ? YES : NO;
}
}

View File

@@ -0,0 +1,31 @@
package com.gitee.sop.admin.common.context;
import org.springframework.context.ApplicationContext;
/**
* @author 六如
*/
public class SpringContext {
private static ApplicationContext ctx;
public static <T> T getBean(Class<T> clazz) {
return ctx.getBean(clazz);
}
public static Object getBean(String beanName) {
return ctx.getBean(beanName);
}
public static void setApplicationContext(ApplicationContext ctx) {
SpringContext.ctx = ctx;
}
public static ApplicationContext getApplicationContext() {
return ctx;
}
public static void publishEvent(Object event) {
ctx.publishEvent(event);
}
}

View File

@@ -0,0 +1,126 @@
package com.gitee.sop.admin.common.context;
import com.auth0.jwt.interfaces.Claim;
import com.gitee.sop.admin.common.manager.UserCacheManager;
import com.gitee.sop.admin.common.user.User;
import com.gitee.sop.admin.common.config.Configs;
import com.gitee.sop.admin.common.enums.ConfigKeyEnum;
import com.gitee.sop.admin.common.exception.ErrorTokenException;
import com.gitee.sop.admin.common.exception.JwtErrorException;
import com.gitee.sop.admin.common.exception.JwtExpiredException;
import com.gitee.sop.admin.common.exception.LoginFailureException;
import com.gitee.sop.admin.common.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
/**
* @author tanghc
*/
@Slf4j
public class UserContext {
public static final String HEADER_AUTHORIZATION = "Authorization";
public static final String JWT_PREFIX = "Bearer ";
private static Supplier<String> tokenGetter = () -> {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
return getToken(request);
};
public static void setTokenGetter(Supplier<String> tokenGetter) {
UserContext.tokenGetter = tokenGetter;
}
/**
* 获取当前登录用户id
* @return 返回id,没有返回null
*/
public static Long getUserId() {
return Optional.ofNullable(getUser()).map(User::getUserId).orElse(null);
}
/**
* 获取当前登录用户
*
* @return 返回当前登录用户没有返回null
*/
public static User getUser() {
String token = tokenGetter.get();
try {
return getUser(token);
} catch (ErrorTokenException e) {
throw new LoginFailureException();
}
}
/**
* 获取当前登录用户
*
* @return 返回当前登录用户没有返回null
*/
public static User getUser(HttpServletRequest request) {
String token = getToken(request);
try {
return getUser(token);
} catch (ErrorTokenException e) {
throw new LoginFailureException();
}
}
public static String getToken(HttpServletRequest request) {
String token = request.getHeader(HEADER_AUTHORIZATION);
if (StringUtils.hasText(token) && token.startsWith(JWT_PREFIX)) {
return token.substring(JWT_PREFIX.length());
}
return token;
}
/**
* 获取登录用户
*
* @param token 格式:<userId>:<jwt>
* @return 返回token对应的用户没有返回null
*/
private static User getUser(String token) throws ErrorTokenException {
if (StringUtils.isEmpty(token)) {
return null;
}
String secret = Configs.getValue(ConfigKeyEnum.JWT_SECRET);
Map<String, Claim> data;
// verify jwt
try {
data = JwtUtil.verifyJwt(token, secret);
} catch (JwtExpiredException | JwtErrorException e) {
log.error("jwt verify failed, token:{}, message:{}", token, e.getMessage(), e);
throw new ErrorTokenException();
}
Claim id = data.get("id");
long userId = NumberUtils.toLong(id.asString(), 0);
if (userId == 0) {
return null;
}
return SpringContext.getBean(UserCacheManager.class).getUser(userId);
}
@Deprecated
public static Locale getLocale() {
try {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
return request.getLocale();
} catch (Exception e) {
return Locale.SIMPLIFIED_CHINESE;
}
}
}

View File

@@ -0,0 +1,14 @@
package com.gitee.sop.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @author 六如
*/
@Data
public class IdDTO {
@NotNull(message = "id不能为空")
private Long id;
}

View File

@@ -0,0 +1,13 @@
package com.gitee.sop.admin.common.dto;
import lombok.Data;
import java.util.List;
/**
* @author 六如
*/
@Data
public class IdsDTO {
private List<Long> ids;
}

View File

@@ -0,0 +1,16 @@
package com.gitee.sop.admin.common.dto;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @author 六如
*/
@Data
public class StatusUpdateBatchDTO extends IdsDTO {
@NotNull(message = "状态不能为空")
private Integer status;
}

Some files were not shown because too many files have changed in this diff Show More