parent
78f85256d6
commit
43c72530d3
@ -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) |
||||
|
@ -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`测试用例进行验证 |
After Width: | Height: | Size: 233 KiB |
After Width: | Height: | Size: 327 KiB |
After Width: | Height: | Size: 94 KiB |
@ -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(); |
||||
}); |
||||
} |
||||
} |
@ -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); |
||||
}); |
||||
} |
||||
}); |
@ -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; |
||||
} |
@ -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()); |
||||
} |
||||
} |
@ -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,10 +0,0 @@ |
||||
package com.gitee.sop.adminserver.api.isv.result; |
||||
|
||||
import lombok.Data; |
||||
|
||||
@Data |
||||
public class PubPriVo { |
||||
private String pubKey; |
||||
private String priKey; |
||||
|
||||
} |
@ -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; |
||||
} |
@ -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; |
||||
} |
@ -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; |
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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; |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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); |
||||
|
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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…
Reference in new issue