parent
c850712f8b
commit
a5a0b5c476
@ -0,0 +1,9 @@ |
||||
CREATE TABLE `config_ip_blacklist` ( |
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, |
||||
`ip` varchar(64) NOT NULL DEFAULT '' COMMENT 'ip', |
||||
`remark` varchar(128) DEFAULT NULL COMMENT '备注', |
||||
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP, |
||||
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
||||
PRIMARY KEY (`id`), |
||||
UNIQUE KEY `uk_ip` (`ip`) USING BTREE |
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='IP黑名单'; |
@ -0,0 +1,184 @@ |
||||
<template> |
||||
<div class="app-container"> |
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini"> |
||||
<el-form-item label="IP"> |
||||
<el-input v-model="searchFormData.ip" :clearable="true" placeholder="输入IP" style="width: 250px;" /> |
||||
</el-form-item> |
||||
<el-form-item> |
||||
<el-button type="primary" icon="el-icon-search" @click="loadTable">查询</el-button> |
||||
</el-form-item> |
||||
</el-form> |
||||
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增IP</el-button> |
||||
<el-table |
||||
:data="pageInfo.rows" |
||||
border |
||||
highlight-current-row |
||||
> |
||||
<el-table-column |
||||
prop="ip" |
||||
label="IP" |
||||
width="200" |
||||
/> |
||||
<el-table-column |
||||
prop="remark" |
||||
label="备注" |
||||
width="300" |
||||
/> |
||||
<el-table-column |
||||
prop="gmtCreate" |
||||
label="添加时间" |
||||
width="160" |
||||
/> |
||||
<el-table-column |
||||
prop="gmtModified" |
||||
label="修改时间" |
||||
width="160" |
||||
/> |
||||
<el-table-column |
||||
label="操作" |
||||
width="150" |
||||
> |
||||
<template slot-scope="scope"> |
||||
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button> |
||||
<el-button type="text" size="mini" @click="onTableDelete(scope.row)">删除</el-button> |
||||
</template> |
||||
</el-table-column> |
||||
</el-table> |
||||
<el-pagination |
||||
background |
||||
style="margin-top: 5px" |
||||
:current-page="searchFormData.pageIndex" |
||||
:page-size="searchFormData.pageSize" |
||||
:page-sizes="[5, 10, 20, 40]" |
||||
:total="pageInfo.total" |
||||
layout="total, sizes, prev, pager, next" |
||||
@size-change="onSizeChange" |
||||
@current-change="onPageIndexChange" |
||||
/> |
||||
<!--dialog--> |
||||
<el-dialog |
||||
:title="dialogTitle" |
||||
:visible.sync="dialogVisible" |
||||
:close-on-click-modal="false" |
||||
@close="resetForm('dialogForm')" |
||||
> |
||||
<el-form |
||||
ref="dialogForm" |
||||
:rules="dialogFormRules" |
||||
:model="dialogFormData" |
||||
label-width="120px" |
||||
size="mini" |
||||
> |
||||
<el-form-item prop="ip" label="IP"> |
||||
<el-input v-show="dialogFormData.id === 0" v-model="dialogFormData.ip" /> |
||||
<span v-show="dialogFormData.id > 0">{{ dialogFormData.ip }}</span> |
||||
</el-form-item> |
||||
<el-form-item prop="remark" label="备注"> |
||||
<el-input v-model="dialogFormData.remark" /> |
||||
</el-form-item> |
||||
</el-form> |
||||
<div slot="footer" class="dialog-footer"> |
||||
<el-button @click="dialogVisible = false">取 消</el-button> |
||||
<el-button type="primary" @click="onDialogSave">保 存</el-button> |
||||
</div> |
||||
</el-dialog> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
data() { |
||||
const ipValidator = (rule, value, callback) => { |
||||
if (value === '') { |
||||
callback(new Error('请输入IP')) |
||||
} else { |
||||
const regexIP = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/ |
||||
if (!regexIP.test(value)) { |
||||
callback(new Error('IP格式不正确')) |
||||
} |
||||
callback() |
||||
} |
||||
} |
||||
return { |
||||
searchFormData: { |
||||
ip: '', |
||||
pageIndex: 1, |
||||
pageSize: 10 |
||||
}, |
||||
pageInfo: { |
||||
rows: [], |
||||
total: 0 |
||||
}, |
||||
dialogVisible: false, |
||||
dialogTitle: '', |
||||
dialogFormData: { |
||||
id: 0, |
||||
ip: '', |
||||
remark: '' |
||||
}, |
||||
dialogFormRules: { |
||||
ip: [ |
||||
{ validator: ipValidator, trigger: 'blur' }, |
||||
{ min: 1, max: 64, message: '长度在 1 到 64 个字符', trigger: 'blur' } |
||||
], |
||||
remark: [ |
||||
{ max: 100, message: '不能超过 100 个字符', trigger: 'blur' } |
||||
] |
||||
} |
||||
} |
||||
}, |
||||
created() { |
||||
this.loadTable() |
||||
}, |
||||
methods: { |
||||
loadTable: function() { |
||||
this.post('ip.blacklist.page', this.searchFormData, function(resp) { |
||||
this.pageInfo = resp.data |
||||
}) |
||||
}, |
||||
onTableUpdate: function(row) { |
||||
this.dialogTitle = '修改IP' |
||||
this.dialogVisible = true |
||||
this.$nextTick(() => { |
||||
Object.assign(this.dialogFormData, row) |
||||
}) |
||||
}, |
||||
onTableDelete: function(row) { |
||||
this.confirm(`确认要移除IP【${row.ip}】吗?`, function(done) { |
||||
const data = { |
||||
id: row.id |
||||
} |
||||
this.post('ip.blacklist.del', data, function() { |
||||
done() |
||||
this.tip('删除成功') |
||||
this.loadTable() |
||||
}) |
||||
}) |
||||
}, |
||||
onDialogSave: function() { |
||||
this.$refs.dialogForm.validate((valid) => { |
||||
if (valid) { |
||||
const uri = this.dialogFormData.id ? 'ip.blacklist.update' : 'ip.blacklist.add' |
||||
this.post(uri, this.dialogFormData, function() { |
||||
this.dialogVisible = false |
||||
this.loadTable() |
||||
}) |
||||
} |
||||
}) |
||||
}, |
||||
onSizeChange: function(size) { |
||||
this.searchFormData.pageSize = size |
||||
this.loadTable() |
||||
}, |
||||
onAdd: function() { |
||||
this.dialogTitle = '新增IP' |
||||
this.dialogVisible = true |
||||
this.dialogFormData.id = 0 |
||||
}, |
||||
onPageIndexChange: function(pageIndex) { |
||||
this.searchFormData.pageIndex = pageIndex |
||||
this.loadTable() |
||||
} |
||||
} |
||||
} |
||||
</script> |
@ -1,347 +0,0 @@ |
||||
<template> |
||||
<div class="app-container"> |
||||
<el-container> |
||||
<el-aside style="min-height: 300px;width: 200px;"> |
||||
<el-input v-model="filterText" prefix-icon="el-icon-search" placeholder="搜索服务..." style="margin-bottom:20px;" size="mini" clearable /> |
||||
<el-tree |
||||
ref="tree2" |
||||
:data="treeData" |
||||
:props="defaultProps" |
||||
:filter-node-method="filterNode" |
||||
:highlight-current="true" |
||||
:expand-on-click-node="false" |
||||
empty-text="无数据" |
||||
node-key="id" |
||||
class="filter-tree" |
||||
default-expand-all |
||||
@node-click="onNodeClick" |
||||
> |
||||
<span slot-scope="{ node, data }" class="custom-tree-node"> |
||||
<span v-if="data.label.length < 15">{{ data.label }}</span> |
||||
<span v-else> |
||||
<el-tooltip :content="data.label" class="item" effect="light" placement="right"> |
||||
<span>{{ data.label.substring(0, 15) + '...' }}</span> |
||||
</el-tooltip> |
||||
</span> |
||||
</span> |
||||
</el-tree> |
||||
</el-aside> |
||||
<el-main style="padding-top:0"> |
||||
<el-form :inline="true" :model="searchFormData" class="demo-form-inline"> |
||||
<el-form-item label="路由名称"> |
||||
<el-input v-model="searchFormData.id" placeholder="输入接口名或版本号" size="mini" /> |
||||
</el-form-item> |
||||
<el-form-item> |
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button> |
||||
</el-form-item> |
||||
</el-form> |
||||
<el-table |
||||
:data="tableData" |
||||
border |
||||
max-height="500" |
||||
> |
||||
<el-table-column |
||||
prop="name" |
||||
label="接口名 (版本号)" |
||||
width="200" |
||||
> |
||||
<template slot-scope="scope"> |
||||
{{ scope.row.name + (scope.row.version ? ' (' + scope.row.version + ')' : '') }} |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
prop="limitType" |
||||
label="限流策略" |
||||
width="120" |
||||
> |
||||
<template slot-scope slot="header"> |
||||
限流策略 <i class="el-icon-question" style="cursor: pointer" @click="onLimitTypeTipClick"></i> |
||||
</template> |
||||
<template slot-scope="scope"> |
||||
<span v-if="scope.row.limitType === 1">漏桶策略</span> |
||||
<span v-if="scope.row.limitType === 2">令牌桶策略</span> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
prop="info" |
||||
label="限流信息" |
||||
width="500" |
||||
> |
||||
<template slot-scope="scope"> |
||||
<span v-html="infoRender(scope.row)"></span> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
prop="limitStatus" |
||||
label="状态" |
||||
width="80" |
||||
> |
||||
<template slot-scope="scope"> |
||||
<span v-if="scope.row.limitStatus === 1" style="color:#67C23A">已开启</span> |
||||
<span v-if="scope.row.limitStatus === 0" style="color:#909399">已关闭</span> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column |
||||
label="操作" |
||||
width="80" |
||||
> |
||||
<template slot-scope="scope"> |
||||
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button> |
||||
</template> |
||||
</el-table-column> |
||||
</el-table> |
||||
<!-- dialog --> |
||||
<el-dialog |
||||
title="设置限流" |
||||
:visible.sync="limitDialogVisible" |
||||
:close-on-click-modal="false" |
||||
@close="onLimitDialogClose" |
||||
> |
||||
<el-form ref="limitDialogFormMain" :model="limitDialogFormData"> |
||||
<el-form-item label="id" :label-width="formLabelWidth"> |
||||
<el-input v-model="limitDialogFormData.routeId" readonly="readonly"/> |
||||
</el-form-item> |
||||
<el-form-item label="限流策略" :label-width="formLabelWidth"> |
||||
<el-radio-group v-model="limitDialogFormData.limitType"> |
||||
<el-radio :label="1">漏桶策略</el-radio> |
||||
<el-radio :label="2">令牌桶策略</el-radio> |
||||
</el-radio-group> |
||||
</el-form-item> |
||||
<el-form-item label="开启状态" :label-width="formLabelWidth"> |
||||
<el-switch |
||||
v-model="limitDialogFormData.limitStatus" |
||||
active-color="#13ce66" |
||||
inactive-color="#ff4949" |
||||
:active-value="1" |
||||
:inactive-value="0" |
||||
> |
||||
</el-switch> |
||||
</el-form-item> |
||||
</el-form> |
||||
<el-form |
||||
v-show="limitDialogFormData.limitType === 1 && limitDialogFormData.limitStatus" |
||||
ref="limitDialogFormLeaky" |
||||
:rules="rulesLeaky" |
||||
:model="limitDialogFormData" |
||||
> |
||||
<el-form-item label="每秒可处理请求数" prop="execCountPerSecond" :label-width="formLabelWidth"> |
||||
<el-input-number v-model="limitDialogFormData.execCountPerSecond" controls-position="right" :min="1" /> |
||||
</el-form-item> |
||||
<el-form-item label="错误码" prop="limitCode" :label-width="formLabelWidth"> |
||||
<el-input v-model="limitDialogFormData.limitCode" /> |
||||
</el-form-item> |
||||
<el-form-item label="错误信息" prop="limitMsg" :label-width="formLabelWidth"> |
||||
<el-input v-model="limitDialogFormData.limitMsg" /> |
||||
</el-form-item> |
||||
</el-form> |
||||
<el-form |
||||
v-show="limitDialogFormData.limitType === 2 && limitDialogFormData.limitStatus" |
||||
ref="limitDialogFormToken" |
||||
:rules="rulesToken" |
||||
:model="limitDialogFormData" |
||||
> |
||||
<el-form-item label="令牌桶容量" prop="tokenBucketCount" :label-width="formLabelWidth"> |
||||
<el-input-number v-model="limitDialogFormData.tokenBucketCount" controls-position="right" :min="1" /> |
||||
</el-form-item> |
||||
</el-form> |
||||
<div slot="footer" class="dialog-footer"> |
||||
<el-button @click="limitDialogVisible = false">取 消</el-button> |
||||
<el-button type="primary" @click="onLimitDialogSave">保 存</el-button> |
||||
</div> |
||||
</el-dialog> |
||||
</el-main> |
||||
</el-container> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
data() { |
||||
return { |
||||
filterText: '', |
||||
treeData: [], |
||||
tableData: [], |
||||
serviceId: '', |
||||
searchFormData: {}, |
||||
defaultProps: { |
||||
children: 'children', |
||||
label: 'label' |
||||
}, |
||||
// dialog |
||||
limitDialogFormData: { |
||||
routeId: '', |
||||
execCountPerSecond: 5, |
||||
limitCode: '', |
||||
limitMsg: '', |
||||
tokenBucketCount: 5, |
||||
limitStatus: 0, // 0: 停用,1:启用 |
||||
limitType: 1 |
||||
}, |
||||
rulesLeaky: { |
||||
execCountPerSecond: [ |
||||
{ required: true, message: '不能为空', trigger: 'blur' } |
||||
], |
||||
limitCode: [ |
||||
{ required: true, message: '不能为空', trigger: 'blur' }, |
||||
{ min: 1, max: 64, message: '长度在 1 到 64 个字符', trigger: 'blur' } |
||||
], |
||||
limitMsg: [ |
||||
{ required: true, message: '不能为空', trigger: 'blur' }, |
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' } |
||||
] |
||||
}, |
||||
rulesToken: { |
||||
tokenBucketCount: [ |
||||
{ required: true, message: '不能为空', trigger: 'blur' } |
||||
] |
||||
}, |
||||
formLabelWidth: '150px', |
||||
limitDialogVisible: false |
||||
|
||||
} |
||||
}, |
||||
watch: { |
||||
filterText(val) { |
||||
this.$refs.tree2.filter(val) |
||||
} |
||||
}, |
||||
created() { |
||||
this.loadTree() |
||||
}, |
||||
methods: { |
||||
// 加载树 |
||||
loadTree: function() { |
||||
this.post('zookeeper.service.list', {}, function(resp) { |
||||
const respData = resp.data |
||||
this.treeData = this.convertToTreeData(respData, 0) |
||||
}) |
||||
}, |
||||
// 树搜索 |
||||
filterNode(value, data) { |
||||
if (!value) return true |
||||
return data.label.indexOf(value) !== -1 |
||||
}, |
||||
// 树点击事件 |
||||
onNodeClick(data, node, tree) { |
||||
if (data.parentId) { |
||||
this.serviceId = data.label |
||||
this.searchFormData.serviceId = this.serviceId |
||||
this.loadTable() |
||||
} |
||||
}, |
||||
/** |
||||
* 数组转成树状结构 |
||||
* @param data 数据结构 [{ |
||||
"_parentId": 14, |
||||
"gmtCreate": "2019-01-15 09:44:38", |
||||
"gmtUpdate": "2019-01-15 09:44:38", |
||||
"id": 15, |
||||
"isShow": 1, |
||||
"name": "用户注册", |
||||
"orderIndex": 10000, |
||||
"parentId": 14 |
||||
},...] |
||||
* @param pid 初始父节点id,一般是0 |
||||
* @return 返回结果 [{ |
||||
label: '一级 1', |
||||
children: [{ |
||||
label: '二级 1-1', |
||||
children: [{ |
||||
label: '三级 1-1-1' |
||||
}] |
||||
}] |
||||
} |
||||
*/ |
||||
convertToTreeData(data, pid) { |
||||
const result = [] |
||||
const root = { |
||||
label: '服务列表', |
||||
parentId: pid |
||||
} |
||||
const children = [] |
||||
for (let i = 0; i < data.length; i++) { |
||||
const item = { label: data[i].serviceId, parentId: 1 } |
||||
children.push(item) |
||||
} |
||||
root.children = children |
||||
result.push(root) |
||||
return result |
||||
}, |
||||
// table |
||||
loadTable: function() { |
||||
this.post('route.limit.list', this.searchFormData, function(resp) { |
||||
this.tableData = resp.data |
||||
}) |
||||
}, |
||||
onSearchTable: function() { |
||||
this.loadTable() |
||||
}, |
||||
onTableUpdate: function(row) { |
||||
this.limitDialogVisible = true |
||||
this.$nextTick(() => { |
||||
Object.assign(this.limitDialogFormData, row) |
||||
}) |
||||
}, |
||||
resetForm(formName) { |
||||
const frm = this.$refs[formName] |
||||
frm && frm.resetFields() |
||||
}, |
||||
onLimitDialogClose: function() { |
||||
this.resetForm('limitDialogFormLeaky') |
||||
this.resetForm('limitDialogFormToken') |
||||
this.limitDialogVisible = false |
||||
}, |
||||
infoRender: function(row) { |
||||
if (!row.hasRecord) { |
||||
return '--' |
||||
} |
||||
const html = [] |
||||
if (row.limitType === 1) { |
||||
html.push('每秒可处理请求数:' + row.execCountPerSecond) |
||||
html.push('subCode:' + row.limitCode) |
||||
html.push('subMsg:' + row.limitMsg) |
||||
} else if (row.limitType === 2) { |
||||
html.push('令牌桶容量:' + row.tokenBucketCount) |
||||
} |
||||
return html.join(',') |
||||
}, |
||||
onLimitDialogSave: function() { |
||||
this.doValidate(function() { |
||||
this.limitDialogFormData.serviceId = this.serviceId |
||||
this.post('route.limit.update', this.limitDialogFormData, function(resp) { |
||||
this.limitDialogVisible = false |
||||
this.loadTable() |
||||
}) |
||||
}) |
||||
}, |
||||
doValidate: function(callback) { |
||||
const that = this |
||||
if (this.limitDialogFormData.limitStatus === 0) { |
||||
callback.call(this) |
||||
return |
||||
} |
||||
if (this.limitDialogFormData.limitType === 1) { |
||||
this.$refs['limitDialogFormLeaky'].validate((valid) => { |
||||
if (valid) { |
||||
callback.call(that) |
||||
} |
||||
}) |
||||
} else { |
||||
this.$refs['limitDialogFormToken'].validate((valid) => { |
||||
if (valid) { |
||||
callback.call(that) |
||||
} |
||||
}) |
||||
} |
||||
}, |
||||
onLimitTypeTipClick: function() { |
||||
const leakyRemark = '漏桶策略:每秒处理固定数量的请求,超出请求返回错误信息。' |
||||
const tokenRemark = '令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。' |
||||
const content = leakyRemark + '<br>' + tokenRemark |
||||
this.$alert(content, '限流策略', { |
||||
dangerouslyUseHTMLString: true |
||||
}) |
||||
} |
||||
} |
||||
} |
||||
</script> |
@ -0,0 +1,64 @@ |
||||
package com.gitee.sop.gateway.manager; |
||||
|
||||
import com.alibaba.fastjson.JSON; |
||||
import com.gitee.sop.gateway.mapper.IPBlacklistMapper; |
||||
import com.gitee.sop.gatewaycommon.bean.ChannelMsg; |
||||
import com.gitee.sop.gatewaycommon.manager.DefaultIPBlacklistManager; |
||||
import com.gitee.sop.gatewaycommon.manager.ZookeeperContext; |
||||
import lombok.Data; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.core.env.Environment; |
||||
|
||||
import javax.annotation.PostConstruct; |
||||
import java.util.List; |
||||
|
||||
/** |
||||
* 限流配置管理 |
||||
* @author tanghc |
||||
*/ |
||||
@Slf4j |
||||
public class DbIPBlacklistManager extends DefaultIPBlacklistManager { |
||||
|
||||
@Autowired |
||||
IPBlacklistMapper ipBlacklistMapper; |
||||
|
||||
@Autowired |
||||
Environment environment; |
||||
|
||||
@Override |
||||
public void load() { |
||||
List<String> ipList = ipBlacklistMapper.listAllIP(); |
||||
log.info("加载IP黑名单, size:{}", ipList.size()); |
||||
ipList.stream().forEach(this::add); |
||||
|
||||
} |
||||
|
||||
@PostConstruct |
||||
protected void after() throws Exception { |
||||
ZookeeperContext.setEnvironment(environment); |
||||
String path = ZookeeperContext.getIpBlacklistChannelPath(); |
||||
ZookeeperContext.listenPath(path, nodeCache -> { |
||||
String nodeData = new String(nodeCache.getCurrentData().getData()); |
||||
ChannelMsg channelMsg = JSON.parseObject(nodeData, ChannelMsg.class); |
||||
final IPDto ipDto = JSON.parseObject(channelMsg.getData(), IPDto.class); |
||||
String ip = ipDto.getIp(); |
||||
switch (channelMsg.getOperation()) { |
||||
case "add": |
||||
log.info("添加IP黑名单,ip:{}", ip); |
||||
add(ip); |
||||
break; |
||||
case "delete": |
||||
log.info("移除IP黑名单,ip:{}", ip); |
||||
remove(ip); |
||||
break; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Data |
||||
private static class IPDto { |
||||
private String ip; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,22 @@ |
||||
package com.gitee.sop.gateway.mapper; |
||||
|
||||
import org.apache.ibatis.annotations.Mapper; |
||||
import org.apache.ibatis.annotations.Select; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* IP黑名单 |
||||
* @author tanghc |
||||
*/ |
||||
@Mapper |
||||
public interface IPBlacklistMapper { |
||||
|
||||
/** |
||||
* 获取所有IP |
||||
* @return |
||||
*/ |
||||
@Select("SELECT ip FROM config_ip_blacklist") |
||||
List<String> listAllIP(); |
||||
|
||||
} |
Loading…
Reference in new issue