tanghc 6 years ago
parent 78f85256d6
commit 43c72530d3
  1. 7
      README.md
  2. 10
      changelog.md
  3. 28
      doc/docs/_sidebar.md
  4. 8
      doc/docs/files/10010_快速体验.md
  5. 14
      doc/docs/files/10085_ISV管理.md
  6. 25
      doc/docs/files/10090_路由授权.md
  7. 2
      doc/docs/files/90011_原理分析之路由存储.md
  8. BIN
      doc/docs/files/images/10085_1.png
  9. BIN
      doc/docs/files/images/10090_1.png
  10. BIN
      doc/docs/files/images/10090_2.png
  11. 5
      doc/docs/index.html
  12. 5
      doc/src/main/java/com/gitee/sop/doc/SidebarTest.java
  13. 1
      sop-admin/sop-admin-front/assets/css/xadmin.css
  14. 30
      sop-admin/sop-admin-front/assets/js/profile.js
  15. 22
      sop-admin/sop-admin-front/assets/js/routerole.js
  16. 31
      sop-admin/sop-admin-front/assets/lib/layuiext/Form.js
  17. 14
      sop-admin/sop-admin-front/index.html
  18. 96
      sop-admin/sop-admin-front/pages/isv/isvAdd.html
  19. 39
      sop-admin/sop-admin-front/pages/isv/isvAdd.js
  20. 51
      sop-admin/sop-admin-front/pages/isv/isvList.html
  21. 114
      sop-admin/sop-admin-front/pages/isv/isvList.js
  22. 97
      sop-admin/sop-admin-front/pages/isv/isvUpdate.html
  23. 62
      sop-admin/sop-admin-front/pages/isv/isvUpdate.js
  24. 34
      sop-admin/sop-admin-front/pages/service/routeManager.html
  25. 75
      sop-admin/sop-admin-front/pages/service/routeManager.js
  26. 2
      sop-admin/sop-admin-server/pom.xml
  27. 79
      sop-admin/sop-admin-server/sop.sql
  28. 18
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/IdParam.java
  29. 139
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/IsvApi.java
  30. 43
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/RoleApi.java
  31. 14
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/param/IsvInfoForm.java
  32. 10
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/param/IsvPageParam.java
  33. 9
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/result/AppKeySecretVo.java
  34. 23
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/result/IsvFormVO.java
  35. 10
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/result/IsvVO.java
  36. 10
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/result/PubPriVo.java
  37. 3
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/result/RoleVO.java
  38. 96
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/RouteApi.java
  39. 2
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/ServiceApi.java
  40. 10
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/RouteParam.java
  41. 22
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/RoutePermissionParam.java
  42. 9
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/ServiceSearchParam.java
  43. 17
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/result/RouteVO.java
  44. 13
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/system/SystemApi.java
  45. 18
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ChannelMsg.java
  46. 5
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/GatewayRouteDefinition.java
  47. 16
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/IsvRoutePermission.java
  48. 10
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/SopAdminConstants.java
  49. 47
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ZookeeperContext.java
  50. 13
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/config/WebConfig.java
  51. 4
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/entity/PermIsvRole.java
  52. 2
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/entity/PermRolePermission.java
  53. 34
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/service/PermService.java
  54. 104
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/service/RoutePermissionService.java
  55. 13
      sop-admin/sop-admin-server/src/main/resources/application-dev.yml
  56. 2
      sop-common/pom.xml
  57. 2
      sop-common/sop-gateway-common/pom.xml
  58. 24
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiConfig.java
  59. 5
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/BaseRouteDefinition.java
  60. 12
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ChannelMsg.java
  61. 25
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/Isv.java
  62. 55
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/IsvDefinition.java
  63. 18
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/IsvRoutePermission.java
  64. 12
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/SopConstants.java
  65. 3
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java
  66. 37
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/BaseRouteManager.java
  67. 47
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultIsvRoutePermissionManager.java
  68. 26
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/EnvironmentContext.java
  69. 34
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/IsvRoutePermissionManager.java
  70. 12
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/RoutePermissionManager.java
  71. 194
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ZookeeperContext.java
  72. 4
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/message/ErrorEnum.java
  73. 43
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/CacheIsvManager.java
  74. 24
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/secret/IsvManager.java
  75. 25
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/validate/ApiValidator.java
  76. 10
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/configuration/BaseZuulConfiguration.java
  77. 2
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/filter/BaseZuulFilter.java
  78. 49
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/filter/PreRoutePermissionFilter.java
  79. 3
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/filter/PreValidateFilter.java
  80. 4
      sop-common/sop-gateway-common/src/main/resources/i18n/open/error_en.properties
  81. 4
      sop-common/sop-gateway-common/src/main/resources/i18n/open/error_zh_CN.properties
  82. 2
      sop-common/sop-service-common/pom.xml
  83. 5
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiAbility.java
  84. 13
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/annotation/ApiMapping.java
  85. 2
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceApiInfo.java
  86. 5
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/bean/ServiceConfig.java
  87. 1
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/manager/DefaultRequestMappingEvent.java
  88. 8
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/manager/ServiceZookeeperApiMetaManager.java
  89. 9
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingHandlerMapping.java
  90. 1
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/mapping/ApiMappingInfo.java
  91. 5
      sop-common/sop-service-common/src/main/java/com/gitee/sop/servercommon/route/GatewayRouteDefinition.java
  92. 2
      sop-example/sop-story/sop-story-web/pom.xml
  93. 23
      sop-example/sop-story/sop-story-web/src/main/java/com/gitee/sop/bookweb/controller/PermissionDemoController.java
  94. 19
      sop-gateway/pom.xml
  95. 14
      sop-gateway/src/main/java/com/gitee/sop/gateway/config/GatewayConfig.java
  96. 14
      sop-gateway/src/main/java/com/gitee/sop/gateway/config/ZuulConfig.java
  97. 48
      sop-gateway/src/main/java/com/gitee/sop/gateway/entity/IsvInfo.java
  98. 39
      sop-gateway/src/main/java/com/gitee/sop/gateway/entity/PermIsvRole.java
  99. 39
      sop-gateway/src/main/java/com/gitee/sop/gateway/entity/PermRolePermission.java
  100. 70
      sop-gateway/src/main/java/com/gitee/sop/gateway/manager/DbIsvManager.java
  101. Some files were not shown because too many files have changed in this diff Show More

@ -38,11 +38,12 @@ SOP封装了开放平台大部分功能包括:签名验证、统一异常处
- 关闭签名校验功能
- 整合[easyopen](https://gitee.com/durcframework/easyopen)
- Admin管理平台,统一管理微服务配置,管理路由管理,微服务上下线
- 接入方管理+秘钥管理
- 接口权限分配
## 后期规划
- 接入方管理+秘钥管理
- 接口权限分配
- SDK
- Spring Cloud Config(Zookeeper)
@ -54,6 +55,8 @@ SOP封装了开放平台大部分功能包括:签名验证、统一异常处
![admin预览](https://images.gitee.com/uploads/images/2019/0323/142643_6fc1b152_332975.png "admin3.png")
![admin预览](https://images.gitee.com/uploads/images/2019/0401/202931_14cc7248_332975.png "admin7.png")
## 工程说明
> 运行环境:JDK8,Maven3,Zookeeper

@ -0,0 +1,10 @@
# changelog
## 1.1.0
- 新增接入方管理
- 新增接口授权
## 1.0.0
- 第一次发布

@ -1,15 +1,17 @@
* [首页](/?t=1553593844358)
* [首页](/?t=1554123435599)
* 开发文档
* [快速体验](files/10010_快速体验.md?t=1553593844358)
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1553593844380)
* [新增接口](files/10020_新增接口.md?t=1553593844380)
* [业务参数校验](files/10030_业务参数校验.md?t=1553593844380)
* [错误处理](files/10040_错误处理.md?t=1553593844380)
* [接口交互详解](files/10050_接口交互详解.md?t=1553593844380)
* [使用SpringCloudGateway](files/10060_使用SpringCloudGateway.md?t=1553593844380)
* [easyopen支持](files/10070_easyopen支持.md?t=1553593844380)
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1553593844380)
* [快速体验](files/10010_快速体验.md?t=1554123435601)
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1554123435620)
* [新增接口](files/10020_新增接口.md?t=1554123435620)
* [业务参数校验](files/10030_业务参数校验.md?t=1554123435620)
* [错误处理](files/10040_错误处理.md?t=1554123435620)
* [接口交互详解](files/10050_接口交互详解.md?t=1554123435621)
* [使用SpringCloudGateway](files/10060_使用SpringCloudGateway.md?t=1554123435621)
* [easyopen支持](files/10070_easyopen支持.md?t=1554123435621)
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1554123435621)
* [ISV管理](files/10085_ISV管理.md?t=1554123435621)
* [路由授权](files/10090_路由授权.md?t=1554123435621)
* 原理分析
* [原理分析之@ApiMapping](files/90010_原理分析之@ApiMapping.md?t=1553593844380)
* [原理分析之路由存储](files/90011_原理分析之路由存储.md?t=1553593844381)
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1553593844381)
* [原理分析之@ApiMapping](files/90010_原理分析之@ApiMapping.md?t=1554123435621)
* [原理分析之路由存储](files/90011_原理分析之路由存储.md?t=1554123435622)
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1554123435622)

@ -1,19 +1,21 @@
# 快速体验
> 运行环境:JDK8,Maven3,Zookeeper
> 运行环境:JDK8,Maven3,Zookeeper,Mysql
- 安装并启动zookeeper,[安装教程](http://zookeeper.apache.org/doc/r3.4.13/zookeeperStarted.html)
- 执行Mysql脚本`sop.sql`
- IDE打开项目(IDEA下可以打开根pom.xml,然后open as project)
- 启动注册中心,sop-registry(运行SopRegistryApplication.java)
- 启动微服务:sop-story-web(运行SopStoryApplication.java)
- 启动网关:sop-gateway(运行SopGatewayApplication.java)
- 启动网关:打开sop-gateway下的`application.yml`,修改数据库`username/password`,SopGatewayApplication.java
- 找到sop-test,打开测试用例,进行接口调用测试,运行com.gitee.sop.AlipayClientPostTest.testPost()
确保注册中心先启动
## 使用admin
- 找到`sop-admin/sop-admin-server`工程,运行`com.gitee.sop.adminserver.SopAdminServerApplication.java`
- 找到`sop-admin/sop-admin-server`工程,打开sop-admin-server下的`application-dev.yml`,修改数据库`username/password`
- 运行`com.gitee.sop.adminserver.SopAdminServerApplication.java`
- 找到`sop-admin/sop-admin-front/index.html`文件,在IDEA下直接右键--Run'index.html'
- 如果没有用到IDEA,则需要把sop-admin-front放到静态服务器中然后访问index.html

@ -0,0 +1,14 @@
# ISV管理
ISV:独立软体开发商(independent software vendor),即接入方或者说接口调用者,在SOP中称为ISV。
---
在1.1.0版本中新增了ISV管理功能,在sop-admin中ISV管理模块下。功能如下:
- 基本信息的增查改
- 设置对应角色
界面如下图所示:
![admin预览](images/10085_1.png "10085_1.png")

@ -0,0 +1,25 @@
# 路由授权
1.1.0版本新增了路由授权功能,采用RBAC权限管理方式实现。
- 每个ISV(appKey)对应一个或多个角色
- 每个角色分配多个路由权限
接口跟角色相关联,ISV拥有哪些角色,就具有角色对应的接口访问权限。
假设把路由a,b,c分配给了`VIP角色`,那么具有VIP角色的ISV可以访问a,b,c三个路由。
默认情况下,接口访问时公开的,ISV都能访问。如果要设置某个接口需要授权,在`@ApiMapping`注解中指定permission=true。
如:`@ApiMapping(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`测试用例进行验证

@ -6,7 +6,7 @@ SOP将路由信息存到了zookeeper当中,服务在启动时,将自己的
zookeeper存储路由的结构如下:
```xml
/com.gitee.sop.route-<profile> 根节点
/com.gitee.sop.route 根节点
/serviceId 服务节点,名字为服务名
/route1 路由节点,名字为:name+version,存放路由信息
/route2

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

@ -31,7 +31,10 @@
<body>
<div id="app">加载中 ...</div>
<script>
window.$docsify = {
window.$docsify = {
alias: {
'/.*/_sidebar.md': '/_sidebar.md?t=' + new Date().getTime()
},
auto2top: true,
coverpage: false,
executeScript: true,

@ -7,6 +7,7 @@ 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;
@ -36,11 +37,15 @@ public class SidebarTest {
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();

@ -675,3 +675,4 @@ table th, table td {
color: #393D49;
}
.x-win {display: none;padding: 20px;}

@ -1,30 +0,0 @@
;(function () {
var currentProfile = ApiUtil.getParam('profile') || 'default';
ApiUtil.post('system.profile.list', {}, function (resp) {
var profileList = resp.data;
var html = ['<div class="layui-tab layui-tab-brief" style="margin-top: 0px;">'];
html.push('<ul id="profileList" class="layui-tab-title">')
for (var i = 0; i < profileList.length; i++) {
var profile = profileList[i];
var cls = currentProfile == profile ? 'layui-this' : '';
html.push('<li><a class="' + cls + '" href="' + getCurrentPage(profile) + '">' + profile + '</a></li>');
}
html.push('</ul>')
html.push('</div>')
$('.x-body').prepend(html.join(''));
});
function getCurrentPage(profile) {
var currentUrl = location.href.toString();
var indexStart = currentUrl.lastIndexOf('/') + 1;
var indexEnd = currentUrl.lastIndexOf('?');
var page = indexEnd > -1
? currentUrl.substring(indexStart, indexEnd)
: currentUrl.substring(indexStart);
return page + '?q=' + new Date().getTime() + '&profile=' + profile;
}
window.profile = currentProfile;
})();

@ -0,0 +1,22 @@
var RouteRole = {
loadAllRole: function (form, contentId, callback) {
ApiUtil.post('role.listall', {}, function (resp) {
var roles = resp.data;
var html = []
for (var i = 0; i < roles.length; i++) {
if (i > 0 && i % 5 === 0) {
html.push('<br>');
}
var role = roles[i];
html.push('<input type="checkbox" name="roleCode" value="'+role.roleCode+'" lay-skin="primary" title="'+role.description+'">');
}
$('#' + contentId).html(html.join(''));
// 如果你的HTML是动态生成的,自动渲染就会失效
// 因此你需要在相应的地方,执行下述方法来手动渲染,跟这类似的还有 element.init();
form.render();
callback && callback();
});
}
}

@ -33,9 +33,11 @@ layui.define(function (exports) {
, parseForm: function ($form) {
var that = this;
this.form = $form[0];
this.$els = $form.find('input,select,textarea');
}
, getEls: function () {
return this.$form.find('input,select,textarea');
}
/**
* 同load(data)
@ -80,7 +82,7 @@ layui.define(function (exports) {
* 清除表单中的值,清除错误信息
*/
, clear: function () {
this.$els.each(function () {
this.getEls().each(function () {
var _$el = $(this);
if (_$el.is(':radio') || _$el.is(':checkbox')) {
this.checked = false;
@ -116,7 +118,7 @@ layui.define(function (exports) {
var that = this;
var data = {};
this.$els.each(function () {
this.getEls().each(function () {
var value = that._getInputVal($(this));
if (value) {
var name = this.name;
@ -139,29 +141,6 @@ layui.define(function (exports) {
return data;
}
, getFormData: function ($form) {
var data = {};
var dataArr = $form.serializeArray();
for (var i = 0, len = dataArr.length; i < len; i++) {
var item = dataArr[i];
var name = item.name;
var itemValue = item.value;
var dataValue = data[name];
if (dataValue) {
if ($.isArray(dataValue)) {
dataValue.push(itemValue);
} else {
data[name] = [dataValue, itemValue];
}
} else {
data[name] = itemValue;
}
}
return data;
}
, _getInputVal: function ($input) {
if ($input.is(":radio") || $input.is(":checkbox")) {
if ($input.is(':checked')) {

@ -85,6 +85,20 @@
</li>
</ul>
</li>
<li class="open">
<a>
<i class="layui-icon layui-icon-layer"></i>
<cite>ISV管理</cite>
</a>
<ul class="sub-menu">
<li date-refresh="1">
<a href="pages/isv/isvList.html">
<i class="layui-icon layui-icon-table"></i>
<cite>ISV列表</cite>
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html class="x-admin-sm">
<head>
<meta charset="UTF-8">
<title>SOP Admin</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport"
content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8"/>
<link rel="stylesheet" href="../../assets/css/font.css">
<link rel="stylesheet" href="../../assets/css/xadmin.css">
<style>
.layui-form textarea{min-height: 60px;}
</style>
</head>
<body>
<div class="x-nav">
<span class="layui-breadcrumb">
<a href="../welcome.html">首页</a>
<a><cite>ISV管理</cite></a>
<a href="isvList.html">ISV列表</a>
<a><cite>添加ISV</cite></a>
</span>
<a class="layui-btn layui-btn-small layui-btn-normal" style="line-height:1.6em;margin-top:4px;float:right"
href="javascript:location.replace(location.href);" title="刷新">
<i class="layui-icon layui-icon-refresh" style="line-height:30px"></i></a>
</div>
<div class="x-body">
<fieldset class="layui-elem-field layui-field-title">
<legend>添加ISV</legend>
</fieldset>
<form id="addForm" style="width: 500px" class="layui-form" action="">
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button id="createFormDataBtn" class="layui-btn layui-btn-primary">一键生成数据</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">appKey</label>
<div class="layui-input-block">
<input type="text" name="appKey" lay-verify="required" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">签名方式</label>
<div class="layui-input-block">
<input type="radio" name="signType" value="1" checked="checked" lay-filter="signTypeFilter" title="RSA2[推荐]">
<input type="radio" name="signType" value="2" lay-filter="signTypeFilter" title="MD5">
</div>
</div>
<div class="layui-form-item sign-type-md5" style="display: none;">
<label class="layui-form-label">secret</label>
<div class="layui-input-block">
<input type="text" name="secret" class="layui-input"/>
</div>
</div>
<div class="layui-form-item sign-type-rsa2">
<label class="layui-form-label">公钥</label>
<div class="layui-input-block">
<textarea name="pubKey" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item sign-type-rsa2">
<label class="layui-form-label">私钥</label>
<div class="layui-input-block">
<textarea name="priKey" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">角色</label>
<div id="roleArea" class="layui-input-block">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" checked="checked" title="启用">
<input type="radio" name="status" value="2" title="<span class='x-red'>禁用</span>">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="addFormSubmitFilter">保存</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="location.href='isvList.html'">取消</button>
</div>
</div>
</form>
</div>
<script type="text/javascript" src="../../assets/js/lib.js"></script>
<script type="text/javascript" src="isvAdd.js"></script>
<script>
</script>
</body>
</html>

@ -0,0 +1,39 @@
lib.importJs('../../assets/js/routerole.js')
.use(['element', 'table', 'form'], function () {
var table = layui.table;
var layer = layui.layer;
var form = layui.form;
var $ = layui.jquery;
var addForm = layui.Form('addForm');
$('#createFormDataBtn').click(function () {
ApiUtil.post('isv.form.gen', {}, function (resp) {
addForm.setData(resp.data);
});
return false;
});
form.on('radio(signTypeFilter)', function(data){
if (data.value == 1) {
$('.sign-type-rsa2').show();
$('.sign-type-md5').hide();
} else {
$('.sign-type-rsa2').hide();
$('.sign-type-md5').show();
}
});
form.on('submit(addFormSubmitFilter)', function (data) {
var param = addForm.getData();
ApiUtil.post('isv.info.add', param, function (resp) {
layer.alert('添加成功', function () {
location.href = 'isvList.html';
})
})
return false;
});
RouteRole.loadAllRole(form, 'roleArea');
});

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html class="x-admin-sm">
<head>
<meta charset="UTF-8">
<title>SOP Admin</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport"
content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8"/>
<link rel="stylesheet" href="../../assets/css/font.css">
<link rel="stylesheet" href="../../assets/css/xadmin.css">
</head>
<body>
<div class="x-nav">
<span class="layui-breadcrumb">
<a href="../welcome.html">首页</a>
<a><cite>ISV管理</cite></a>
<a><cite>ISV列表</cite></a>
</span>
<a class="layui-btn layui-btn-small layui-btn-normal" style="line-height:1.6em;margin-top:4px;float:right"
href="javascript:location.replace(location.href);" title="刷新">
<i class="layui-icon layui-icon-refresh" style="line-height:30px"></i></a>
</div>
<div class="x-body">
<div class="layui-row">
<form class="layui-form layui-col-md12 x-so" action="" lay-filter="searchFrm">
appKey:
<input name="appKey" class="layui-input" style="width: 200px;" placeholder="">
<button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="searchFilter">
<i class="layui-icon layui-icon-search"></i>搜索
</button>
</form>
</div>
<script type="text/html" id="toolbar">
<div class="layui-btn-container">
<button class="layui-btn layui-btn-ms layui-btn-normal" lay-event="add">
<i class="layui-icon layui-icon-add-1"></i>添加ISV
</button>
</div>
</script>
<table class="layui-hide" id="isvTable" lay-filter="isvTableFilter"></table>
</div>
<script type="text/javascript" src="../../assets/js/lib.js"></script>
<script type="text/javascript" src="isvList.js"></script>
<script>
</script>
</body>
</html>

@ -0,0 +1,114 @@
lib.use(['element', 'table', 'form'], function () {
var table = layui.table;
var layer = layui.layer;
var form = layui.form;
var isvTable;
var STATUS_ENUM = {
'1': '<span class="x-green">已启用</span>'
,'2': '<span class="x-red">已禁用</span>'
}
// 渲染表格
var renderTable = function (postData) {
var params = {
data: JSON.stringify(postData || {})
};
layer.load(2);
isvTable = table.render({
elem: '#isvTable'
, toolbar: '#toolbar'
, url: ApiUtil.createUrl('isv.info.page')
// 对分页请求的参数:page、limit重新设定名称
,request: {
pageName: 'pageIndex' //页码的参数名称,默认:page
,limitName: 'pageSize' //每页数据量的参数名,默认:limit
}
, page: true
, where: params
, cellMinWidth: 80 //全局定义常规单元格的最小宽度,layui 2.2.1 新增
, cols: [[
{field: 'id', title: 'id', width: 80}
, {field: 'appKey', title: 'appKey', width: 250}
, {field: 'secret', title: 'secret', width: 80, templet: function (row) {
return '<button class="layui-btn layui-btn-xs" onclick="View.secret(\''+row.secret+'\')">查看</button>';
}}
, {field: '', title: '公私钥', width: 80, templet: function (row) {
return '<button class="layui-btn layui-btn-xs" onclick="View.pubPriKey(\''+row.pubKey+'\', \''+row.priKey+'\')">查看</button>';
}}
, {field: 'roleList', title: '角色', templet: function (row) {
var html = [];
var roleList = row.roleList;
for (var i = 0; i < roleList.length; i++) {
html.push(roleList[i].description);
}
return html.join(', ');
}}
, {field: 'status', title: '状态', width: 80, templet: function (row) {
var display = STATUS_ENUM[row.status + ''];
return display ? display : 'unknown';
}}
, {field: 'gmtCreate', title: '添加时间', width: 160}
, {field: 'gmtModified', title: '修改时间', width: 160}
, {
fixed: 'right', title: '操作', width: 100, templet: function (row) {
return '<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="edit">修改</a>';
}
}
]]
,parseData: function(res){ //将原始数据解析成 table 组件所规定的数据
return {
"code": res.code, //解析接口状态
"msg": res.msg, //解析提示文本
"count": res.data.total, //解析数据长度
"data": res.data.list //解析数据列表
};
}
,done: function () {
layer.closeAll('loading');
}
});
//监听单元格事件
table.on('tool(isvTableFilter)', function(obj) {
if (obj.event === 'edit') {
location.href = 'isvUpdate.html?id=' + obj.data.id;
}
});
table.on('toolbar(isvTableFilter)', function(obj) {
if (obj.event === 'add') {
location.href = 'isvAdd.html';
}
});
};
renderTable();
form.on('submit(searchFilter)', function(data){
var param = data.field;
renderTable(param)
return false;
});
window.View = {
secret: function (secret) {
layer.alert(secret);
}
,pubPriKey: function (pubKey, priKey) {
var content = '<div style="width: 550px;padding: 10px;">公钥:<textarea class="layui-textarea" readonly="readonly">' + pubKey + '</textarea><br>' +
'私钥:<textarea class="layui-textarea" readonly="readonly">' + priKey + '</textarea></div>';
layer.open({
type: 1,
area: ['600px', '400px'],
fix: false, //不固定
shadeClose: true,
shade:0.4,
title: '公私钥',
content: content
});
}
}
});

@ -0,0 +1,97 @@
<!DOCTYPE html>
<html class="x-admin-sm">
<head>
<meta charset="UTF-8">
<title>SOP Admin</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport"
content="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8"/>
<link rel="stylesheet" href="../../assets/css/font.css">
<link rel="stylesheet" href="../../assets/css/xadmin.css">
<style>
.layui-form textarea{min-height: 60px;}
</style>
</head>
<body>
<div class="x-nav">
<span class="layui-breadcrumb">
<a href="../welcome.html">首页</a>
<a><cite>ISV管理</cite></a>
<a href="isvList.html">ISV列表</a>
<a><cite>修改ISV</cite></a>
</span>
<a class="layui-btn layui-btn-small layui-btn-normal" style="line-height:1.6em;margin-top:4px;float:right"
href="javascript:location.replace(location.href);" title="刷新">
<i class="layui-icon layui-icon-refresh" style="line-height:30px"></i></a>
</div>
<div class="x-body">
<fieldset class="layui-elem-field layui-field-title">
<legend>修改ISV</legend>
</fieldset>
<form id="updateForm" style="width: 500px" class="layui-form" action="">
<input name="id" type="hidden">
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<button id="createFormDataBtn" class="layui-btn layui-btn-primary">一键生成数据</button>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">appKey</label>
<div class="layui-input-block">
<input type="text" name="appKey" lay-verify="required" class="layui-input" readonly="readonly"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">签名方式</label>
<div class="layui-input-block">
<input type="radio" name="signType" value="1" checked="checked" lay-filter="signTypeFilter" title="RSA2[推荐]">
<input type="radio" name="signType" value="2" lay-filter="signTypeFilter" title="MD5">
</div>
</div>
<div class="layui-form-item sign-type-md5" style="display: none;">
<label class="layui-form-label">secret</label>
<div class="layui-input-block">
<input type="text" name="secret" class="layui-input"/>
</div>
</div>
<div class="layui-form-item sign-type-rsa2">
<label class="layui-form-label">公钥</label>
<div class="layui-input-block">
<textarea name="pubKey" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item sign-type-rsa2">
<label class="layui-form-label">私钥</label>
<div class="layui-input-block">
<textarea name="priKey" class="layui-textarea"></textarea>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">角色</label>
<div id="roleArea" class="layui-input-block">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">状态</label>
<div class="layui-input-block">
<input type="radio" name="status" value="1" checked="checked" title="启用">
<input type="radio" name="status" value="2" title="<span class='x-red'>禁用</span>">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="updateFormSubmitFilter">保存</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="location.href='isvList.html'">取消</button>
</div>
</div>
</form>
</div>
<script type="text/javascript" src="../../assets/js/lib.js"></script>
<script type="text/javascript" src="isvUpdate.js"></script>
<script>
</script>
</body>
</html>

@ -0,0 +1,62 @@
lib.importJs('../../assets/js/routerole.js')
.use(['element', 'table', 'form'], function () {
var table = layui.table;
var layer = layui.layer;
var form = layui.form;
var $ = layui.jquery;
var updateForm = layui.Form('updateForm');
$('#createFormDataBtn').click(function () {
ApiUtil.post('isv.form.gen', {}, function (resp) {
var data = resp.data;
var appKey = updateForm.getData('appKey');
data.appKey = appKey;
updateForm.setData(data);
});
return false;
});
form.on('radio(signTypeFilter)', function(data){
if (data.value == 1) {
$('.sign-type-rsa2').show();
$('.sign-type-md5').hide();
} else {
$('.sign-type-rsa2').hide();
$('.sign-type-md5').show();
}
});
form.on('submit(updateFormSubmitFilter)', function (data) {
var param = updateForm.getData();
ApiUtil.post('isv.info.update', param, function (resp) {
layer.alert('修改成功', function () {
location.href = 'isvList.html';
})
})
return false;
});
RouteRole.loadAllRole(form, 'roleArea', function () {
loadFormData();
})
function loadFormData() {
var id = ApiUtil.getParam('id');
if (!id) {
alert('id错误');
return;
}
ApiUtil.post('isv.info.get', {id: id}, function (resp) {
var isvInfo = resp.data;
var roleList = isvInfo.roleList;
var roleCode = [];
for (var i = 0; i < roleList.length; i++) {
roleCode.push(roleList[i].roleCode);
}
isvInfo.roleCode = roleCode;
updateForm.setData(isvInfo);
});
}
});

@ -22,9 +22,6 @@
<i class="layui-icon layui-icon-refresh" style="line-height:30px"></i></a>
</div>
<div class="x-body">
<div class="layui-row">
</div>
<div class="layui-row">
<div class="layui-col-md2">
<ul id="leftTree"></ul>
@ -57,14 +54,13 @@
</div>
<div id="addWin" style="display: none;padding: 20px;">
<div id="addWin" class="x-win">
<form id="addForm" class="layui-form" action="" lay-filter="addWinFilter">
<input type="hidden" name="serviceId" />
<input type="hidden" name="profile" />
<div class="layui-form-item">
<label class="layui-form-label">id</label>
<div class="layui-input-block">
<input type="text" name="id" class="layui-input"/>
<input type="text" name="id" lay-verify="required" class="layui-input"/>
</div>
</div>
<div class="layui-form-item">
@ -109,10 +105,9 @@
</form>
</div>
<div id="updateWin" style="display: none;padding: 20px;">
<div id="updateWin" class="x-win">
<form id="updateForm" class="layui-form" action="" lay-filter="updateWinFilter">
<input type="hidden" name="serviceId" />
<input type="hidden" name="profile" />
<div class="layui-form-item">
<label class="layui-form-label">id</label>
<div class="layui-input-block">
@ -160,6 +155,29 @@
</div>
</form>
</div>
<div id="authWin" class="x-win">
<form id="authForm" style="width: 400px" class="layui-form" action="">
<div class="layui-form-item">
<label class="layui-form-label">id</label>
<div class="layui-input-block">
<input type="text" name="routeId" class="layui-input" readonly="readonly"/>
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">角色</label>
<div id="roleArea" class="layui-input-block">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn layui-btn-normal" lay-submit="" lay-filter="authFormSubmitFilter">保存</button>
<button type="button" class="layui-btn layui-btn-primary" onclick="layer.closeAll()">取消</button>
</div>
</div>
</form>
</div>
<script type="text/javascript" src="../../assets/js/lib.js"></script>
<script type="text/javascript" src="routeManager.js"></script>
</body>

@ -1,13 +1,14 @@
lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'form'], function () {
lib.importJs('../../assets/js/routerole.js')
.use(['element', 'table', 'tree', 'form'], function () {
var ROUTE_STATUS = {
'0': '待审核'
,'1': '<span class="x-green">已启用</span>'
,'2': '<span class="x-red">已禁用</span>'
}
var profile = window.profile;
var form = layui.form;
var updateForm = layui.Form('updateForm');
var addForm = layui.Form('addForm');
var authForm = layui.Form('authForm');
var table = layui.table;
var currentServiceId;
@ -40,8 +41,18 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
return false;
});
form.on('submit(authFormSubmitFilter)', function(data) {
var params = authForm.getData();
ApiUtil.post('route.role.update', params, function (resp) {
layer.closeAll();
routeTable.reload();
})
return false;
});
function initTree() {
ApiUtil.post('service.list', {profile: profile}, function (resp) {
ApiUtil.post('service.list', {}, function (resp) {
var serviceList = resp.data;
var children = [];
for (var i = 0; i < serviceList.length; i++) {
@ -55,7 +66,7 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
layui.tree({
elem: '#leftTree' //传入元素选择器
, nodes: [{ //节点
name: '服务列表(' + profile + ')'
name: '服务列表'
, spread: true // 展开
, children: children
}]
@ -80,7 +91,7 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
searchTable({
serviceId: serviceId
});
smTitle = '[ <strong>profile:</strong>' + profile + '&nbsp;&nbsp;&nbsp;&nbsp;<strong>serviceId:</strong>' + currentServiceId + ' ]';
smTitle = '[ <strong>serviceId:</strong>' + currentServiceId + ' ]';
}
/**
@ -90,11 +101,9 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
function searchTable(params) {
var postData = {
data: JSON.stringify(params)
, profile: profile
};
if (!routeTable) {
routeTable = initTable(postData);
routeTable = renderTable(postData);
} else {
routeTable.reload({
where: postData
@ -102,7 +111,7 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
}
}
function initTable(postData) {
function renderTable(postData) {
var routeTable = table.render({
elem: '#routeTable'
, toolbar: '#toolbar'
@ -110,9 +119,20 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
, where: postData
, cellMinWidth: 80 //全局定义常规单元格的最小宽度,layui 2.2.1 新增
, cols: [[
{field: 'id', title: 'id(接口名+版本号)', width: 250}
{field: 'id', title: 'id(接口名+版本号)'}
, {field: 'uri', title: 'uri', width: 200}
, {field: 'path', title: 'path', width: 200}
, {field: 'path', title: 'path'}
, {field: 'roles', title: '访问权限', width: 100, templet: function (row) {
if (!row.permission) {
return '(公开)';
}
var html = [];
var roles = row.roles;
for (var i = 0; i < roles.length; i++) {
html.push(roles[i].description);
}
return html.length > 0 ? html.join(', ') : '<span class="x-red">未授权</span>';
}}
, {
field: 'ignoreValidate', width: 80, title: '忽略验证', templet: function (row) {
return row.ignoreValidate ? '<span class="x-red">是</span>' : '<span>否</span>';
@ -129,8 +149,12 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
}
}
, {
fixed: 'right', title: '操作', width: 100, templet: function (row) {
return '<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="edit">修改</a>';
fixed: 'right', title: '操作', width: 150, templet: function (row) {
var html = ['<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="edit">修改</a>'];
if (row.permission) {
html.push('<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="auth">授权</a>');
}
return html.join('');
}
}
]]
@ -139,9 +163,9 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
//监听单元格事件
table.on('tool(routeTableFilter)', function(obj) {
var data = obj.data;
if(obj.event === 'edit'){
var event = obj.event;
if(event === 'edit'){
//表单初始赋值
data.profile = profile;
data.serviceId = currentServiceId;
updateForm.setData(data);
@ -152,12 +176,29 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
,area: ['500px', '460px']
,content: $('#updateWin') //这里content是一个DOM,注意:最好该元素要存放在body最外层,否则可能被其它的相对元素所影响
});
} else if (event === 'auth') {
ApiUtil.post('route.role.get', {id: data.id}, function (resp) {
var roleList = resp.data;
var roleCode = [];
for (var i = 0; i < roleList.length; i++) {
roleCode.push(roleList[i].roleCode);
}
authForm.setData({
routeId: data.id
, roleCode: roleCode
})
layer.open({
type: 1
,title: '路由授权'
,area: ['500px', '260px']
,content: $('#authWin')
});
})
}
});
table.on('toolbar(routeTableFilter)', function(obj) {
if (obj.event === 'add') {
var data = {};
data.profile = profile;
data.serviceId = currentServiceId;
data.id = '';
// 新加的路由先设置成禁用
@ -178,4 +219,6 @@ lib.importJs('../../assets/js/profile.js').use(['element', 'table', 'tree', 'for
initTree();
RouteRole.loadAllRole(form, 'roleArea');
});

@ -43,7 +43,7 @@
<dependency>
<groupId>net.oschina.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
<version>1.5.1</version>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>

@ -1,79 +0,0 @@
/*
Navicat Premium Data Transfer
Source Server : mysql-localhost
Source Server Type : MySQL
Source Server Version : 50724
Source Host : localhost:3306
Source Schema : sop
Target Server Type : MySQL
Target Server Version : 50724
File Encoding : 65001
Date: 27/03/2019 20:16:41
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for isv_info
-- ----------------------------
DROP TABLE IF EXISTS `isv_info`;
CREATE TABLE `isv_info` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`app_key` varchar(100) NOT NULL COMMENT 'appKey',
`secret` varchar(200) NOT NULL COMMENT 'secret',
`pub_key` text COMMENT '公钥',
`pri_key` text COMMENT '私钥',
`status` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '0启用,1禁用',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_app_key` (`app_key`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='isv信息表';
-- ----------------------------
-- Table structure for perm_isv_role
-- ----------------------------
DROP TABLE IF EXISTS `perm_isv_role`;
CREATE TABLE `perm_isv_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`isv_info_id` bigint(20) NOT NULL COMMENT 'isv_info.id',
`role_code` varchar(50) NOT NULL COMMENT '角色code',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`isv_info_id`,`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='isv角色';
-- ----------------------------
-- Table structure for perm_role
-- ----------------------------
DROP TABLE IF EXISTS `perm_role`;
CREATE TABLE `perm_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`role_code` varchar(50) NOT NULL COMMENT '角色代码',
`description` varchar(50) NOT NULL COMMENT '角色描述',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
-- ----------------------------
-- Table structure for perm_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `perm_role_permission`;
CREATE TABLE `perm_role_permission` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`role_code` varchar(50) NOT NULL COMMENT '角色表code',
`route_id` bigint(20) NOT NULL COMMENT 'api_id',
`gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_perm` (`role_code`,`route_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限表';
SET FOREIGN_KEY_CHECKS = 1;

@ -0,0 +1,18 @@
package com.gitee.sop.adminserver.api;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
/**
* @author tanghc
*/
@Getter
@Setter
public class IdParam {
@NotNull(message = "id不能为空")
@ApiDocField(description = "id")
private Long id;
}

@ -1,23 +1,29 @@
package com.gitee.sop.adminserver.api.isv;
import com.alibaba.fastjson.JSON;
import com.gitee.easyopen.annotation.Api;
import com.gitee.easyopen.annotation.ApiService;
import com.gitee.easyopen.doc.DataType;
import com.gitee.easyopen.doc.annotation.ApiDoc;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import com.gitee.easyopen.doc.annotation.ApiDocMethod;
import com.gitee.easyopen.exception.ApiException;
import com.gitee.easyopen.util.CopyUtil;
import com.gitee.easyopen.util.KeyStore;
import com.gitee.easyopen.util.RSAUtil;
import com.gitee.fastmybatis.core.PageInfo;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.fastmybatis.core.support.PageEasyui;
import com.gitee.fastmybatis.core.util.MapperUtil;
import com.gitee.sop.adminserver.api.IdParam;
import com.gitee.sop.adminserver.api.isv.param.IsvInfoForm;
import com.gitee.sop.adminserver.api.isv.param.IsvInfoFormAdd;
import com.gitee.sop.adminserver.api.isv.param.IsvInfoFormUpdate;
import com.gitee.sop.adminserver.api.isv.param.IsvPageParam;
import com.gitee.sop.adminserver.api.isv.result.AppKeySecretVo;
import com.gitee.sop.adminserver.api.isv.result.IsvFormVO;
import com.gitee.sop.adminserver.api.isv.result.IsvVO;
import com.gitee.sop.adminserver.api.isv.result.PubPriVo;
import com.gitee.sop.adminserver.api.isv.result.RoleVO;
import com.gitee.sop.adminserver.bean.ChannelMsg;
import com.gitee.sop.adminserver.bean.ZookeeperContext;
import com.gitee.sop.adminserver.common.IdGen;
import com.gitee.sop.adminserver.entity.IsvInfo;
import com.gitee.sop.adminserver.entity.PermIsvRole;
@ -25,9 +31,11 @@ import com.gitee.sop.adminserver.entity.PermRole;
import com.gitee.sop.adminserver.mapper.IsvInfoMapper;
import com.gitee.sop.adminserver.mapper.PermIsvRoleMapper;
import com.gitee.sop.adminserver.mapper.PermRoleMapper;
import com.gitee.sop.adminserver.service.PermService;
import com.gitee.sop.adminserver.service.RoutePermissionService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.text.SimpleDateFormat;
import java.util.Collections;
@ -40,6 +48,7 @@ import java.util.stream.Collectors;
*/
@ApiService
@ApiDoc("ISV管理")
@Slf4j
public class IsvApi {
@Autowired
@ -52,31 +61,50 @@ public class IsvApi {
PermRoleMapper permRoleMapper;
@Autowired
PermService permService;
RoutePermissionService routePermissionService;
@Api(name = "isv.info.page")
@ApiDocMethod(description = "接入方列表")
PageEasyui pageIsv(IsvPageParam param) {
@ApiDocMethod(description = "isv列表", results = {
@ApiDocField(name = "pageIndex", description = "第几页", dataType = DataType.INT, example = "1"),
@ApiDocField(name = "pageSize", description = "每页几条数据", dataType = DataType.INT, example = "10"),
@ApiDocField(name = "total", description = "每页几条数据", dataType = DataType.LONG, example = "100"),
@ApiDocField(name = "rows", description = "数据", dataType = DataType.ARRAY, elementClass = IsvVO.class)
})
PageInfo<IsvVO> pageIsv(IsvPageParam param) {
Query query = Query.build(param);
PageInfo<IsvInfo> pageInfo = MapperUtil.query(isvInfoMapper, query);
List<IsvInfo> list = pageInfo.getList();
List<IsvVO> retList = list.stream()
.map(isvInfo -> {
IsvVO vo = new IsvVO();
CopyUtil.copyProperties(isvInfo, vo);
vo.setRoleList(this.buildIsvRole(isvInfo));
return vo;
return buildIsvVO(isvInfo);
})
.collect(Collectors.toList());
PageEasyui<IsvVO> pageInfoRet = new PageEasyui<>();
PageInfo<IsvVO> pageInfoRet = new PageInfo<>();
pageInfoRet.setTotal(pageInfo.getTotal());
pageInfoRet.setList(retList);
return pageInfoRet;
}
@Api(name = "isv.info.get")
@ApiDocMethod(description = "获取isv")
IsvVO getIsvVO(IdParam param) {
IsvInfo isvInfo = isvInfoMapper.getById(param.getId());
return buildIsvVO(isvInfo);
}
private IsvVO buildIsvVO(IsvInfo isvInfo) {
if (isvInfo == null) {
return null;
}
IsvVO vo = new IsvVO();
CopyUtil.copyProperties(isvInfo, vo);
vo.setRoleList(this.buildIsvRole(isvInfo));
return vo;
}
/**
* 构建ISV拥有的角色
*
@ -84,7 +112,7 @@ public class IsvApi {
* @return
*/
List<RoleVO> buildIsvRole(IsvInfo permClient) {
List<String> roleCodeList = permService.listClientRoleCode(permClient.getId());
List<String> roleCodeList = routePermissionService.listClientRoleCode(permClient.getId());
if (CollectionUtils.isEmpty(roleCodeList)) {
return Collections.emptyList();
}
@ -101,64 +129,99 @@ public class IsvApi {
@Api(name = "isv.info.add")
@ApiDocMethod(description = "添加isv")
void addIsv(IsvInfoFormAdd param) {
@Transactional
void addIsv(IsvInfoFormAdd param) throws Exception {
if (isvInfoMapper.getByColumn("app_key", param.getAppKey()) != null) {
throw new ApiException("appKey已存在");
}
formatForm(param);
IsvInfo rec = new IsvInfo();
CopyUtil.copyPropertiesIgnoreNull(param, rec);
isvInfoMapper.saveIgnoreNull(rec);
if (CollectionUtils.isNotEmpty(param.getRoleCode())) {
this.saveIsvRole(rec, param.getRoleCode());
}
this.saveClientRole(rec, param.getRoleCode());
// TODO:发送消息队列到zookeeper
this.sendChannelMsg(rec);
}
@Api(name = "isv.info.update")
@ApiDocMethod(description = "修改isv")
@Transactional
void updateIsv(IsvInfoFormUpdate param) {
formatForm(param);
IsvInfo rec = isvInfoMapper.getById(param.getId());
CopyUtil.copyPropertiesIgnoreNull(param, rec);
isvInfoMapper.updateIgnoreNull(rec);
this.saveIsvRole(rec, param.getRoleCode());
this.saveClientRole(rec, param.getRoleCode());
this.sendChannelMsg(rec);
}
// syncService.syncAppSecretConfig(Sets.newHashSet(param.getApp()));
// TODO:发送消息队列到zookeeper
private void formatForm(IsvInfoForm form) {
if (form.getSignType() == 1) {
form.setSecret("");
} else {
form.setPubKey("");
form.setPriKey("");
}
}
@Api(name = "isv.pubprikey.create")
@ApiDocMethod(description = "生成公私钥")
PubPriVo createPubPriKey() throws Exception {
KeyStore keyStore = RSAUtil.createKeys();
PubPriVo vo = new PubPriVo();
vo.setPubKey(keyStore.getPublicKey());
vo.setPriKey(keyStore.getPrivateKey());
return vo;
private void sendChannelMsg(IsvInfo rec) {
ChannelMsg channelMsg = new ChannelMsg("update", rec);
String path = ZookeeperContext.getIsvInfoChannelPath();
String data = JSON.toJSONString(channelMsg);
try {
log.info("消息推送--ISV信息(update), path:{}, data:{}", path, data);
ZookeeperContext.updatePathData(path, data);
} catch (Exception e) {
log.error("发送isvChannelMsg失败, path:{}, msg:{}", path, data, e);
throw new ApiException("保存失败,请查看日志");
}
}
@Api(name = "isv.appkeysecret.create")
@ApiDocMethod(description = "生成appkey")
AppKeySecretVo createAppKeySecret() {
@Api(name = "isv.form.gen")
@ApiDocMethod(description = "isv表单内容一键生成")
IsvFormVO createIsvForm() throws Exception {
IsvFormVO isvFormVO = new IsvFormVO();
String appKey = new SimpleDateFormat("yyyyMMdd").format(new Date()) + IdGen.nextId();
String secret = IdGen.uuid();
AppKeySecretVo vo = new AppKeySecretVo();
vo.setAppKey(appKey);
vo.setSecret(secret);
return vo;
isvFormVO.setAppKey(appKey);
isvFormVO.setSecret(secret);
KeyStore keyStore = RSAUtil.createKeys();
isvFormVO.setPubKey(keyStore.getPublicKey());
isvFormVO.setPriKey(keyStore.getPrivateKey());
return isvFormVO;
}
void saveClientRole(IsvInfo isvInfo, List<String> roleCodeList) {
void saveIsvRole(IsvInfo isvInfo, List<String> roleCodeList) {
Query query = new Query();
long isvInfoId = isvInfo.getId();
query.eq("isv_info_id", isvInfoId);
query.eq("isv_id", isvInfoId);
permIsvRoleMapper.deleteByQuery(query);
List<PermIsvRole> tobeSaveList = roleCodeList.stream()
.map(roleCode -> {
PermIsvRole rec = new PermIsvRole();
rec.setIsvInfoId(isvInfoId);
rec.setIsvId(isvInfoId);
rec.setRoleCode(roleCode);
return rec;
})
.collect(Collectors.toList());
permIsvRoleMapper.saveBatch(tobeSaveList);
if (CollectionUtils.isNotEmpty(tobeSaveList)) {
permIsvRoleMapper.saveBatch(tobeSaveList);
}
try {
routePermissionService.sendIsvRolePermissionToZookeeper(isvInfo.getAppKey(), roleCodeList);
} catch (Exception e) {
log.error("保存到zookeeper中失败,isvInfo:{}, roleCodeList:{}", isvInfo, roleCodeList);
throw new ApiException("保存失败,请查看日志");
}
}
}

@ -0,0 +1,43 @@
package com.gitee.sop.adminserver.api.isv;
import com.gitee.easyopen.annotation.Api;
import com.gitee.easyopen.annotation.ApiService;
import com.gitee.easyopen.doc.annotation.ApiDoc;
import com.gitee.easyopen.util.CopyUtil;
import com.gitee.fastmybatis.core.PageInfo;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.fastmybatis.core.query.Sort;
import com.gitee.fastmybatis.core.support.PageEasyui;
import com.gitee.fastmybatis.core.util.MapperUtil;
import com.gitee.sop.adminserver.api.isv.result.RoleVO;
import com.gitee.sop.adminserver.mapper.PermRoleMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author tanghc
*/
@ApiService
@ApiDoc("ISV管理")
@Slf4j
public class RoleApi {
@Autowired
PermRoleMapper permRoleMapper;
@Api(name = "role.listall")
List<RoleVO> roleListall() {
Query query = new Query();
query.orderby("id", Sort.ASC);
return permRoleMapper.list(query).stream()
.map(permRole -> {
RoleVO vo = new RoleVO();
CopyUtil.copyProperties(permRole, vo);
return vo;
})
.collect(Collectors.toList());
}
}

@ -5,7 +5,7 @@ import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.util.Collections;
import java.util.List;
/**
@ -13,11 +13,13 @@ import java.util.List;
*/
@Data
public class IsvInfoForm {
private int signType = 1;
/** secret, 数据库字段:secret */
@ApiDocField(description = "secret", required = true)
@NotBlank(message = "secret不能为空")
@ApiDocField(description = "secret")
@Length(max = 100,message = "secret长度不能超过100")
private String secret;
private String secret = "";
/** 公钥, 数据库字段:pub_key */
@ApiDocField(description = "pubKey", required = true)
@ -33,6 +35,6 @@ public class IsvInfoForm {
@ApiDocField(description = "状态:0:启用,1:禁用")
private Byte status = 0;
@NotEmpty(message = "角色不能为空")
private List<String> roleCode;
@ApiDocField(description = "roleCode数组", elementClass = String.class)
private List<String> roleCode = Collections.emptyList();
}

@ -1,5 +1,9 @@
package com.gitee.sop.adminserver.api.isv.param;
import com.gitee.easyopen.doc.DataType;
import com.gitee.easyopen.doc.annotation.ApiDocBean;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import com.gitee.fastmybatis.core.query.annotation.Condition;
import com.gitee.fastmybatis.core.query.param.PageParam;
import lombok.Getter;
import lombok.Setter;
@ -9,6 +13,12 @@ import lombok.Setter;
*/
@Getter
@Setter
@ApiDocBean(fields = {
@ApiDocField(name = "pageIndex", description = "第几页", dataType = DataType.INT, example = "1"),
@ApiDocField(name = "pageSize", description = "每页几条数据", dataType = DataType.INT, example = "10"),
})
public class IsvPageParam extends PageParam {
@ApiDocField(name = "appKey", description = "appKey", dataType = DataType.STRING, example = "111111")
@Condition(ignoreEmptyString = true)
private String appKey;
}

@ -1,9 +0,0 @@
package com.gitee.sop.adminserver.api.isv.result;
import lombok.Data;
@Data
public class AppKeySecretVo {
private String appKey;
private String secret;
}

@ -0,0 +1,23 @@
package com.gitee.sop.adminserver.api.isv.result;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
/**
* @author tanghc
*/
@Data
public class IsvFormVO {
@ApiDocField(description = "appKey")
private String appKey;
@ApiDocField(description = "secret")
private String secret;
@ApiDocField(description = "pubKey")
private String pubKey;
@ApiDocField(description = "priKey")
private String priKey;
}

@ -1,5 +1,6 @@
package com.gitee.sop.adminserver.api.isv.result;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
import java.util.Date;
@ -11,28 +12,37 @@ import java.util.List;
@Data
public class IsvVO {
/** 数据库字段:id */
@ApiDocField(description = "id")
private Long id;
/** appKey, 数据库字段:app_key */
@ApiDocField(description = "appKey")
private String appKey;
/** secret, 数据库字段:secret */
@ApiDocField(description = "appKey")
private String secret;
/** 公钥, 数据库字段:pub_key */
@ApiDocField(description = "pubKey")
private String pubKey;
/** 私钥, 数据库字段:pri_key */
@ApiDocField(description = "priKey")
private String priKey;
/** 0启用,1禁用, 数据库字段:status */
@ApiDocField(description = "状态:0启用,1禁用")
private Byte status;
/** 数据库字段:gmt_create */
@ApiDocField(description = "添加时间")
private Date gmtCreate;
/** 数据库字段:gmt_modified */
@ApiDocField(description = "修改时间")
private Date gmtModified;
@ApiDocField(description = "角色列表")
private List<RoleVO> roleList;
}

@ -1,10 +0,0 @@
package com.gitee.sop.adminserver.api.isv.result;
import lombok.Data;
@Data
public class PubPriVo {
private String pubKey;
private String priKey;
}

@ -1,5 +1,6 @@
package com.gitee.sop.adminserver.api.isv.result;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
import java.util.Date;
@ -10,7 +11,9 @@ import java.util.Date;
@Data
public class RoleVO {
private Long id;
@ApiDocField(description = "角色码")
private String roleCode;
@ApiDocField(description = "描述")
private String description;
private Date gmtCreate;
}

@ -6,16 +6,29 @@ import com.gitee.easyopen.annotation.ApiService;
import com.gitee.easyopen.doc.annotation.ApiDoc;
import com.gitee.easyopen.doc.annotation.ApiDocMethod;
import com.gitee.easyopen.exception.ApiException;
import com.gitee.sop.adminserver.api.service.param.RouteSearchParam;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.adminserver.api.isv.result.RoleVO;
import com.gitee.sop.adminserver.api.service.param.RouteParam;
import com.gitee.sop.adminserver.api.service.param.RoutePermissionParam;
import com.gitee.sop.adminserver.api.service.param.RouteSearchParam;
import com.gitee.sop.adminserver.api.service.result.RouteVO;
import com.gitee.sop.adminserver.api.service.result.ServiceInfo;
import com.gitee.sop.adminserver.bean.GatewayRouteDefinition;
import com.gitee.sop.adminserver.bean.ZookeeperContext;
import com.gitee.sop.adminserver.entity.PermRole;
import com.gitee.sop.adminserver.entity.PermRolePermission;
import com.gitee.sop.adminserver.mapper.PermRoleMapper;
import com.gitee.sop.adminserver.mapper.PermRolePermissionMapper;
import com.gitee.sop.adminserver.service.RoutePermissionService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -26,20 +39,30 @@ import java.util.stream.Collectors;
*/
@ApiService
@ApiDoc("服务管理")
@Slf4j
public class RouteApi {
@Autowired
PermRolePermissionMapper permRolePermissionMapper;
@Autowired
PermRoleMapper permRoleMapper;
@Autowired
RoutePermissionService routePermissionService;
@Api(name = "route.list")
@ApiDocMethod(description = "路由列表", elementClass = GatewayRouteDefinition.class)
List<GatewayRouteDefinition> listRoute(RouteSearchParam param) throws Exception {
@ApiDocMethod(description = "路由列表")
List<RouteVO> listRoute(RouteSearchParam param) throws Exception {
if (StringUtils.isBlank(param.getServiceId())) {
return Collections.emptyList();
}
String searchPath = ZookeeperContext.getSopRouteRootPath(param.getProfile()) + "/" + param.getServiceId();
String searchPath = ZookeeperContext.getSopRouteRootPath() + "/" + param.getServiceId();
List<ChildData> childDataList = ZookeeperContext.getChildrenData(searchPath);
List<GatewayRouteDefinition> routeDefinitionList = childDataList.stream()
List<RouteVO> routeDefinitionList = childDataList.stream()
.map(childData -> {
String serviceNodeData = new String(childData.getData());
GatewayRouteDefinition routeDefinition = JSON.parseObject(serviceNodeData, GatewayRouteDefinition.class);
@ -50,10 +73,16 @@ public class RouteApi {
String id = param.getId();
if (StringUtils.isBlank(id)) {
return isRoute;
}else {
} else {
return isRoute && gatewayRouteDefinition.getId().contains(id);
}
})
.map(gatewayRouteDefinition->{
RouteVO vo = new RouteVO();
BeanUtils.copyProperties(gatewayRouteDefinition, vo);
vo.setRoles(this.getRouteRole(gatewayRouteDefinition.getId()));
return vo;
})
.collect(Collectors.toList());
return routeDefinitionList;
@ -62,7 +91,7 @@ public class RouteApi {
@Api(name = "route.update")
@ApiDocMethod(description = "修改路由")
void updateRoute(RouteParam param) throws Exception {
String serviceIdPath = ZookeeperContext.getSopRouteRootPath(param.getProfile()) + "/" + param.getServiceId();
String serviceIdPath = ZookeeperContext.getSopRouteRootPath() + "/" + param.getServiceId();
String zookeeperRoutePath = serviceIdPath + "/" + param.getId();
String data = ZookeeperContext.getData(zookeeperRoutePath);
GatewayRouteDefinition routeDefinition = JSON.parseObject(data, GatewayRouteDefinition.class);
@ -73,7 +102,7 @@ public class RouteApi {
@Api(name = "route.add")
@ApiDocMethod(description = "新增路由")
void addRoute(RouteParam param) throws Exception {
String serviceIdPath = ZookeeperContext.getSopRouteRootPath(param.getProfile()) + "/" + param.getServiceId();
String serviceIdPath = ZookeeperContext.getSopRouteRootPath() + "/" + param.getServiceId();
String zookeeperRoutePath = serviceIdPath + "/" + param.getId();
if (ZookeeperContext.isPathExist(zookeeperRoutePath)) {
throw new ApiException("id已存在");
@ -90,4 +119,55 @@ public class RouteApi {
ZookeeperContext.updatePathData(serviceIdPath, JSON.toJSONString(serviceInfo));
}
@Api(name = "route.role.get")
@ApiDocMethod(description = "获取路由对应的角色", elementClass = RoleVO.class)
List<RoleVO> getRouteRole(RouteSearchParam param) {
if (StringUtils.isBlank(param.getId())) {
throw new ApiException("id不能为空");
}
return this.getRouteRole(param.getId());
}
private List<RoleVO> getRouteRole(String id) {
return permRolePermissionMapper.listByColumn("route_id", id)
.stream()
.map(permRolePermission -> {
RoleVO vo = new RoleVO();
String roleCode = permRolePermission.getRoleCode();
PermRole permRole = permRoleMapper.getByColumn("role_code", roleCode);
BeanUtils.copyProperties(permRole, vo);
return vo;
})
.collect(Collectors.toList());
}
@Api(name = "route.role.update")
@ApiDocMethod(description = "更新路由对应的角色")
void updateRouteRole(RoutePermissionParam param) {
String routeId = param.getRouteId();
// 删除所有数据
Query delQuery = new Query();
delQuery.eq("route_id", routeId);
permRolePermissionMapper.deleteByQuery(delQuery);
List<String> roleCodes = param.getRoleCode();
if (CollectionUtils.isNotEmpty(roleCodes)) {
List<PermRolePermission> tobeSave = new ArrayList<>(roleCodes.size());
for (String roleCode : roleCodes) {
PermRolePermission permRolePermission = new PermRolePermission();
permRolePermission.setRoleCode(roleCode);
permRolePermission.setRouteId(routeId);
tobeSave.add(permRolePermission);
}
// 批量添加
permRolePermissionMapper.saveBatch(tobeSave);
}
try {
routePermissionService.sendRoutePermissionReloadToZookeeper();
} catch (Exception e) {
log.info("消息推送--路由权限(reload)失败",e);
}
}
}

@ -53,7 +53,7 @@ public class ServiceApi {
@Api(name = "service.list")
@ApiDocMethod(description = "服务列表(旧)", elementClass = ServiceInfo.class)
List<ServiceInfo> listServiceInfo(ServiceSearchParam param) throws Exception {
String routeRootPath = ZookeeperContext.getSopRouteRootPath(param.getProfile());
String routeRootPath = ZookeeperContext.getSopRouteRootPath();
List<ChildData> childDataList = ZookeeperContext.getChildrenData(routeRootPath);
List<ServiceInfo> serviceInfoList = childDataList.stream()
.filter(childData -> childData.getData() != null && childData.getData().length > 0)

@ -1,7 +1,8 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@ -9,13 +10,10 @@ import javax.validation.constraints.NotNull;
/**
* @author tanghc
*/
@Data
@Getter
@Setter
public class RouteParam {
@NotBlank(message = "profile不能为空")
@ApiDocField(description = "profile")
private String profile;
@NotBlank(message = "serviceId不能为空")
@ApiDocField(description = "serviceId")
private String serviceId;

@ -0,0 +1,22 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* @author tanghc
*/
@Getter
@Setter
public class RoutePermissionParam {
@ApiDocField(description = "routeId", required = true)
@NotBlank(message = "routeId不能为空")
private String routeId;
@ApiDocField(description = "角色")
private List<String> roleCode;
}

@ -1,17 +1,16 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
/**
* @author tanghc
*/
@Data
@Getter
@Setter
public class ServiceSearchParam {
@ApiDocField(description = "profile")
private String profile;
@ApiDocField(description = "serviceId")
private String serviceId;
}

@ -0,0 +1,17 @@
package com.gitee.sop.adminserver.api.service.result;
import com.gitee.sop.adminserver.api.isv.result.RoleVO;
import com.gitee.sop.adminserver.bean.GatewayRouteDefinition;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @author tanghc
*/
@Getter
@Setter
public class RouteVO extends GatewayRouteDefinition {
private List<RoleVO> roles;
}

@ -17,18 +17,5 @@ import java.util.stream.Stream;
@ApiDoc("系统接口")
public class SystemApi {
@Value("${sop-admin.profiles}")
private String profiles;
private List<String> profileList;
@ApiDocMethod(description = "获取profile列表")
@Api(name = "system.profile.list")
public List<String> listProfiles() {
if (profileList == null) {
String[] arr = profiles.split("\\,");
profileList = Stream.of(arr).collect(Collectors.toList());
}
return profileList;
}
}

@ -0,0 +1,18 @@
package com.gitee.sop.adminserver.bean;
import lombok.Data;
/**
* @author tanghc
*/
@Data
public class ChannelMsg {
public ChannelMsg(String operation, Object data) {
this.operation = operation;
this.data = data;
}
private String operation;
private Object data;
}

@ -54,4 +54,9 @@ public class GatewayRouteDefinition {
* 合并结果
*/
private int mergeResult = 1;
/**
* 是否需要授权才能访问
*/
private int permission;
}

@ -0,0 +1,16 @@
package com.gitee.sop.adminserver.bean;
import lombok.Data;
import java.util.List;
/**
* isv授权过的路由
* @author tanghc
*/
@Data
public class IsvRoutePermission {
private String appKey;
private List<String> routeIdList;
private String routeIdListMd5;
}

@ -9,4 +9,14 @@ public class SopAdminConstants {
*/
public static final String SOP_SERVICE_ROUTE_PATH = "/com.gitee.sop.route";
/**
* zookeeper存放路由授权信息根目录
*/
public static final String SOP_ROUTE_PERMISSION_PATH = "/com.gitee.sop.isv.route.permission";
/**
* 消息监听路径
*/
public static final String SOP_MSG_CHANNEL_PATH = "/com.gitee.sop.channel";
}

@ -16,14 +16,15 @@ import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
import static com.gitee.sop.adminserver.bean.SopAdminConstants.SOP_MSG_CHANNEL_PATH;
import static com.gitee.sop.adminserver.bean.SopAdminConstants.SOP_ROUTE_PERMISSION_PATH;
/**
* @author tanghc
*/
@Configuration
public class ZookeeperContext {
public static final String SOP_ROUTE_ROOT_PATH = SopAdminConstants.SOP_SERVICE_ROUTE_PATH + "-%s";
private static CuratorFramework client;
@Value("${spring.cloud.zookeeper.connect-string}")
@ -37,6 +38,9 @@ public class ZookeeperContext {
@PostConstruct
protected void after() {
if (StringUtils.isBlank(zookeeperServerAddr)) {
throw new IllegalArgumentException("未指定spring.cloud.zookeeper.connect-string参数");
}
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(zookeeperServerAddr)
.retryPolicy(new ExponentialBackoffRetry(NumberUtils.toInt(baseSleepTimeMs, 3000), NumberUtils.toInt(maxRetries, 3)))
@ -47,11 +51,20 @@ public class ZookeeperContext {
setClient(client);
}
public static String getSopRouteRootPath(String profile) {
if (StringUtils.isBlank(profile)) {
profile = "default";
}
return String.format(SOP_ROUTE_ROOT_PATH, profile);
public static String getSopRouteRootPath() {
return SopAdminConstants.SOP_SERVICE_ROUTE_PATH;
}
public static String getRoutePermissionPath() {
return SOP_ROUTE_PERMISSION_PATH;
}
public static String getIsvInfoChannelPath() {
return SOP_MSG_CHANNEL_PATH + "/isvinfo";
}
public static String getIsvRoutePermissionChannelPath() {
return SOP_MSG_CHANNEL_PATH + "/isv-route-permission";
}
public static CuratorFramework getClient() {
@ -71,7 +84,7 @@ public class ZookeeperContext {
}
/**
* 对已存在的path赋值
* 对已存在的path赋值如果path不存在抛异常
*
* @param path 已存在的
* @param data
@ -86,7 +99,7 @@ public class ZookeeperContext {
}
/**
* 创建新的path并赋值
* 创建新的path并赋值如果path已存在抛异常
* @param path 待创建的path
* @param data
*/
@ -100,6 +113,22 @@ public class ZookeeperContext {
.forPath(path, data.getBytes());
}
/**
* 新建或保存节点
* @param path
* @param data
* @return
* @throws Exception
*/
public static String createOrUpdateData(String path, String data) throws Exception {
return getClient().create()
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
.orSetData()
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
.creatingParentContainersIfNeeded()
.forPath(path, data.getBytes());
}
public static String getData(String path) throws Exception {
if (!isPathExist(path)) {
return null;

@ -1,5 +1,10 @@
package com.gitee.sop.adminserver.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.gitee.easyopen.ApiConfig;
import com.gitee.easyopen.ResultSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@ -14,6 +19,14 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupp
public class WebConfig extends WebMvcConfigurationSupport {
@Bean
ApiConfig apiConfig() {
ApiConfig apiConfig = new ApiConfig();
apiConfig.setJsonResultSerializer(obj -> {
return JSON.toJSONString(obj, SerializerFeature.WriteDateUseDateFormat);
});
return apiConfig;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {

@ -26,8 +26,8 @@ public class PermIsvRole {
/** 数据库字段:id */
private Long id;
/** isv_info.id, 数据库字段:isv_info_id */
private Long isvInfoId;
/** isv_info表id, 数据库字段:isv_id */
private Long isvId;
/** 角色code, 数据库字段:role_code */
private String roleCode;

@ -30,7 +30,7 @@ public class PermRolePermission {
private String roleCode;
/** api_id, 数据库字段:route_id */
private Long routeId;
private String routeId;
/** 数据库字段:gmt_create */
private Date gmtCreate;

@ -1,34 +0,0 @@
package com.gitee.sop.adminserver.service;
import com.gitee.sop.adminserver.entity.PermIsvRole;
import com.gitee.sop.adminserver.mapper.PermIsvRoleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author thc
*/
@Service
public class PermService {
@Autowired
PermIsvRoleMapper permClientRoleMapper;
/**
* 获取客户端角色码列表
*
* @param clientId
* @return
*/
public List<String> listClientRoleCode(Long clientId) {
List<PermIsvRole> list = permClientRoleMapper.listByColumn("client_id", clientId);
List<String> retList = new ArrayList<>(list.size());
for (PermIsvRole permClientRole : list) {
retList.add(permClientRole.getRoleCode());
}
return retList;
}
}

@ -0,0 +1,104 @@
package com.gitee.sop.adminserver.service;
import com.alibaba.fastjson.JSON;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.adminserver.bean.ChannelMsg;
import com.gitee.sop.adminserver.bean.IsvRoutePermission;
import com.gitee.sop.adminserver.bean.ZookeeperContext;
import com.gitee.sop.adminserver.entity.PermIsvRole;
import com.gitee.sop.adminserver.entity.PermRolePermission;
import com.gitee.sop.adminserver.mapper.IsvInfoMapper;
import com.gitee.sop.adminserver.mapper.PermIsvRoleMapper;
import com.gitee.sop.adminserver.mapper.PermRolePermissionMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 路由权限业务类
*
* @author thc
*/
@Service
@Slf4j
public class RoutePermissionService {
@Autowired
IsvInfoMapper isvInfoMapper;
@Autowired
PermIsvRoleMapper permIsvRoleMapper;
@Autowired
PermRolePermissionMapper permRolePermissionMapper;
/**
* 获取客户端角色码列表
*
* @param isvId
* @return
*/
public List<String> listClientRoleCode(long isvId) {
List<PermIsvRole> list = permIsvRoleMapper.listByColumn("isv_id", isvId);
return list.stream()
.map(PermIsvRole::getRoleCode)
.collect(Collectors.toList());
}
/**
* 推送isv路由权限到zookeeper
*
* @param appKey
* @param roleCodeList
*/
public void sendIsvRolePermissionToZookeeper(String appKey, List<String> roleCodeList) throws Exception {
Collections.sort(roleCodeList);
List<String> routeIdList = this.getRouteIdList(roleCodeList);
String roleCodeListMd5 = DigestUtils.md5Hex(JSON.toJSONString(routeIdList));
IsvRoutePermission isvRoutePermission = new IsvRoutePermission();
isvRoutePermission.setAppKey(appKey);
isvRoutePermission.setRouteIdList(routeIdList);
isvRoutePermission.setRouteIdListMd5(roleCodeListMd5);
ChannelMsg channelMsg = new ChannelMsg("update", isvRoutePermission);
String jsonData = JSON.toJSONString(channelMsg);
String path = ZookeeperContext.getIsvRoutePermissionChannelPath();
log.info("消息推送--路由权限(update), path:{}, data:{}", path, jsonData);
ZookeeperContext.createOrUpdateData(path, jsonData);
}
/**
* 获取角色对应的路由
*
* @param roleCodeList
* @return
*/
public List<String> getRouteIdList(List<String> roleCodeList) {
if (CollectionUtils.isEmpty(roleCodeList)) {
return Collections.emptyList();
}
Query query = new Query();
query.in("role_code", roleCodeList);
List<PermRolePermission> rolePermissionList = permRolePermissionMapper.list(query);
return rolePermissionList.stream()
.map(PermRolePermission::getRouteId)
.collect(Collectors.toList());
}
/**
* 推送所有路由权限到zookeeper
*/
public void sendRoutePermissionReloadToZookeeper() throws Exception {
ChannelMsg channelMsg = new ChannelMsg("reload", null);
String jsonData = JSON.toJSONString(channelMsg);
String path = ZookeeperContext.getIsvRoutePermissionChannelPath();
log.info("消息推送--路由权限(reload), path:{}, data:{}", path, jsonData);
ZookeeperContext.createOrUpdateData(path, jsonData);
}
}

@ -1,7 +1,7 @@
server:
port: 8082
# 不用改
spring:
application:
name: sop-admin
@ -27,11 +27,7 @@ easyopen:
mono: false
ignore-validate: true
# 默认profile列表,新增可以在后面加
sop-admin:
profiles: default,prod,dev,test
# 注册中心地址,这里只是参数,并不会去注册
# 注册中心地址,根据实际情况改,这里只是参数,并不会去注册
eureka:
port: 1111
host: localhost
@ -39,8 +35,13 @@ eureka:
serviceUrl:
defaultZone: http://${eureka.host}:${eureka.port}/eureka/
# 根据实际情况改
logging:
level:
com:
gitee: debug
# 不用改
mybatis:
fill: {com.gitee.fastmybatis.core.support.DateFillInsert: gmt_create,
com.gitee.fastmybatis.core.support.DateFillUpdate: gmt_modified}

@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>

@ -5,7 +5,7 @@
<parent>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.gitee.sop</groupId>

@ -2,11 +2,13 @@ package com.gitee.sop.gatewaycommon.bean;
import com.gitee.sop.gatewaycommon.gateway.param.GatewayParamBuilder;
import com.gitee.sop.gatewaycommon.gateway.result.GatewayResult;
import com.gitee.sop.gatewaycommon.param.ParamBuilder;
import com.gitee.sop.gatewaycommon.gateway.result.GatewayResultExecutor;
import com.gitee.sop.gatewaycommon.manager.DefaultIsvRoutePermissionManager;
import com.gitee.sop.gatewaycommon.manager.IsvRoutePermissionManager;
import com.gitee.sop.gatewaycommon.param.ParamBuilder;
import com.gitee.sop.gatewaycommon.result.ResultExecutor;
import com.gitee.sop.gatewaycommon.secret.AppSecretManager;
import com.gitee.sop.gatewaycommon.secret.CacheAppSecretManager;
import com.gitee.sop.gatewaycommon.secret.CacheIsvManager;
import com.gitee.sop.gatewaycommon.secret.IsvManager;
import com.gitee.sop.gatewaycommon.session.ApiSessionManager;
import com.gitee.sop.gatewaycommon.session.SessionManager;
import com.gitee.sop.gatewaycommon.validate.ApiEncrypter;
@ -48,9 +50,9 @@ public class ApiConfig {
private ResultExecutor<RequestContext, String> zuulResultExecutor = new ZuulResultExecutor();
/**
* app秘钥管理
* isv管理
*/
private AppSecretManager appSecretManager = new CacheAppSecretManager();
private IsvManager isvManager = new CacheIsvManager();
/**
* 加密工具
@ -87,12 +89,18 @@ public class ApiConfig {
*/
private ZuulErrorController zuulErrorController = new ZuulErrorController();
/**
* isv路由权限
*/
private IsvRoutePermissionManager isvRoutePermissionManager = new DefaultIsvRoutePermissionManager();
// -------- fields ---------
/**
* 错误模块
*/
private List<String> i18nModules = new ArrayList<>();
// -------- fields ---------
/**
* 忽略验证设置true则所有接口不会进行签名校验
@ -111,7 +119,9 @@ public class ApiConfig {
private int timeoutSeconds = 60 * 5;
public void addAppSecret(Map<String, String> appSecretPair) {
this.appSecretManager.addAppSecret(appSecretPair);
for (Map.Entry<String, String> entry : appSecretPair.entrySet()) {
this.isvManager.update(new IsvDefinition(entry.getKey(), entry.getValue()));
}
}
public static ApiConfig getInstance() {

@ -44,6 +44,11 @@ public class BaseRouteDefinition {
*/
private int mergeResult;
/**
* 接口是否需要授权才能访问
*/
private int permission;
public boolean enable() {
return status == RouteStatus.ENABLE.getStatus();
}

@ -0,0 +1,12 @@
package com.gitee.sop.gatewaycommon.bean;
import lombok.Data;
/**
* @author tanghc
*/
@Data
public class ChannelMsg {
private String operation = "_unknown_";
private String data = "{}";
}

@ -0,0 +1,25 @@
package com.gitee.sop.gatewaycommon.bean;
/**
* @author tanghc
*/
public interface Isv {
/**
* appKey
* @return
*/
String getAppKey();
/**
* 秘钥
* @return
*/
String getSecretInfo();
/**
* 0启用1禁用
* @return
*/
Byte getStatus();
}

@ -0,0 +1,55 @@
package com.gitee.sop.gatewaycommon.bean;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import java.util.Date;
/**
* @author tanghc
*/
@Getter
@Setter
public class IsvDefinition implements Isv {
public IsvDefinition() {
}
public IsvDefinition(String appKey, String secret) {
this.appKey = appKey;
this.secret = secret;
}
private Long id;
private String appKey;
/** 秘钥,如果是支付宝开放平台,对应的pubKey */
private String secret;
private String pubKey;
/** 0启用,1禁用 */
private Byte status;
private Date gmtCreate;
private Date gmtModified;
@Override
public String getSecretInfo() {
return StringUtils.isBlank(pubKey) ? secret : pubKey;
}
@Override
public String toString() {
return "IsvDefinition{" +
"id=" + id +
", appKey='" + appKey + '\'' +
", status=" + status +
", gmtCreate=" + gmtCreate +
", gmtModified=" + gmtModified +
'}';
}
}

@ -0,0 +1,18 @@
package com.gitee.sop.gatewaycommon.bean;
import lombok.Data;
import java.util.Collections;
import java.util.List;
/**
* isv授权过的路由
* @author tanghc
*/
@Data
public class IsvRoutePermission {
private String appKey;
private List<String> routeIdList = Collections.emptyList();
private String routeIdListMd5;
}

@ -38,4 +38,16 @@ public class SopConstants {
*/
public static final String SOP_SERVICE_ROUTE_PATH = "/com.gitee.sop.route";
/**
* zookeeper存放路由授权信息根目录
*/
public static final String SOP_ROUTE_PERMISSION_PATH = "/com.gitee.sop.isv.route.permission";
/**
* 消息监听路径
*/
public static final String SOP_MSG_CHANNEL_PATH = "/com.gitee.sop.channel";
}

@ -22,6 +22,9 @@ public class AbstractConfiguration {
if (RouteRepositoryContext.getRouteRepository() == null) {
throw new IllegalArgumentException("RouteRepositoryContext.setRouteRepository()方法未使用");
}
EnvironmentContext.setEnvironment(environment);
ZookeeperContext.setEnvironment(environment);
initMessage();
apiMetaManager.refresh();
doAfter();

@ -6,19 +6,14 @@ import com.gitee.sop.gatewaycommon.bean.BaseServiceRouteInfo;
import com.gitee.sop.gatewaycommon.bean.TargetRoute;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import java.util.List;
import static com.gitee.sop.gatewaycommon.bean.SopConstants.SOP_SERVICE_ROUTE_PATH;
/**
* 路由管理采用zookeeper实现监听路由的增删改并适时更新到本地路由的存储格式为
@ -70,38 +65,26 @@ public abstract class BaseRouteManager<R extends BaseServiceRouteInfo<E>, E exte
public BaseRouteManager(Environment environment, RouteRepository<T> routeRepository) {
this.environment = environment;
this.routeRepository = routeRepository;
String profile = environment.getProperty("spring.profiles.active", "default");
this.routeRootPath = SOP_SERVICE_ROUTE_PATH + "-" + profile;
this.routeRootPath = ZookeeperContext.getRouteRootPath();
}
@Override
public void refresh() {
try {
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string");
String profile = environment.getProperty("spring.profiles.active", "default");
if (StringUtils.isEmpty(zookeeperServerAddr)) {
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数");
}
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(zookeeperServerAddr)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
client.create()
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
.orSetData()
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
.creatingParentContainersIfNeeded()
.forPath(routeRootPath, "".getBytes());
this.refreshRouteInfo();
}
protected void refreshRouteInfo() {
try {
ZookeeperContext.createOrUpdateData(routeRootPath, "");
CuratorFramework client = ZookeeperContext.getClient();
this.watchServiceChange(client, routeRootPath);
} catch (Exception e) {
e.printStackTrace();
log.error("刷新路由配置失败", e);
throw new IllegalStateException("刷新路由配置失败");
}
}
/**
* 监听微服务更改
*

@ -0,0 +1,47 @@
package com.gitee.sop.gatewaycommon.manager;
import com.gitee.sop.gatewaycommon.bean.IsvRoutePermission;
import org.apache.commons.lang3.StringUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author tanghc
*/
public class DefaultIsvRoutePermissionManager implements IsvRoutePermissionManager {
/** key: appKey */
protected static Map<String, IsvRoutePermission> isvRoutePermissionMap = new ConcurrentHashMap<>(64);
@Override
public void load() {
}
@Override
public void update(IsvRoutePermission isvRoutePermission) {
IsvRoutePermission routePermission = isvRoutePermissionMap.get(isvRoutePermission.getAppKey());
if (routePermission == null) {
isvRoutePermissionMap.put(isvRoutePermission.getAppKey(), isvRoutePermission);
} else {
if (!StringUtils.equals(isvRoutePermission.getRouteIdListMd5(), routePermission.getRouteIdListMd5())) {
isvRoutePermissionMap.put(isvRoutePermission.getAppKey(), isvRoutePermission);
}
}
}
@Override
public boolean hasPermission(String appKey, String routeId) {
IsvRoutePermission routePermission = isvRoutePermissionMap.get(appKey);
if (routePermission == null) {
return false;
}
return routePermission.getRouteIdList().contains(routeId);
}
@Override
public void remove(String appKey) {
isvRoutePermissionMap.remove(appKey);
}
}

@ -0,0 +1,26 @@
package com.gitee.sop.gatewaycommon.manager;
import org.springframework.core.env.Environment;
/**
* @author tanghc
*/
public class EnvironmentContext {
private static Environment environment;
public static Environment getEnvironment() {
return environment;
}
public static void setEnvironment(Environment environment) {
EnvironmentContext.environment = environment;
}
public static String getProfile(Environment env) {
return env.getProperty("spring.profiles.active", "default");
}
public static String getProfile() {
return getProfile(environment);
}
}

@ -0,0 +1,34 @@
package com.gitee.sop.gatewaycommon.manager;
import com.gitee.sop.gatewaycommon.bean.IsvRoutePermission;
/**
* @author tanghc
*/
public interface IsvRoutePermissionManager {
/**
* 加载路由权限信息
*/
void load();
/**
* 加载权限
* @param isvRoutePermission
*/
void update(IsvRoutePermission isvRoutePermission);
/**
* 判断是否有权限
* @param appKey
* @param routeId
* @return
*/
boolean hasPermission(String appKey, String routeId);
/**
* 删除权限
* @param appKey
*/
void remove(String appKey);
}

@ -0,0 +1,12 @@
package com.gitee.sop.gatewaycommon.manager;
import com.gitee.sop.gatewaycommon.bean.IsvRoutePermission;
/**
* @author tanghc
*/
public interface RoutePermissionManager {
void load();
void update(IsvRoutePermission isvRoutePermission);
}

@ -0,0 +1,194 @@
package com.gitee.sop.gatewaycommon.manager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.framework.recipes.cache.NodeCacheListener;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.data.Stat;
import org.springframework.core.env.Environment;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import static com.gitee.sop.gatewaycommon.bean.SopConstants.SOP_MSG_CHANNEL_PATH;
import static com.gitee.sop.gatewaycommon.bean.SopConstants.SOP_SERVICE_ROUTE_PATH;
import static com.gitee.sop.gatewaycommon.bean.SopConstants.SOP_ROUTE_PERMISSION_PATH;
/**
* @author tanghc
*/
@Slf4j
public class ZookeeperContext {
private static CuratorFramework client;
public static void setEnvironment(Environment environment) {
Assert.notNull(environment, "environment不能为null");
initZookeeperClient(environment);
}
public synchronized static void initZookeeperClient(Environment environment) {
if (client != null) {
return;
}
String zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string");
if (StringUtils.isBlank(zookeeperServerAddr)) {
throw new RuntimeException("未指定spring.cloud.zookeeper.connect-string参数");
}
String baseSleepTimeMs = environment.getProperty("spring.cloud.zookeeper.baseSleepTimeMs");
String maxRetries = environment.getProperty("spring.cloud.zookeeper.maxRetries");
log.info("初始化zookeeper客户端,zookeeperServerAddr:{}, baseSleepTimeMs:{}, maxRetries:{}",
zookeeperServerAddr, baseSleepTimeMs, maxRetries);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString(zookeeperServerAddr)
.retryPolicy(new ExponentialBackoffRetry(NumberUtils.toInt(baseSleepTimeMs, 3000), NumberUtils.toInt(maxRetries, 3)))
.build();
client.start();
setClient(client);
}
public static String getRouteRootPath() {
return SOP_SERVICE_ROUTE_PATH;
}
public static String getRoutePermissionPath() {
return SOP_ROUTE_PERMISSION_PATH;
}
public static String getIsvInfoChannelPath() {
return SOP_MSG_CHANNEL_PATH + "/isvinfo";
}
public static String getIsvRoutePermissionChannelPath() {
return SOP_MSG_CHANNEL_PATH + "/isv-route-permission";
}
public static CuratorFramework getClient() {
return client;
}
public static void setClient(CuratorFramework client) {
ZookeeperContext.client = client;
}
public static boolean isPathExist(String path) {
try {
return client.checkExists().forPath(path) != null;
} catch (Exception e) {
return false;
}
}
/**
* 对已存在的path赋值如果path不存在抛异常
*
* @param path 已存在的
* @param data
* @return
* @throws Exception
*/
public static Stat updatePathData(String path, String data) throws Exception {
if (!isPathExist(path)) {
throw new IllegalStateException("path " + path + " 不存在");
}
return getClient().setData().forPath(path, data.getBytes());
}
/**
* 创建新的path并赋值如果path已存在抛异常
* @param path 待创建的path
* @param data
*/
public static String createNewData(String path, String data) throws Exception {
if (isPathExist(path)) {
throw new IllegalStateException("path " + path + " 已存在");
}
return getClient().create()
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
.creatingParentContainersIfNeeded()
.forPath(path, data.getBytes());
}
/**
* 新建或保存节点
* @param path
* @param data
* @return
* @throws Exception
*/
public static String createOrUpdateData(String path, String data) throws Exception {
return getClient().create()
// 如果节点存在则Curator将会使用给出的数据设置这个节点的值
.orSetData()
// 如果指定节点的父节点不存在,则Curator将会自动级联创建父节点
.creatingParentContainersIfNeeded()
.forPath(path, data.getBytes());
}
/**
* 监听一个节点
* @param path
* @param onChange 节点修改后触发
* @return
* @throws Exception
*/
public static String listenPath(String path, Consumer<NodeCache> onChange) throws Exception {
String ret = createOrUpdateData(path, "{}");
final NodeCache cache = new NodeCache(client, path, false);
cache.getListenable().addListener(new NodeCacheListener() {
@Override
public void nodeChanged() throws Exception {
onChange.accept(cache);
}
});
cache.start();
return ret;
}
public static String getData(String path) throws Exception {
if (!isPathExist(path)) {
return null;
}
byte[] data = getClient().getData().forPath(path);
return new String(data);
}
/**
* 获取子节点数据
*
* @param parentPath 父节点
* @return
* @throws Exception
*/
public static List<ChildData> getChildrenData(String parentPath) throws Exception {
PathChildrenCache pathChildrenCache = buildPathChildrenCache(parentPath);
if (pathChildrenCache == null) {
return Collections.emptyList();
}
return pathChildrenCache.getCurrentData();
}
public static PathChildrenCache buildPathChildrenCache(String path) throws Exception {
if (!isPathExist(path)) {
return null;
}
// PathChildrenCache: 监听数据节点的增删改,可以设置触发的事件
// 且第三个参数要设置为true,不然ChildData对象中的getData返回null
PathChildrenCache childrenCache = new PathChildrenCache(client, path, true);
// 列出子节点数据列表,需要使用BUILD_INITIAL_CACHE同步初始化模式才能获得,异步是获取不到的
childrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
return childrenCache;
}
}

@ -93,6 +93,10 @@ public enum ErrorEnum {
ISV_INSUFFICIENT_ISV_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.insufficient-isv-permissions"),
/** 代理的商户没有当前接口权限 */
ISV_INSUFFICIENT_USER_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.insufficient-user-permissions"),
/** 没有当前接口权限 */
ISV_ROUTE_NO_PERMISSIONS(Codes.CODE_ISV_PERM, "isv.route-no-permissions"),
/** 禁止访问 */
ISV_ACCESS_FORBIDDEN(Codes.CODE_ISV_PERM, "isv.access-forbidden"),
;
private ErrorMeta errorMeta;

@ -0,0 +1,43 @@
package com.gitee.sop.gatewaycommon.secret;
import com.gitee.sop.gatewaycommon.bean.IsvDefinition;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* @author tanghc
*/
public class CacheIsvManager implements IsvManager<IsvDefinition> {
/**
* key: appKey
*/
private Map<String, IsvDefinition> isvCache = new ConcurrentHashMap<>(64);
@Override
public void load(Function<Object, String> secretGetter) {
}
@Override
public void update(IsvDefinition isvInfo) {
isvCache.put(isvInfo.getAppKey(), isvInfo);
}
@Override
public void remove(String appKey) {
isvCache.remove(appKey);
}
@Override
public IsvDefinition getIsv(String appKey) {
return isvCache.get(appKey);
}
public Map<String, IsvDefinition> getIsvCache() {
return isvCache;
}
}

@ -0,0 +1,24 @@
package com.gitee.sop.gatewaycommon.secret;
import com.gitee.sop.gatewaycommon.bean.Isv;
import java.util.function.Function;
/**
* Isv管理
* @author tanghc
*/
public interface IsvManager<T extends Isv> {
/**
* 加载isv信息
* @param secretGetter secret获取
*/
void load(Function<Object, String> secretGetter);
void update(T t);
void remove(String appKey);
T getIsv(String appKey);
}

@ -2,12 +2,12 @@ package com.gitee.sop.gatewaycommon.validate;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import com.gitee.sop.gatewaycommon.bean.ApiContext;
import com.gitee.sop.gatewaycommon.bean.Isv;
import com.gitee.sop.gatewaycommon.message.ErrorEnum;
import com.gitee.sop.gatewaycommon.param.ApiParam;
import com.gitee.sop.gatewaycommon.param.ParamNames;
import com.gitee.sop.gatewaycommon.param.UploadContext;
import com.gitee.sop.gatewaycommon.secret.AppSecretManager;
import com.gitee.sop.gatewaycommon.zuul.ZuulContext;
import com.gitee.sop.gatewaycommon.secret.IsvManager;
import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -31,6 +31,7 @@ public class ApiValidator implements Validator {
private static final Logger logger = LoggerFactory.getLogger(ApiValidator.class);
private static final int MILLISECOND_OF_ONE_SECOND = 1000;
public static final int STATUS_FORBIDDEN = 2;
private static List<String> FORMAT_LIST = Arrays.asList("json", "xml");
@ -50,7 +51,7 @@ public class ApiValidator implements Validator {
logger.debug("忽略签名验证, name:{}, version:{}", param.fetchName(), param.fetchVersion());
}
} else {
// 需要验证签名
// 需要验证签名,先校验appKey,后校验签名顺序不能变
checkAppKey(param);
checkSign(param);
}
@ -112,12 +113,17 @@ public class ApiValidator implements Validator {
if (StringUtils.isEmpty(param.fetchAppKey())) {
throw ErrorEnum.ISV_MISSING_APP_ID.getErrorMeta().getException();
}
AppSecretManager appSecretManager = ApiContext.getApiConfig().getAppSecretManager();
Assert.notNull(appSecretManager, "appSecretManager未初始化");
boolean isTrueAppKey = appSecretManager.isValidAppKey(param.fetchAppKey());
if (!isTrueAppKey) {
IsvManager isvManager = ApiContext.getApiConfig().getIsvManager();
Assert.notNull(isvManager, "isvManager未初始化");
Isv isv = isvManager.getIsv(param.fetchAppKey());
// 没有用户
if (isv == null) {
throw ErrorEnum.ISV_INVALID_APP_ID.getErrorMeta().getException();
}
// 禁止访问
if (isv.getStatus() == null || isv.getStatus() == STATUS_FORBIDDEN) {
throw ErrorEnum.ISV_ACCESS_FORBIDDEN.getErrorMeta().getException();
}
}
protected void checkSign(ApiParam param) {
@ -127,9 +133,10 @@ public class ApiValidator implements Validator {
throw ErrorEnum.ISV_MISSING_SIGNATURE.getErrorMeta().getException(param.fetchNameVersion(), ParamNames.SIGN_NAME);
}
ApiConfig apiConfig = ApiContext.getApiConfig();
AppSecretManager appSecretManager = apiConfig.getAppSecretManager();
IsvManager isvManager = apiConfig.getIsvManager();
// 根据appId获取秘钥
String secret = appSecretManager.getSecret(param.fetchAppKey());
Isv isvInfo = isvManager.getIsv(param.fetchAppKey());
String secret = isvInfo.getSecretInfo();
if (StringUtils.isEmpty(secret)) {
throw ErrorEnum.ISV_MISSING_SIGNATURE_CONFIG.getErrorMeta().getException();
}

@ -5,6 +5,7 @@ import com.gitee.sop.gatewaycommon.manager.AbstractConfiguration;
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext;
import com.gitee.sop.gatewaycommon.zuul.filter.ErrorFilter;
import com.gitee.sop.gatewaycommon.zuul.filter.PostResultFilter;
import com.gitee.sop.gatewaycommon.zuul.filter.PreRoutePermissionFilter;
import com.gitee.sop.gatewaycommon.zuul.filter.PreValidateFilter;
import com.gitee.sop.gatewaycommon.zuul.route.SopRouteLocator;
import com.gitee.sop.gatewaycommon.zuul.route.ZuulRouteRepository;
@ -76,6 +77,15 @@ public class BaseZuulConfiguration extends AbstractConfiguration {
return new PreValidateFilter();
}
/**
* 权限校验
* @return
*/
@Bean
PreRoutePermissionFilter preRoutePermissionFilter() {
return new PreRoutePermissionFilter();
}
/**
* 错误处理扩展
* @return

@ -15,6 +15,8 @@ public abstract class BaseZuulFilter extends ZuulFilter {
protected Logger log = LoggerFactory.getLogger(getClass());
public static final int PRE_VALIDATE_FILTER_ORDER = -1000;
private Integer filterOrder;
/**

@ -0,0 +1,49 @@
package com.gitee.sop.gatewaycommon.zuul.filter;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import com.gitee.sop.gatewaycommon.bean.BaseRouteDefinition;
import com.gitee.sop.gatewaycommon.bean.TargetRoute;
import com.gitee.sop.gatewaycommon.manager.IsvRoutePermissionManager;
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext;
import com.gitee.sop.gatewaycommon.message.ErrorEnum;
import com.gitee.sop.gatewaycommon.param.ApiParam;
import com.gitee.sop.gatewaycommon.zuul.ZuulContext;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang3.BooleanUtils;
/**
* 路由权限校验有些接口需要配置权限才能访问
* @author tanghc
*/
public class PreRoutePermissionFilter extends BaseZuulFilter {
@Override
protected FilterType getFilterType() {
return FilterType.PRE;
}
@Override
protected int getFilterOrder() {
// 放在签名验证后面
return PRE_VALIDATE_FILTER_ORDER + 1;
}
@Override
protected Object doRun(RequestContext requestContext) throws ZuulException {
ApiParam apiParam = ZuulContext.getApiParam();
String routeId = apiParam.fetchNameVersion();
TargetRoute targetRoute = RouteRepositoryContext.getRouteRepository().get(routeId);
BaseRouteDefinition routeDefinition = targetRoute.getRouteDefinition();
boolean needCheckPermission = BooleanUtils.toBoolean(routeDefinition.getPermission());
if (needCheckPermission) {
IsvRoutePermissionManager isvRoutePermissionManager = ApiConfig.getInstance().getIsvRoutePermissionManager();
String appKey = apiParam.fetchAppKey();
boolean hasPermission = isvRoutePermissionManager.hasPermission(appKey, routeId);
if (!hasPermission) {
throw ErrorEnum.ISV_ROUTE_NO_PERMISSIONS.getErrorMeta().getException();
}
}
return null;
}
}

@ -22,8 +22,7 @@ public class PreValidateFilter extends BaseZuulFilter {
@Override
protected int getFilterOrder() {
// 在org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter前面
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
return PRE_VALIDATE_FILTER_ORDER;
}
@Override

@ -52,4 +52,6 @@ open.error_40004_=Business processing failure
open.error_40006=Insufficient permissions
open.error_40006_isv.insufficient-isv-permissions=Insufficient ISV permissions
open.error_40006_isv.insufficient-user-permissions=Insufficient user permissions
open.error_40006_isv.insufficient-user-permissions=Insufficient user permissions
open.error_40006_isv.route-no-permissions=No api permissions
open.error_40006_isv.access-forbidden=Access forbidden

@ -51,6 +51,8 @@
#open.error_40006=权限不足
#open.error_40006_isv.insufficient-isv-permissions=请检查配置的账户是否有当前接口权限
#open.error_40006_isv.insufficient-user-permissions=代理的商户没有当前接口权限
#open.error_40006_isv.route-no-permissions=没有当前接口权限
#open.error_40006_isv.access-forbidden=无权访问
open.error_10000=Success
@ -105,3 +107,5 @@ open.error_40004_=\u4e1a\u52a1\u5904\u7406\u5931\u8d25
open.error_40006=\u6743\u9650\u4e0d\u8db3
open.error_40006_isv.insufficient-isv-permissions=\u8bf7\u68c0\u67e5\u914d\u7f6e\u7684\u8d26\u6237\u662f\u5426\u6709\u5f53\u524d\u63a5\u53e3\u6743\u9650
open.error_40006_isv.insufficient-user-permissions=\u4ee3\u7406\u7684\u5546\u6237\u6ca1\u6709\u5f53\u524d\u63a5\u53e3\u6743\u9650
open.error_40006_isv.route-no-permissions=\u6ca1\u6709\u5f53\u524d\u63a5\u53e3\u6743\u9650
open.error_40006_isv.access-forbidden=\u65e0\u6743\u8bbf\u95ee

@ -6,7 +6,7 @@
<parent>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>com.gitee.sop</groupId>

@ -30,4 +30,9 @@ public @interface ApiAbility {
* 告诉网关是否对结果进行合并默认合并设置为false客户端将直接收到微服务端的结果
*/
boolean mergeResult() default true;
/**
* 指定接口是否需要授权才能访问可在admin中进行修改
*/
boolean permission() default false;
}

@ -21,6 +21,8 @@ import java.lang.annotation.Target;
@RequestMapping
public @interface ApiMapping {
// ------------ 自定义属性 ------------
/**
* 版本号默认版本号是""<br>
* 改默认版本号<code>ServiceContext.getSopServerConfig().setDefaultVersion("1.0");</code>
@ -37,6 +39,17 @@ public @interface ApiMapping {
*/
boolean mergeResult() default true;
/**
* 指定接口是否需要授权才能访问可在admin中进行修改
*/
boolean permission() default false;
// ------------ 自定义属性 end ------------
// ============ 以下是springmvc自带的属性 ============
/**
* 接口名
* Alias for {@link RequestMapping#value}.

@ -26,6 +26,8 @@ public class ServiceApiInfo {
private int ignoreValidate;
/** 是否合并结果 */
private int mergeResult;
/** 是否需要授权才能访问 */
private int permission;
public ApiMeta() {
}

@ -65,4 +65,9 @@ public class ServiceConfig {
*/
private boolean mergeResult = true;
/**
* 是否需要授权才能访问
*/
private boolean permission;
}

@ -89,6 +89,7 @@ public class DefaultRequestMappingEvent implements RequestMappingEvent {
ServiceApiInfo.ApiMeta apiMeta = new ServiceApiInfo.ApiMeta(name, path, version);
apiMeta.setIgnoreValidate(BooleanUtils.toInteger(apiMappingInfo.isIgnoreValidate()));
apiMeta.setMergeResult(BooleanUtils.toInteger(apiMappingInfo.isMergeResult()));
apiMeta.setPermission(BooleanUtils.toInteger(apiMappingInfo.isPermission()));
return apiMeta;
}
return null;

@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.BeanUtils;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
@ -50,8 +51,7 @@ public class ServiceZookeeperApiMetaManager implements ApiMetaManager {
public ServiceZookeeperApiMetaManager(Environment environment) {
this.environment = environment;
String profile = environment.getProperty("spring.profiles.active", "default");
this.routeRootPath = SOP_SERVICE_ROUTE_PATH + "-" + profile;
this.routeRootPath = SOP_SERVICE_ROUTE_PATH;
zookeeperServerAddr = environment.getProperty("spring.cloud.zookeeper.connect-string");
if (StringUtils.isEmpty(zookeeperServerAddr)) {
throw new IllegalArgumentException("未指定spring.cloud.zookeeper.connect-string参数");
@ -107,17 +107,15 @@ public class ServiceZookeeperApiMetaManager implements ApiMetaManager {
protected GatewayRouteDefinition buildGatewayRouteDefinition(ServiceApiInfo serviceApiInfo, ServiceApiInfo.ApiMeta apiMeta) {
GatewayRouteDefinition gatewayRouteDefinition = new GatewayRouteDefinition();
// 唯一id规则:接口名 + 版本号
BeanUtils.copyProperties(apiMeta, gatewayRouteDefinition);
gatewayRouteDefinition.setId(apiMeta.fetchNameVersion());
gatewayRouteDefinition.setFilters(Collections.emptyList());
gatewayRouteDefinition.setOrder(0);
List<GatewayPredicateDefinition> predicates = Arrays.asList(this.buildNameVersionPredicateDefinition(apiMeta));
gatewayRouteDefinition.setPredicates(predicates);
String uri = this.buildUri(serviceApiInfo, apiMeta);
String path = this.buildServletPath(serviceApiInfo, apiMeta);
gatewayRouteDefinition.setUri(uri);
gatewayRouteDefinition.setPath(path);
gatewayRouteDefinition.setIgnoreValidate(apiMeta.getIgnoreValidate());
gatewayRouteDefinition.setMergeResult(apiMeta.getMergeResult());
return gatewayRouteDefinition;
}

@ -45,18 +45,21 @@ public class ApiMappingHandlerMapping extends RequestMappingHandlerMapping imple
String version;
boolean ignoreValidate = false;
boolean mergeResult = true;
boolean permission = false;
ApiMapping apiMapping = method.getAnnotation(ApiMapping.class);
if (apiMapping != null) {
name = apiMapping.value()[0];
version = apiMapping.version();
ignoreValidate = apiMapping.ignoreValidate();
mergeResult = apiMapping.mergeResult();
permission = apiMapping.permission();
} else {
ApiAbility apiAbility = this.findApiAbilityAnnotation(method);
if (apiAbility != null) {
version = apiAbility.version();
ignoreValidate = apiAbility.ignoreValidate();
mergeResult = apiAbility.mergeResult();
permission = apiAbility.permission();
} else {
return super.getCustomMethodCondition(method);
}
@ -69,11 +72,17 @@ public class ApiMappingHandlerMapping extends RequestMappingHandlerMapping imple
if (!ignoreValidate) {
ignoreValidate = ServiceConfig.getInstance().isIgnoreValidate();
}
// 如果是默认配置,则采用全局配置
if (mergeResult) {
mergeResult = ServiceConfig.getInstance().isMergeResult();
}
// 如果是默认配置,则采用全局配置
if (!permission) {
permission = ServiceConfig.getInstance().isPermission();
}
apiMappingInfo.setIgnoreValidate(ignoreValidate);
apiMappingInfo.setMergeResult(mergeResult);
apiMappingInfo.setPermission(permission);
logger.info("注册接口,method:" + method + ", version:" + version);
return new ApiMappingRequestCondition(apiMappingInfo);
}

@ -11,6 +11,7 @@ public class ApiMappingInfo {
private String version;
private boolean ignoreValidate;
private boolean mergeResult;
private boolean permission;
public ApiMappingInfo(String name, String version) {
this.name = name;

@ -54,4 +54,9 @@ public class GatewayRouteDefinition {
* 是否合并结果
*/
private int mergeResult;
/**
* 是否需要授权才能访问
*/
private int permission;
}

@ -23,7 +23,7 @@
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-service-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.gitee.sop</groupId>

@ -0,0 +1,23 @@
package com.gitee.sop.bookweb.controller;
import com.gitee.sop.servercommon.annotation.ApiMapping;
import com.gitee.sop.story.api.domain.Story;
import org.springframework.web.bind.annotation.RestController;
/**
* 支付宝服务端假设签名验证通过后到达这里进行具体的业务处理
* 这里演示如何接受业务参数
* @author tanghc
*/
@RestController
public class PermissionDemoController {
@ApiMapping(value = "permission.story.get", permission = true)
public Story getStory() {
Story story = new Story();
story.setId(1);
story.setName("海底小纵队(permission.story.get)");
return story;
}
}

@ -23,7 +23,7 @@
<dependency>
<groupId>com.gitee.sop</groupId>
<artifactId>sop-gateway-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.1.0-SNAPSHOT</version>
</dependency>
<!-- ↓↓↓ 使用spring cloud zuul ↓↓↓ -->
@ -50,6 +50,17 @@
-->
<!-- ↑↑↑ 使用spring cloud gateway ↑↑↑ -->
<dependency>
<groupId>net.oschina.durcframework</groupId>
<artifactId>fastmybatis-spring-boot-starter</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
@ -60,6 +71,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>

@ -1,8 +1,14 @@
package com.gitee.sop.gateway.config;
import com.gitee.sop.gateway.entity.IsvInfo;
import com.gitee.sop.gateway.manager.ManagerInitializer;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import com.gitee.sop.gatewaycommon.bean.ApiContext;
import com.gitee.sop.gatewaycommon.gateway.configuration.AlipayGatewayConfiguration;
import com.gitee.sop.gatewaycommon.manager.IsvRoutePermissionManager;
import com.gitee.sop.gatewaycommon.secret.IsvManager;
import com.gitee.sop.gatewaycommon.zuul.configuration.AlipayZuulConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@ -30,6 +36,14 @@ public class GatewayConfig extends AlipayGatewayConfiguration {
appSecretStore.put("2019032617262200001", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlyb9aUBaljQP/vjmBFe1mF8HsWSvyfC2NTlpT/V9E+sBxTr8TSkbzJCeeeOEm4LCaVXL0Qz63MZoT24v7AIXTuMdj4jyiM/WJ4tjrWAgnmohNOegfntTto16C3l234vXz4ryWZMR/7W+MXy5B92wPGQEJ0LKFwNEoLspDEWZ7RdE53VH7w6y6sIZUfK+YkXWSwehfKPKlx+lDw3zRJ3/yvMF+U+BAdW/MfECe1GuBnCFKnlMRh3UKczWyXWkL6ItOpYHHJi/jx85op5BWDje2pY9QowzfN94+0DB3T7UvZeweu3zlP6diwAJDzLaFQX8ULfWhY+wfKxIRgs9NoiSAQIDAQAB");
ApiContext.getApiConfig().addAppSecret(appSecretStore);
}
@Autowired
ManagerInitializer managerInitializer;
@Override
protected void doAfter() {
managerInitializer.init();
}
}
/**

@ -1,9 +1,15 @@
package com.gitee.sop.gateway.config;
import com.gitee.sop.gateway.entity.IsvInfo;
import com.gitee.sop.gateway.manager.ManagerInitializer;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import com.gitee.sop.gatewaycommon.bean.ApiContext;
import com.gitee.sop.gatewaycommon.easyopen.EasyopenZuulConfiguration;
import com.gitee.sop.gatewaycommon.manager.IsvRoutePermissionManager;
import com.gitee.sop.gatewaycommon.secret.IsvManager;
import com.gitee.sop.gatewaycommon.zuul.configuration.AlipayZuulConfiguration;
import com.gitee.sop.gatewaycommon.zuul.configuration.TaobaoZuulConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@ -30,6 +36,14 @@ public class ZuulConfig extends AlipayZuulConfiguration {
appSecretStore.put("2019032617262200001", "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlyb9aUBaljQP/vjmBFe1mF8HsWSvyfC2NTlpT/V9E+sBxTr8TSkbzJCeeeOEm4LCaVXL0Qz63MZoT24v7AIXTuMdj4jyiM/WJ4tjrWAgnmohNOegfntTto16C3l234vXz4ryWZMR/7W+MXy5B92wPGQEJ0LKFwNEoLspDEWZ7RdE53VH7w6y6sIZUfK+YkXWSwehfKPKlx+lDw3zRJ3/yvMF+U+BAdW/MfECe1GuBnCFKnlMRh3UKczWyXWkL6ItOpYHHJi/jx85op5BWDje2pY9QowzfN94+0DB3T7UvZeweu3zlP6diwAJDzLaFQX8ULfWhY+wfKxIRgs9NoiSAQIDAQAB");
ApiContext.getApiConfig().addAppSecret(appSecretStore);
}
@Autowired
ManagerInitializer managerInitializer;
@Override
protected void doAfter() {
managerInitializer.init();
}
}
/**

@ -0,0 +1,48 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* 表名isv_info
* 备注isv信息表
*
* @author tanghc
*/
@Table(name = "isv_info")
@Data
public class IsvInfo {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
/** 数据库字段:id */
private Long id;
/** appKey, 数据库字段:app_key */
private String appKey;
/** secret, 数据库字段:secret */
private String secret;
/** 公钥, 数据库字段:pub_key */
private String pubKey;
/** 私钥, 数据库字段:pri_key */
private String priKey;
/** 0启用,1禁用, 数据库字段:status */
private Byte status;
/** 数据库字段:gmt_create */
private Date gmtCreate;
/** 数据库字段:gmt_modified */
private Date gmtModified;
}

@ -0,0 +1,39 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* 表名perm_isv_role
* 备注isv角色
*
* @author tanghc
*/
@Table(name = "perm_isv_role")
@Data
public class PermIsvRole {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
/** 数据库字段:id */
private Long id;
/** isv_info表id, 数据库字段:isv_id */
private Long isvId;
/** 角色code, 数据库字段:role_code */
private String roleCode;
/** 数据库字段:gmt_create */
private Date gmtCreate;
/** 数据库字段:gmt_modified */
private Date gmtModified;
}

@ -0,0 +1,39 @@
package com.gitee.sop.gateway.entity;
import lombok.Data;
import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Date;
/**
* 表名perm_role_permission
* 备注角色权限表
*
* @author tanghc
*/
@Table(name = "perm_role_permission")
@Data
public class PermRolePermission {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
/** 数据库字段:id */
private Long id;
/** 角色表code, 数据库字段:role_code */
private String roleCode;
/** api_id, 数据库字段:route_id */
private String routeId;
/** 数据库字段:gmt_create */
private Date gmtCreate;
/** 数据库字段:gmt_modified */
private Date gmtModified;
}

@ -0,0 +1,70 @@
package com.gitee.sop.gateway.manager;
import com.alibaba.fastjson.JSON;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.gateway.entity.IsvInfo;
import com.gitee.sop.gateway.mapper.IsvInfoMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.IsvDefinition;
import com.gitee.sop.gatewaycommon.manager.ZookeeperContext;
import com.gitee.sop.gatewaycommon.secret.CacheIsvManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.function.Function;
/**
* @author tanghc
*/
@Component
@Slf4j
public class DbIsvManager extends CacheIsvManager {
@Autowired
IsvInfoMapper isvInfoMapper;
@Autowired
Environment environment;
@Override
public void load(Function<Object, String> secretGetter) {
log.info("从数据库读取ISV信息,保存到本地");
List<IsvInfo> isvInfoList = isvInfoMapper.list(new Query());
isvInfoList.stream()
.forEach(isvInfo -> {
IsvDefinition isvDefinition = new IsvDefinition();
BeanUtils.copyProperties(isvInfo, isvDefinition);
String secret = secretGetter.apply(isvInfo);
isvDefinition.setSecret(secret);
this.getIsvCache().put(isvDefinition.getAppKey(), isvDefinition);
});
}
@PostConstruct
protected void after() throws Exception {
ZookeeperContext.setEnvironment(environment);
String isvChannelPath = ZookeeperContext.getIsvInfoChannelPath();
ZookeeperContext.listenPath(isvChannelPath, nodeCache -> {
String nodeData = new String(nodeCache.getCurrentData().getData());
ChannelMsg channelMsg = JSON.parseObject(nodeData, ChannelMsg.class);
final IsvDefinition isvDefinition = JSON.parseObject(channelMsg.getData(), IsvDefinition.class);
switch (channelMsg.getOperation()) {
case "update":
log.info("更新ISV信息,isvDefinition:{}", isvDefinition);
update(isvDefinition);
break;
case "remove":
log.info("删除ISV,isvDefinition:{}", isvDefinition);
remove(isvDefinition.getAppKey());
break;
}
});
}
}

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

Loading…
Cancel
Save