限流改造

1.x
tanghc 6 years ago
parent c39a087e35
commit 8ebc0925cb
  1. 111
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/LimitNewApi.java
  2. 15
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/RouteApi.java
  3. 62
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/LimitNewAddParam.java
  4. 28
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/LimitNewParam.java
  5. 67
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/param/LimitNewUpdateParam.java
  6. 82
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/service/result/LimitNewVO.java
  7. 50
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ConfigLimitDto.java
  8. 4
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/bean/ZookeeperContext.java
  9. 66
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/entity/ConfigLimit.java
  10. 12
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/mapper/ConfigLimitMapper.java
  11. 15
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/service/RouteConfigService.java
  12. 2
      sop-admin/sop-admin-vue/src/router/index.js
  13. 34
      sop-admin/sop-admin-vue/src/views/isv/index.vue
  14. 486
      sop-admin/sop-admin-vue/src/views/service/limit/index2.vue
  15. 6
      sop-admin/sop-admin-vue/src/views/service/list/index.vue
  16. 34
      sop-admin/sop-admin-vue/src/views/service/route/index.vue
  17. 7
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ApiConfig.java
  18. 99
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/ConfigLimitDto.java
  19. 2
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/bean/RouteConfig.java
  20. 67
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/gateway/filter/LimitFilter.java
  21. 9
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/limit/DefaultLimitManager.java
  22. 6
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/limit/LimitManager.java
  23. 4
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/AbstractConfiguration.java
  24. 80
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/DefaultLimitConfigManager.java
  25. 22
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/LimitConfigManager.java
  26. 4
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/manager/ZookeeperContext.java
  27. 29
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/util/RequestUtil.java
  28. 63
      sop-common/sop-gateway-common/src/main/java/com/gitee/sop/gatewaycommon/zuul/filter/PreLimitFilter.java
  29. 67
      sop-gateway/src/main/java/com/gitee/sop/gateway/entity/ConfigLimit.java
  30. 67
      sop-gateway/src/main/java/com/gitee/sop/gateway/manager/DbLimitConfigManager.java
  31. 1
      sop-gateway/src/main/java/com/gitee/sop/gateway/manager/ManagerInitializer.java
  32. 11
      sop-gateway/src/main/java/com/gitee/sop/gateway/mapper/ConfigLimitMapper.java

@ -0,0 +1,111 @@
package com.gitee.sop.adminserver.api.service;
import com.gitee.easyopen.annotation.Api;
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.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.query.expression.ValueExpression;
import com.gitee.fastmybatis.core.util.MapperUtil;
import com.gitee.fastmybatis.core.util.MyBeanUtil;
import com.gitee.sop.adminserver.api.service.param.LimitNewAddParam;
import com.gitee.sop.adminserver.api.service.param.LimitNewParam;
import com.gitee.sop.adminserver.api.service.param.LimitNewUpdateParam;
import com.gitee.sop.adminserver.api.service.param.RouteSearchParam;
import com.gitee.sop.adminserver.api.service.result.LimitNewVO;
import com.gitee.sop.adminserver.bean.ConfigLimitDto;
import com.gitee.sop.adminserver.bean.GatewayRouteDefinition;
import com.gitee.sop.adminserver.common.LimitEnum;
import com.gitee.sop.adminserver.entity.ConfigLimit;
import com.gitee.sop.adminserver.mapper.ConfigLimitMapper;
import com.gitee.sop.adminserver.service.RouteConfigService;
import com.gitee.sop.adminserver.service.RouteService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 限流
*
* @author tanghc
*/
@ApiService
@ApiDoc("服务管理")
@Slf4j
public class LimitNewApi {
@Autowired
RouteService routeService;
@Autowired
RouteConfigService routeConfigService;
@Autowired
ConfigLimitMapper configLimitMapper;
@Api(name = "config.limit.list")
@ApiDocMethod(description = "限流列表(新)", elementClass = LimitNewVO.class)
PageInfo<ConfigLimit> listLimit(LimitNewParam param) throws Exception {
Query query = Query.build(param);
query.orderby("route_id", Sort.ASC)
.orderby("app_key", Sort.ASC)
.orderby("order_index", Sort.ASC);
PageInfo<ConfigLimit> pageInfo = MapperUtil.query(configLimitMapper, query);
return pageInfo;
}
@Api(name = "config.limit.add")
@ApiDocMethod(description = "新增限流(新)")
@Transactional(rollbackFor = Exception.class)
public void createLimtit(LimitNewAddParam param) {
ConfigLimit configLimit = new ConfigLimit();
CopyUtil.copyPropertiesIgnoreNull(param, configLimit);
configLimitMapper.save(configLimit);
ConfigLimitDto configLimitDto = buildConfigLimitDto(configLimit);
try {
routeConfigService.sendLimitConfigMsg(configLimitDto);
} catch (Exception e) {
log.error("推送限流消息错误, param:{}", param, e);
throw new ApiException("新增失败,请查看日志");
}
}
@Api(name = "config.limit.update")
@ApiDocMethod(description = "修改限流(新)")
@Transactional(rollbackFor = Exception.class)
public void updateLimtit(LimitNewUpdateParam param) {
ConfigLimit configLimit = configLimitMapper.getById(param.getId());
if (configLimit == null) {
configLimit = new ConfigLimit();
CopyUtil.copyPropertiesIgnoreNull(param, configLimit);
configLimitMapper.save(configLimit);
} else {
CopyUtil.copyPropertiesIgnoreNull(param, configLimit);
configLimitMapper.update(configLimit);
}
ConfigLimitDto configLimitDto = buildConfigLimitDto(configLimit);
try {
routeConfigService.sendLimitConfigMsg(configLimitDto);
} catch (Exception e) {
log.error("推送限流消息错误, param:{}", param, e);
throw new ApiException("修改失败,请查看日志");
}
}
private ConfigLimitDto buildConfigLimitDto(ConfigLimit configLimit) {
ConfigLimitDto configLimitDto = new ConfigLimitDto();
CopyUtil.copyPropertiesIgnoreNull(configLimit, configLimitDto);
return configLimitDto;
}
}

@ -81,6 +81,21 @@ public class RouteApi {
return routeDefinitionList;
}
@Api(name = "route.list", version = "1.2")
@ApiDocMethod(description = "路由列表1.2")
List<RouteVO> listRoute2(RouteSearchParam param) throws Exception {
List<RouteVO> routeDefinitionList = routeService.getRouteDefinitionList(param)
.stream()
.map(gatewayRouteDefinition -> {
RouteVO vo = new RouteVO();
BeanUtils.copyProperties(gatewayRouteDefinition, vo);
return vo;
})
.collect(Collectors.toList());
return routeDefinitionList;
}
@Api(name = "route.update")
@ApiDocMethod(description = "修改路由")
void updateRoute(RouteParam param) throws Exception {

@ -0,0 +1,62 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 限流
* @author tanghc
*/
@Data
public class LimitNewAddParam {
/** 路由id, 数据库字段:route_id */
@ApiDocField(description = "routeId")
private String routeId;
/** 数据库字段:app_key */
@ApiDocField(description = "appKey")
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
@ApiDocField(description = "limitIp")
private String limitIp;
@ApiDocField(description = "serviceId")
@NotBlank(message = "serviceId can not null")
private String serviceId;
/** 限流策略,1:漏桶策略,2:令牌桶策略, 数据库字段:limit_type */
@ApiDocField(description = "限流策略,1:漏桶策略,2:令牌桶策略")
@NotNull(message = "limitType不能为空")
private Byte limitType;
/** 每秒可处理请求数, 数据库字段:exec_count_per_second */
@ApiDocField(description = "每秒可处理请求数")
private Integer execCountPerSecond;
/** 返回的错误码, 数据库字段:limit_code */
@ApiDocField(description = "返回的错误码")
private String limitCode;
/** 返回的错误信息, 数据库字段:limit_msg */
@ApiDocField(description = "返回的错误信息")
private String limitMsg;
/** 令牌桶容量, 数据库字段:token_bucket_count */
@ApiDocField(description = "令牌桶容量")
private Integer tokenBucketCount;
/** 1:开启,0关闭, 数据库字段:limit_status */
@ApiDocField(description = "1:开启,0关闭")
@NotNull(message = "limitStatus不能为空")
private Byte limitStatus;
@ApiDocField(description = "排序字段")
@NotNull(message = "orderIndex不能为空")
@Min(value = 0, message = "orderIndex必须大于等于0")
private Integer orderIndex;
}

@ -0,0 +1,28 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import com.gitee.fastmybatis.core.query.Operator;
import com.gitee.fastmybatis.core.query.annotation.Condition;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author tanghc
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class LimitNewParam extends ServiceSearchParam {
@ApiDocField(description = "路由id")
@Condition(operator = Operator.like)
private String routeId;
/** 数据库字段:app_key */
@ApiDocField(description = "appKey")
@Condition(operator = Operator.like)
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
@ApiDocField(description = "限流ip,多个用英文逗号隔开")
@Condition(operator = Operator.like)
private String limitIp;
}

@ -0,0 +1,67 @@
package com.gitee.sop.adminserver.api.service.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 限流
* @author tanghc
*/
@Data
public class LimitNewUpdateParam {
@ApiDocField(description = "id")
@NotNull(message = "id can not null")
@Min(value = 1, message = "id不正确")
private Long id;
/** 路由id, 数据库字段:route_id */
@ApiDocField(description = "routeId")
private String routeId;
/** 数据库字段:app_key */
@ApiDocField(description = "appKey")
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
@ApiDocField(description = "limitIp")
private String limitIp;
@ApiDocField(description = "serviceId")
@NotBlank(message = "serviceId can not null")
private String serviceId;
/** 限流策略,1:漏桶策略,2:令牌桶策略, 数据库字段:limit_type */
@ApiDocField(description = "限流策略,1:漏桶策略,2:令牌桶策略")
@NotNull(message = "limitType不能为空")
private Byte limitType;
/** 每秒可处理请求数, 数据库字段:exec_count_per_second */
@ApiDocField(description = "每秒可处理请求数")
private Integer execCountPerSecond;
/** 返回的错误码, 数据库字段:limit_code */
@ApiDocField(description = "返回的错误码")
private String limitCode;
/** 返回的错误信息, 数据库字段:limit_msg */
@ApiDocField(description = "返回的错误信息")
private String limitMsg;
/** 令牌桶容量, 数据库字段:token_bucket_count */
@ApiDocField(description = "令牌桶容量")
private Integer tokenBucketCount;
/** 1:开启,0关闭, 数据库字段:limit_status */
@ApiDocField(description = "1:开启,0关闭")
@NotNull(message = "limitStatus不能为空")
private Byte limitStatus;
@ApiDocField(description = "排序字段")
@NotNull(message = "orderIndex不能为空")
@Min(value = 0, message = "orderIndex必须大于等于0")
private Integer orderIndex;
}

@ -0,0 +1,82 @@
package com.gitee.sop.adminserver.api.service.result;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
/**
* 限流
*
* @author tanghc
*/
@Data
public class LimitNewVO {
/** 路由id, 数据库字段:route_id */
@ApiDocField(description = "路由id")
private String routeId;
/** 数据库字段:app_key */
@ApiDocField(description = "appKey")
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
@ApiDocField(description = "限流ip,多个用英文逗号隔开")
private String limitIp;
@ApiDocField(description = "限流key")
private String limitKey;
/**
* 接口名
*/
@ApiDocField(description = "接口名")
private String name;
/**
* 版本号
*/
@ApiDocField(description = "版本号")
private String version;
@ApiDocField(description = "serviceId")
private String serviceId;
/**
* 限流策略1漏桶策略2令牌桶策略, 数据库字段limit_type
*/
@ApiDocField(description = "限流策略,1:漏桶策略,2:令牌桶策略")
private Byte limitType;
/**
* 每秒可处理请求数, 数据库字段exec_count_per_second
*/
@ApiDocField(description = "每秒可处理请求数")
private Integer execCountPerSecond;
/**
* 返回的错误码, 数据库字段limit_code
*/
@ApiDocField(description = "返回的错误码")
private String limitCode;
/**
* 返回的错误信息, 数据库字段limit_msg
*/
@ApiDocField(description = "返回的错误信息")
private String limitMsg;
/**
* 令牌桶容量, 数据库字段token_bucket_count
*/
@ApiDocField(description = "令牌桶容量")
private Integer tokenBucketCount;
/**
* 1:开启0关闭, 数据库字段limit_status
*/
@ApiDocField(description = "1:开启,0关闭")
private Byte limitStatus;
/** 顺序,值小的优先执行, 数据库字段:order_index */
@ApiDocField(description = "排序字段")
private Integer orderIndex;
}

@ -0,0 +1,50 @@
package com.gitee.sop.adminserver.bean;
import lombok.Data;
import java.util.Date;
@Data
public class ConfigLimitDto {
/** 数据库字段:id */
private Long id;
/** 路由id, 数据库字段:route_id */
private String routeId;
/** 数据库字段:app_key */
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
private String limitIp;
/** 服务id, 数据库字段:service_id */
private String serviceId;
/** 限流策略,1:漏桶策略,2:令牌桶策略, 数据库字段:limit_type */
private Byte limitType;
/** 每秒可处理请求数, 数据库字段:exec_count_per_second */
private Integer execCountPerSecond;
/** 返回的错误码, 数据库字段:limit_code */
private String limitCode;
/** 返回的错误信息, 数据库字段:limit_msg */
private String limitMsg;
/** 令牌桶容量, 数据库字段:token_bucket_count */
private Integer tokenBucketCount;
/** 限流开启状态,1:开启,0关闭, 数据库字段:limit_status */
private Byte limitStatus;
/** 顺序,值小的优先执行, 数据库字段:order_index */
private Integer orderIndex;
/** 数据库字段:gmt_create */
private Date gmtCreate;
/** 数据库字段:gmt_modified */
private Date gmtModified;
}

@ -71,6 +71,10 @@ public class ZookeeperContext {
return SOP_MSG_CHANNEL_PATH + "/route-conf";
}
public static String getLimitConfigChannelPath() {
return SOP_MSG_CHANNEL_PATH + "/limit-conf";
}
public static CuratorFramework getClient() {
return client;
}

@ -0,0 +1,66 @@
package com.gitee.sop.adminserver.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;
/**
* 表名config_limit
* 备注限流配置
*
* @author tanghc
*/
@Table(name = "config_limit")
@Data
public class ConfigLimit {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
/** 数据库字段:id */
private Long id;
/** 路由id, 数据库字段:route_id */
private String routeId;
/** 数据库字段:app_key */
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
private String limitIp;
/** 服务id, 数据库字段:service_id */
private String serviceId;
/** 限流策略,1:漏桶策略,2:令牌桶策略, 数据库字段:limit_type */
private Byte limitType;
/** 每秒可处理请求数, 数据库字段:exec_count_per_second */
private Integer execCountPerSecond;
/** 返回的错误码, 数据库字段:limit_code */
private String limitCode;
/** 返回的错误信息, 数据库字段:limit_msg */
private String limitMsg;
/** 令牌桶容量, 数据库字段:token_bucket_count */
private Integer tokenBucketCount;
/** 限流开启状态,1:开启,0关闭, 数据库字段:limit_status */
private Byte limitStatus;
/** 顺序,值小的优先执行, 数据库字段:order_index */
private Integer orderIndex;
/** 数据库字段:gmt_create */
private Date gmtCreate;
/** 数据库字段:gmt_modified */
private Date gmtModified;
}

@ -0,0 +1,12 @@
package com.gitee.sop.adminserver.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.adminserver.entity.ConfigLimit;
/**
* @author tanghc
*/
public interface ConfigLimitMapper extends CrudMapper<ConfigLimit, Long> {
}

@ -2,6 +2,7 @@ package com.gitee.sop.adminserver.service;
import com.alibaba.fastjson.JSON;
import com.gitee.sop.adminserver.bean.ChannelMsg;
import com.gitee.sop.adminserver.bean.ConfigLimitDto;
import com.gitee.sop.adminserver.bean.RouteConfigDto;
import com.gitee.sop.adminserver.bean.ZookeeperContext;
import lombok.extern.slf4j.Slf4j;
@ -27,4 +28,18 @@ public class RouteConfigService {
log.info("消息推送--路由配置(update), path:{}, data:{}", path, jsonData);
ZookeeperContext.createOrUpdateData(path, jsonData);
}
/**
* 推送路由配置
* @param routeConfigDto
* @throws Exception
*/
public void sendLimitConfigMsg(ConfigLimitDto routeConfigDto) throws Exception {
String configData = JSON.toJSONString(routeConfigDto);
ChannelMsg channelMsg = new ChannelMsg("update", configData);
String jsonData = JSON.toJSONString(channelMsg);
String path = ZookeeperContext.getLimitConfigChannelPath();
log.info("消息推送--限流配置(update), path:{}, data:{}", path, jsonData);
ZookeeperContext.createOrUpdateData(path, jsonData);
}
}

@ -76,7 +76,7 @@ export const constantRoutes = [
{
path: 'limit',
name: 'Limit',
component: () => import('@/views/service/limit/index'),
component: () => import('@/views/service/limit/index2'),
meta: { title: '限流管理' }
}
]

@ -1,20 +1,19 @@
<template>
<div class="app-container">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
<el-form-item label="appKey">
<el-input v-model="searchFormData.appKey" :clearable="true" placeholder="appKey" size="mini" style="width: 250px;" />
<el-input v-model="searchFormData.appKey" :clearable="true" placeholder="appKey" style="width: 250px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button>
<el-button type="primary" icon="el-icon-search" @click="onSearchTable">查询</el-button>
</el-form-item>
</el-form>
<el-button type="primary" size="mini" icon="el-icon-plus" @click="onAdd">新增ISV</el-button>
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增ISV</el-button>
<el-table
:data="pageInfo.list"
border
fit
highlight-current-row
style="margin-top: 20px;"
>
<el-table-column
prop="id"
@ -110,34 +109,40 @@
:close-on-click-modal="false"
@close="onIsvDialogClose"
>
<el-form ref="isvForm" :rules="rulesIsvForm" :model="isvDialogFormData">
<el-form-item label="" :label-width="formLabelWidth">
<el-form
ref="isvForm"
:rules="rulesIsvForm"
:model="isvDialogFormData"
label-width="120px"
size="mini"
>
<el-form-item label="">
<el-button size="mini" @click="onDataGen">一键生成数据</el-button>
</el-form-item>
<el-form-item prop="appKey" label="appKey" :label-width="formLabelWidth">
<el-form-item prop="appKey" label="appKey">
<el-input v-model="isvDialogFormData.appKey" size="mini" />
</el-form-item>
<el-form-item prop="signType" label="签名方式" :label-width="formLabelWidth">
<el-form-item prop="signType" label="签名方式">
<el-radio-group v-model="isvDialogFormData.signType">
<el-radio :label="1" name="status">RSA2</el-radio>
<el-radio :label="2" name="status">MD5</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-show="isvDialogFormData.signType === 2" prop="secret" label="secret" :label-width="formLabelWidth">
<el-form-item v-show="isvDialogFormData.signType === 2" prop="secret" label="secret">
<el-input v-model="isvDialogFormData.secret" size="mini" />
</el-form-item>
<el-form-item v-show="isvDialogFormData.signType === 1" prop="pubKey" label="公钥" :label-width="formLabelWidth">
<el-form-item v-show="isvDialogFormData.signType === 1" prop="pubKey" label="公钥">
<el-input v-model="isvDialogFormData.pubKey" type="textarea" />
</el-form-item>
<el-form-item v-show="isvDialogFormData.signType === 1" prop="priKey" label="私钥" :label-width="formLabelWidth">
<el-form-item v-show="isvDialogFormData.signType === 1" prop="priKey" label="私钥">
<el-input v-model="isvDialogFormData.priKey" type="textarea" />
</el-form-item>
<el-form-item label="角色" :label-width="formLabelWidth">
<el-form-item label="角色">
<el-checkbox-group v-model="isvDialogFormData.roleCode">
<el-checkbox v-for="item in roles" :key="item.roleCode" :label="item.roleCode">{{ item.description }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="状态" :label-width="formLabelWidth">
<el-form-item label="状态">
<el-radio-group v-model="isvDialogFormData.status">
<el-radio :label="1" name="status">启用</el-radio>
<el-radio :label="2" name="status">禁用</el-radio>
@ -175,7 +180,6 @@ export default {
callback()
}
return {
formLabelWidth: '120px',
searchFormData: {
appKey: ''
},

@ -0,0 +1,486 @@
<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" size="mini">
<el-form-item label="路由ID">
<el-input v-model="searchFormData.routeId" placeholder="接口名,支持模糊查询" clearable />
</el-form-item>
<el-form-item label="AppKey">
<el-input v-model="searchFormData.appKey" placeholder="AppKey,支持模糊查询" clearable />
</el-form-item>
<el-form-item label="IP">
<el-input v-model="searchFormData.limitIp" placeholder="ip,支持模糊查询" clearable />
</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-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增限流</el-button>
<el-table
:data="pageInfo.list"
border
>
<el-table-column
prop="limitKey"
label="限流维度"
width="400"
>
<template slot-scope="scope">
<div v-html="limitRender(scope.row)"></div>
</template>
</el-table-column>
<el-table-column
prop="limitType"
label="限流策略"
width="120"
>
<template slot="header" slot-scope>
限流策略 <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="250"
>
<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
prop="orderIndex"
label="排序"
width="80"
/>
<el-table-column
prop="gmtCreate"
label="创建时间"
width="160"
/>
<el-table-column
prop="gmtModified"
label="修改时间"
width="160"
/>
<el-table-column
label="操作"
fixed="right"
width="80"
>
<template slot-scope="scope">
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
background
style="margin-top: 5px"
:current-page="pageInfo.pageIndex"
:page-sizes="[5, 10, 20, 40]"
:page-size="pageInfo.pageSize"
:total="pageInfo.total"
layout="total, sizes, prev, pager, next"
@size-change="onSizeChange"
@current-change="onPageIndexChange"
/>
<!-- dialog -->
<el-dialog
:title="dlgTitle"
:visible.sync="limitDialogVisible"
:close-on-click-modal="false"
@close="onLimitDialogClose"
>
<el-form
ref="limitDialogForm"
:model="limitDialogFormData"
:rules="rulesLimit"
label-width="150px"
size="mini"
>
<el-form-item label="限流维度" prop="typeKey">
<el-checkbox-group v-model="limitDialogFormData.typeKey">
<el-checkbox v-model="limitDialogFormData.typeKey[0]" :label="1" name="typeKey" @change="checked=>onLimitKeyTypeChange(checked, 'routeId')">路由ID</el-checkbox>
<el-checkbox v-model="limitDialogFormData.typeKey[1]" :label="2" name="typeKey" @change="checked=>onLimitKeyTypeChange(checked, 'appKey')">AppKey</el-checkbox>
<el-checkbox v-model="limitDialogFormData.typeKey[2]" :label="3" name="typeKey" @change="checked=>onLimitKeyTypeChange(checked, 'limitIp')">IP</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item v-show="checkTypeKey(1)" prop="routeId" label="路由ID" :rules="checkTypeKey(1) ? rulesLimit.routeId : []">
<el-select v-model="limitDialogFormData.routeId" filterable placeholder="可筛选" style="width: 300px;">
<el-option
v-for="item in routeList"
:key="item.id"
:label="item.id"
:value="item.id"
>
<span style="float: left">{{ item.name }}</span>
<span style="float: right; color: #8492a6; font-size: 13px">{{ item.version }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item v-show="checkTypeKey(2)" prop="appKey" label="AppKey" :rules="checkTypeKey(2) ? rulesLimit.appKey : []">
<el-input v-model="limitDialogFormData.appKey" placeholder="需要限流的appKey" />
</el-form-item>
<el-form-item v-show="checkTypeKey(3)" label="限流IP" prop="limitIp" :rules="checkTypeKey(3) ? rulesLimit.ip : []">
<el-input v-model="limitDialogFormData.limitIp" type="textarea" :rows="2" placeholder="多个用英文逗号隔开" />
</el-form-item>
<el-form-item label="限流策略">
<el-radio-group v-model="limitDialogFormData.limitType">
<el-radio :label="1">漏桶策略</el-radio>
<el-radio :label="2">令牌桶策略</el-radio>
</el-radio-group>
<i class="el-icon-question limit-tip" @click="onLimitTypeTipClick"></i>
</el-form-item>
<el-form-item label="开启状态">
<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-item label="排序" prop="orderIndex">
<el-input-number v-model="limitDialogFormData.orderIndex" controls-position="right" :min="0" />
<el-tooltip class="item" content="值小优先执行" placement="top">
<i class="el-icon-question limit-tip"></i>
</el-tooltip>
</el-form-item>
<el-form-item v-show="isLeakyType()" label="每秒可处理请求数" prop="execCountPerSecond" :rules="isLeakyType() ? rulesLimit.execCountPerSecond : []">
<el-input-number v-model="limitDialogFormData.execCountPerSecond" controls-position="right" :min="1" />
</el-form-item>
<el-form-item v-show="isLeakyType()" label="错误码" prop="limitCode" :rules="isLeakyType() ? rulesLimit.limitCode : []">
<el-input v-model="limitDialogFormData.limitCode" />
</el-form-item>
<el-form-item v-show="isLeakyType()" label="错误信息" prop="limitMsg" :rules="isLeakyType() ? rulesLimit.limitMsg : []">
<el-input v-model="limitDialogFormData.limitMsg" />
</el-form-item>
<el-form-item v-show="isTokenType()" label="令牌桶容量" prop="tokenBucketCount" :rules="isTokenType() ? rulesLimit.tokenBucketCount : []">
<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: {},
pageInfo: {
list: [],
pageIndex: 1,
pageSize: 10,
total: 0
},
routeList: [],
defaultProps: {
children: 'children',
label: 'label'
},
// dialog
dlgTitle: '设置限流',
limitDialogVisible: false,
limitDialogFormData: {
id: 0,
routeId: '',
appKey: '',
limitIp: '',
limitKey: '',
execCountPerSecond: 5,
limitCode: '',
limitMsg: '',
tokenBucketCount: 5,
limitStatus: 0, // 0: 1
limitType: 1,
orderIndex: 0,
typeKey: []
},
rulesLimit: {
typeKey: [
{ type: 'array', required: true, message: '请至少选择一个', trigger: 'change' }
],
routeId: [
{ required: true, message: '不能为空', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
appKey: [
{ required: true, message: '不能为空', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
ip: [
{ required: true, message: '不能为空', trigger: 'blur' },
{ min: 1, max: 500, message: '长度在 1 到 500 个字符', trigger: 'blur' }
],
// leaky
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' }
],
// token
tokenBucketCount: [
{ required: true, message: '不能为空', trigger: 'blur' }
],
orderIndex: [
{ required: true, message: '不能为空', trigger: 'blur' }
]
}
}
},
watch: {
filterText(val) {
this.$refs.tree2.filter(val)
}
},
created() {
this.loadTree()
},
methods: {
//
loadTree: function() {
this.post('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()
this.loadRouteList(this.serviceId)
}
},
/**
* 数组转成树状结构
* @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('config.limit.list', this.searchFormData, function(resp) {
this.pageInfo = resp.data
})
},
loadRouteList: function(serviceId) {
this.post('route.list/1.2', { serviceId: serviceId }, function(resp) {
this.routeList = resp.data
})
},
onAdd: function() {
if (!this.serviceId) {
this.tip('请选择服务', 'info')
return
}
this.dlgTitle = '新增限流'
this.limitDialogFormData.id = 0
this.limitDialogVisible = true
},
onSearchTable: function() {
this.loadTable()
},
onTableUpdate: function(row) {
this.dlgTitle = '修改限流'
this.limitDialogVisible = true
this.$nextTick(() => {
Object.assign(this.limitDialogFormData, row)
if (row.routeId) {
this.limitDialogFormData.typeKey.push(1)
}
if (row.appKey) {
this.limitDialogFormData.typeKey.push(2)
}
if (row.limitIp) {
this.limitDialogFormData.typeKey.push(3)
}
})
},
resetForm(formName) {
const frm = this.$refs[formName]
frm && frm.resetFields()
},
onLimitDialogClose: function() {
this.resetForm('limitDialogForm')
this.limitDialogVisible = false
},
infoRender: function(row) {
const html = []
if (row.limitType === 1) {
html.push('每秒可处理请求数:' + row.execCountPerSecond)
html.push('<br>subCode:' + row.limitCode)
html.push('<br>subMsg:' + row.limitMsg)
} else if (row.limitType === 2) {
html.push('令牌桶容量:' + row.tokenBucketCount)
}
return html.join('')
},
onLimitDialogSave: function() {
this.$refs['limitDialogForm'].validate((valid) => {
if (valid) {
this.limitDialogFormData.serviceId = this.serviceId
const uri = this.limitDialogFormData.id ? 'config.limit.update' : 'config.limit.add'
this.post(uri, this.limitDialogFormData, function(resp) {
this.limitDialogVisible = false
this.loadTable()
})
}
})
},
onLimitTypeTipClick: function() {
const leakyRemark = '漏桶策略:每秒处理固定数量的请求,超出请求返回错误信息。'
const tokenRemark = '令牌桶策略:每秒放置固定数量的令牌数,每个请求进来后先去拿令牌,拿到了令牌才能继续,拿不到则等候令牌重新生成了再拿。'
const content = leakyRemark + '<br>' + tokenRemark
this.$alert(content, '限流策略', {
dangerouslyUseHTMLString: true
})
},
onSizeChange: function(size) {
this.searchFormData.pageSize = size
this.loadTable()
},
onPageIndexChange: function(pageIndex) {
this.searchFormData.pageIndex = pageIndex
this.loadTable()
},
onLimitKeyTypeChange: function(checked, name) {
if (!checked) {
this.limitDialogFormData[name] = ''
}
},
checkTypeKey: function(val) {
return this.limitDialogFormData.typeKey.find((value, index, arr) => {
return value === val
})
},
isLeakyType: function() {
return this.limitDialogFormData.limitType === 1
},
isTokenType: function() {
return this.limitDialogFormData.limitType === 2
},
limitRender: function(row) {
const html = []
const val = []
html.push('(')
if (row.routeId) {
val.push(row.routeId)
html.push('路由ID')
}
if (row.appKey) {
val.push(' + ' + row.appKey)
html.push(' + AppKey')
}
if (row.limitIp) {
val.push(' + ' + row.limitIp)
html.push(' + IP')
}
html.push(')')
return val.join('') + '<br>' + html.join('')
}
}
}
</script>
<style scoped>
.limit-tip {
cursor: pointer;
margin-left: 10px;
}
</style>

@ -1,11 +1,11 @@
<template>
<div class="app-container">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
<el-form-item label="serviceId">
<el-input v-model="searchFormData.serviceId" :clearable="true" placeholder="serviceId" size="mini" style="width: 250px;" />
<el-input v-model="searchFormData.serviceId" :clearable="true" placeholder="serviceId" style="width: 250px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button>
<el-button type="primary" icon="el-icon-search" @click="onSearchTable">查询</el-button>
</el-form-item>
</el-form>
<el-table

@ -27,12 +27,12 @@
</el-tree>
</el-aside>
<el-main style="padding-top:0">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
<el-form-item label="路由名称">
<el-input v-model="searchFormData.id" placeholder="输入接口名或版本号" size="mini" />
<el-input v-model="searchFormData.id" placeholder="输入接口名或版本号" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="onSearchTable">查询</el-button>
<el-button type="primary" icon="el-icon-search" @click="onSearchTable">查询</el-button>
</el-form-item>
</el-form>
<el-table
@ -111,17 +111,21 @@
</el-table>
<!-- route dialog -->
<el-dialog title="修改路由" :visible.sync="routeDialogVisible" :close-on-click-modal="false">
<el-form :model="routeDialogFormData">
<el-form-item label="id" :label-width="formLabelWidth">
<el-form
:model="routeDialogFormData"
label-width="120px"
size="mini"
>
<el-form-item label="id">
<el-input v-model="routeDialogFormData.id" readonly="readonly" />
</el-form-item>
<el-form-item label="uri" :label-width="formLabelWidth">
<el-form-item label="uri">
<el-input v-model="routeDialogFormData.uri" />
</el-form-item>
<el-form-item label="path" :label-width="formLabelWidth">
<el-form-item label="path">
<el-input v-model="routeDialogFormData.path" />
</el-form-item>
<el-form-item label="状态" :label-width="formLabelWidth">
<el-form-item label="状态">
<el-radio-group v-model="routeDialogFormData.status">
<el-radio :label="1" name="status">启用</el-radio>
<el-radio :label="2" name="status" style="color:#F56C6C">禁用</el-radio>
@ -134,12 +138,16 @@
</div>
</el-dialog>
<!-- auth dialog -->
<el-dialog title="修改路由" :visible.sync="authDialogVisible" :close-on-click-modal="false">
<el-form :model="authDialogFormData">
<el-form-item label="id" :label-width="formLabelWidth">
<el-dialog title="路由授权" :visible.sync="authDialogVisible" :close-on-click-modal="false">
<el-form
:model="authDialogFormData"
label-width="120px"
size="mini"
>
<el-form-item label="id">
<el-input v-model="authDialogFormData.routeId" readonly="readonly" />
</el-form-item>
<el-form-item label="角色" :label-width="formLabelWidth">
<el-form-item label="角色">
<el-checkbox-group v-model="authDialogFormData.roleCode">
<el-checkbox v-for="item in roles" :key="item.roleCode" :label="item.roleCode">{{ item.description }}</el-checkbox>
</el-checkbox-group>
@ -172,9 +180,7 @@ export default {
routeDialogFormData: {
status: 1
},
formLabelWidth: '120px',
routeDialogVisible: false,
roles: [],
authDialogFormData: {
routeId: '',

@ -6,8 +6,10 @@ import com.gitee.sop.gatewaycommon.gateway.result.GatewayResultExecutor;
import com.gitee.sop.gatewaycommon.limit.DefaultLimitManager;
import com.gitee.sop.gatewaycommon.limit.LimitManager;
import com.gitee.sop.gatewaycommon.manager.DefaultIsvRoutePermissionManager;
import com.gitee.sop.gatewaycommon.manager.DefaultLimitConfigManager;
import com.gitee.sop.gatewaycommon.manager.DefaultRouteConfigManager;
import com.gitee.sop.gatewaycommon.manager.IsvRoutePermissionManager;
import com.gitee.sop.gatewaycommon.manager.LimitConfigManager;
import com.gitee.sop.gatewaycommon.manager.RouteConfigManager;
import com.gitee.sop.gatewaycommon.param.ParamBuilder;
import com.gitee.sop.gatewaycommon.result.DataNameBuilder;
@ -106,6 +108,11 @@ public class ApiConfig {
*/
private RouteConfigManager routeConfigManager = new DefaultRouteConfigManager();
/**
* 限流配置
*/
private LimitConfigManager limitConfigManager = new DefaultLimitConfigManager();
/**
* 限流管理
*/

@ -0,0 +1,99 @@
package com.gitee.sop.gatewaycommon.bean;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.RateLimiter;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Data
public class ConfigLimitDto {
/** 数据库字段:id */
private Long id;
/** 路由id, 数据库字段:route_id */
private String routeId;
/** 数据库字段:app_key */
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
private String limitIp;
/** 服务id, 数据库字段:service_id */
private String serviceId;
/** 限流策略,1:漏桶策略,2:令牌桶策略, 数据库字段:limit_type */
private Byte limitType;
/** 每秒可处理请求数, 数据库字段:exec_count_per_second */
private Integer execCountPerSecond;
/** 返回的错误码, 数据库字段:limit_code */
private String limitCode;
/** 返回的错误信息, 数据库字段:limit_msg */
private String limitMsg;
/** 令牌桶容量, 数据库字段:token_bucket_count */
private Integer tokenBucketCount;
/** 限流开启状态,1:开启,0关闭, 数据库字段:limit_status */
private Byte limitStatus;
/** 顺序,值小的优先执行, 数据库字段:order_index */
private Integer orderIndex;
/** 数据库字段:gmt_create */
private Date gmtCreate;
/** 数据库字段:gmt_modified */
private Date gmtModified;
/**
* 漏桶计数器
*/
private LoadingCache<Long, AtomicLong> counter = CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>() {
@Override
public AtomicLong load(Long seconds) throws Exception {
return new AtomicLong(0);
}
});
/**
* 令牌桶
*/
@Getter(AccessLevel.PRIVATE)
@Setter(AccessLevel.PRIVATE)
private volatile RateLimiter rateLimiter;
public synchronized void initRateLimiter() {
rateLimiter = RateLimiter.create(tokenBucketCount);
}
/**
* 获取令牌桶
* @return
*/
public RateLimiter fetchRateLimiter() {
if (rateLimiter == null) {
synchronized (this.id) {
if (rateLimiter == null) {
rateLimiter = RateLimiter.create(tokenBucketCount);
}
}
}
return rateLimiter;
}
}

@ -73,7 +73,7 @@ public class RouteConfig {
* 获取令牌桶
* @return
*/
public synchronized RateLimiter fetchRateLimiter() {
public RateLimiter fetchRateLimiter() {
if (rateLimiter == null) {
synchronized (this.routeId) {
if (rateLimiter == null) {

@ -1,19 +1,15 @@
package com.gitee.sop.gatewaycommon.gateway.filter;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import com.gitee.sop.gatewaycommon.bean.ApiContext;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
import com.gitee.sop.gatewaycommon.bean.SopConstants;
import com.gitee.sop.gatewaycommon.exception.ApiException;
import com.gitee.sop.gatewaycommon.limit.LimitManager;
import com.gitee.sop.gatewaycommon.limit.LimitType;
import com.gitee.sop.gatewaycommon.manager.RouteConfigManager;
import com.gitee.sop.gatewaycommon.manager.LimitConfigManager;
import com.gitee.sop.gatewaycommon.message.ErrorImpl;
import com.gitee.sop.gatewaycommon.param.ApiParam;
import com.gitee.sop.gatewaycommon.param.ParamNames;
import com.gitee.sop.gatewaycommon.util.RouteUtil;
import com.gitee.sop.gatewaycommon.validate.Validator;
import com.gitee.sop.gatewaycommon.zuul.ZuulContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
@ -21,7 +17,10 @@ import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author tanghc
@ -36,26 +35,25 @@ public class LimitFilter implements GlobalFilter, Ordered {
if (!apiConfig.isOpenLimit()) {
return chain.filter(exchange);
}
Map<String, ?> apiParam = exchange.getAttribute(SopConstants.CACHE_API_PARAM);
String routeId = apiParam.get(ParamNames.API_NAME).toString() + apiParam.get(ParamNames.VERSION_NAME);
RouteConfigManager routeConfigManager = apiConfig.getRouteConfigManager();
RouteConfig routeConfig = routeConfigManager.get(routeId);
if (routeConfig == null) {
return chain.filter(exchange);
ApiParam apiParam = exchange.getAttribute(SopConstants.CACHE_API_PARAM);
ConfigLimitDto configLimitDto = this.findConfigLimitDto(apiConfig, apiParam, exchange);
if (configLimitDto == null) {
return null;
}
// 某个路由限流功能未开启
if (routeConfig.getLimitStatus() == RouteConfig.LIMIT_STATUS_CLOSE) {
return chain.filter(exchange);
// 单个限流功能未开启
if (configLimitDto.getLimitStatus() == RouteConfig.LIMIT_STATUS_CLOSE) {
return null;
}
byte limitType = routeConfig.getLimitType().byteValue();
byte limitType = configLimitDto.getLimitType().byteValue();
LimitManager limitManager = ApiConfig.getInstance().getLimitManager();
// 如果是漏桶策略
if (limitType == LimitType.LEAKY_BUCKET.getType()) {
boolean acquire = limitManager.acquire(routeConfig);
boolean acquire = limitManager.acquire(configLimitDto);
if (!acquire) {
throw new ApiException(new ErrorImpl(routeConfig.getLimitCode(), routeConfig.getLimitMsg()));
throw new ApiException(new ErrorImpl(configLimitDto.getLimitCode(), configLimitDto.getLimitMsg()));
}
} else if (limitType == LimitType.TOKEN_BUCKET.getType()) {
limitManager.acquireToken(routeConfig);
limitManager.acquireToken(configLimitDto);
}
return chain.filter(exchange);
}
@ -64,4 +62,33 @@ public class LimitFilter implements GlobalFilter, Ordered {
public int getOrder() {
return Orders.LIMIT_ORDER;
}
protected ConfigLimitDto findConfigLimitDto(ApiConfig apiConfig, ApiParam apiParam, ServerWebExchange exchange) {
LimitConfigManager limitConfigManager = apiConfig.getLimitConfigManager();
String routeId = apiParam.fetchNameVersion();
String appKey = apiParam.fetchAppKey();
String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
String[] limitKeys = new String[]{
routeId,
appKey,
routeId + appKey,
ip + routeId,
ip + appKey,
ip + routeId + appKey,
};
List<ConfigLimitDto> limitConfigList = new ArrayList<>();
for (String limitKey : limitKeys) {
ConfigLimitDto configLimitDto = limitConfigManager.get(limitKey);
limitConfigList.add(configLimitDto);
}
if (limitConfigList.isEmpty()) {
return null;
}
Collections.sort(limitConfigList, Comparator.comparing(ConfigLimitDto::getOrderIndex));
return limitConfigList.get(0);
}
}

@ -1,5 +1,6 @@
package com.gitee.sop.gatewaycommon.limit;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
import com.google.common.cache.LoadingCache;
import lombok.extern.slf4j.Slf4j;
@ -14,24 +15,24 @@ import java.util.concurrent.atomic.AtomicLong;
public class DefaultLimitManager implements LimitManager {
@Override
public double acquireToken(RouteConfig routeConfig) {
public double acquireToken(ConfigLimitDto routeConfig) {
if (routeConfig.getLimitStatus() == RouteConfig.LIMIT_STATUS_CLOSE) {
return 0;
}
if (LimitType.LEAKY_BUCKET.getType() == routeConfig.getLimitType().byteValue()) {
throw new IllegalStateException("漏桶策略无法调用此方法");
return 0;
}
return routeConfig.fetchRateLimiter().acquire();
}
@Override
public boolean acquire(RouteConfig routeConfig) {
public boolean acquire(ConfigLimitDto routeConfig) {
if (routeConfig.getLimitStatus() == RouteConfig.LIMIT_STATUS_CLOSE) {
return true;
}
if (LimitType.TOKEN_BUCKET.getType() == routeConfig.getLimitType().byteValue()) {
throw new IllegalStateException("令牌桶策略无法调用此方法");
return true;
}
int execCountPerSecond = routeConfig.getExecCountPerSecond();
long currentSeconds = System.currentTimeMillis() / 1000;

@ -1,6 +1,6 @@
package com.gitee.sop.gatewaycommon.limit;
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
/**
* 限流管理
@ -15,7 +15,7 @@ public interface LimitManager {
* @param routeConfig 路由配置
* @return 返回耗时时间
*/
double acquireToken(RouteConfig routeConfig);
double acquireToken(ConfigLimitDto routeConfig);
/**
* 是否需要限流如果使用{@link LimitType#LEAKY_BUCKET
@ -24,6 +24,6 @@ public interface LimitManager {
* @param routeConfig 路由配置
* @return 如果返回true表示可以执行业务代码返回false则需要限流
*/
boolean acquire(RouteConfig routeConfig);
boolean acquire(ConfigLimitDto routeConfig);
}

@ -49,6 +49,10 @@ public class AbstractConfiguration implements ApplicationContextAware {
return ApiConfig.getInstance().getRouteConfigManager();
}
@Bean
LimitConfigManager limitConfigManager() {
return ApiConfig.getInstance().getLimitConfigManager();
}
/**
* 跨域过滤器
*

@ -0,0 +1,80 @@
package com.gitee.sop.gatewaycommon.manager;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
import org.apache.commons.lang.StringUtils;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author tanghc
*/
public class DefaultLimitConfigManager implements LimitConfigManager {
/**
* key: limitKey
*/
protected static Map<String, ConfigLimitDto> limitCache = new ConcurrentHashMap<>();
protected static Map<Long, Set<String>> idKeyMap = new HashMap<>();
@Override
public void update(ConfigLimitDto configLimitDto) {
configLimitDto.initRateLimiter();
Long id = configLimitDto.getId();
this.remove(id);
Set<String> keys = this.buildKeys(configLimitDto);
idKeyMap.put(id, keys);
for (String key : keys) {
this.doUpdate(key, configLimitDto);
}
}
protected Set<String> buildKeys(ConfigLimitDto configLimitDto) {
Set<String> keys = new HashSet<>();
String routeId = Optional.ofNullable(configLimitDto.getRouteId()).orElse("");
String appKey = Optional.ofNullable(configLimitDto.getAppKey()).orElse("");
String limitIp = Optional.ofNullable(configLimitDto.getLimitIp()).orElse("").replaceAll("\\s", "");
String baseKey = routeId.trim() + appKey.trim();
keys.add(baseKey);
if (StringUtils.isNotBlank(limitIp)) {
String[] ips = limitIp.split("\\,|\\,");
for (String ip : ips) {
keys.add(ip + baseKey);
}
}
return keys;
}
protected void doUpdate(String key, ConfigLimitDto configLimitDto) {
if (StringUtils.isBlank(key)) {
return;
}
limitCache.put(key, configLimitDto);
}
protected void remove(Long id) {
Set<String> list = idKeyMap.getOrDefault(id, Collections.emptySet());
for (String key : list) {
limitCache.remove(key);
}
}
@Override
public ConfigLimitDto get(String limitKey) {
return limitCache.get(limitKey);
}
@Override
public void load() {
}
}

@ -0,0 +1,22 @@
package com.gitee.sop.gatewaycommon.manager;
import com.gitee.sop.gatewaycommon.bean.BeanInitializer;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
/**
* @author tanghc
*/
public interface LimitConfigManager extends BeanInitializer {
/**
* 更新限流配置
* @param configLimitDto 路由配置
*/
void update(ConfigLimitDto configLimitDto);
/**
* 获取限流配置
* @param limitKey 路由id
* @return 返回ConfigLimitDto
*/
ConfigLimitDto get(String limitKey);
}

@ -76,6 +76,10 @@ public class ZookeeperContext {
return SOP_MSG_CHANNEL_PATH + "/route-conf";
}
public static String getLimitConfigChannelPath() {
return SOP_MSG_CHANNEL_PATH + "/limit-conf";
}
public static CuratorFramework getClient() {
return client;
}

@ -11,7 +11,9 @@ import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.URLDecoder;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -126,4 +128,31 @@ public class RequestUtil {
return IOUtils.toString(request.getInputStream(), UTF8);
}
public static String getIP(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if ("127.0.0.1".equals(ipAddress)) {
try {
InetAddress inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (UnknownHostException var3) {
}
}
}
if (ipAddress != null && ipAddress.length() > 15 && ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
return ipAddress;
}
}

@ -1,19 +1,25 @@
package com.gitee.sop.gatewaycommon.zuul.filter;
import com.gitee.sop.gatewaycommon.bean.ApiConfig;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
import com.gitee.sop.gatewaycommon.bean.RouteConfig;
import com.gitee.sop.gatewaycommon.bean.TargetRoute;
import com.gitee.sop.gatewaycommon.exception.ApiException;
import com.gitee.sop.gatewaycommon.limit.LimitManager;
import com.gitee.sop.gatewaycommon.limit.LimitType;
import com.gitee.sop.gatewaycommon.manager.RouteConfigManager;
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext;
import com.gitee.sop.gatewaycommon.manager.LimitConfigManager;
import com.gitee.sop.gatewaycommon.message.ErrorImpl;
import com.gitee.sop.gatewaycommon.param.ApiParam;
import com.gitee.sop.gatewaycommon.util.RequestUtil;
import com.gitee.sop.gatewaycommon.zuul.ZuulContext;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* 限流拦截器
* @author tanghc
@ -30,33 +36,62 @@ public class PreLimitFilter extends BaseZuulFilter {
}
@Override
protected Object doRun(RequestContext requestContext) throws ZuulException {
protected Object doRun(RequestContext requestContext) throws ZuulException
{
ApiConfig apiConfig = ApiConfig.getInstance();
// 限流功能未开启,直接返回
if (!apiConfig.isOpenLimit()) {
return null;
}
ApiParam apiParam = ZuulContext.getApiParam();
String routeId = apiParam.getRouteId();
RouteConfigManager routeConfigManager = apiConfig.getRouteConfigManager();
RouteConfig routeConfig = routeConfigManager.get(routeId);
if (routeConfig == null) {
ConfigLimitDto configLimitDto = this.findConfigLimitDto(apiConfig, apiParam, requestContext.getRequest());
if (configLimitDto == null) {
return null;
}
// 某个路由限流功能未开启
if (routeConfig.getLimitStatus() == RouteConfig.LIMIT_STATUS_CLOSE) {
// 单个限流功能未开启
if (configLimitDto.getLimitStatus() == RouteConfig.LIMIT_STATUS_CLOSE) {
return null;
}
byte limitType = routeConfig.getLimitType().byteValue();
byte limitType = configLimitDto.getLimitType().byteValue();
LimitManager limitManager = ApiConfig.getInstance().getLimitManager();
// 如果是漏桶策略
if (limitType == LimitType.LEAKY_BUCKET.getType()) {
boolean acquire = limitManager.acquire(routeConfig);
boolean acquire = limitManager.acquire(configLimitDto);
if (!acquire) {
throw new ApiException(new ErrorImpl(routeConfig.getLimitCode(), routeConfig.getLimitMsg()));
throw new ApiException(new ErrorImpl(configLimitDto.getLimitCode(), configLimitDto.getLimitMsg()));
}
} else if (limitType == LimitType.TOKEN_BUCKET.getType()) {
limitManager.acquireToken(routeConfig);
limitManager.acquireToken(configLimitDto);
}
return null;
}
protected ConfigLimitDto findConfigLimitDto(ApiConfig apiConfig, ApiParam apiParam, HttpServletRequest request) {
LimitConfigManager limitConfigManager = apiConfig.getLimitConfigManager();
String routeId = apiParam.fetchNameVersion();
String appKey = apiParam.fetchAppKey();
String ip = RequestUtil.getIP(request);
String[] limitKeys = new String[]{
routeId,
appKey,
routeId + appKey,
ip + routeId,
ip + appKey,
ip + routeId + appKey,
};
List<ConfigLimitDto> limitConfigList = new ArrayList<>();
for (String limitKey : limitKeys) {
ConfigLimitDto configLimitDto = limitConfigManager.get(limitKey);
limitConfigList.add(configLimitDto);
}
if (limitConfigList.isEmpty()) {
return null;
}
Collections.sort(limitConfigList, Comparator.comparing(ConfigLimitDto::getOrderIndex));
return limitConfigList.get(0);
}
}

@ -0,0 +1,67 @@
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;
/**
* 表名config_limit
* 备注限流配置
*
* @author tanghc
*/
@Table(name = "config_limit")
@Data
public class ConfigLimit {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
/** 数据库字段:id */
private Long id;
/** 路由id, 数据库字段:route_id */
private String routeId;
/** 数据库字段:app_key */
private String appKey;
/** 限流ip,多个用英文逗号隔开, 数据库字段:limit_ip */
private String limitIp;
/** 服务id, 数据库字段:service_id */
private String serviceId;
/** 限流策略,1:漏桶策略,2:令牌桶策略, 数据库字段:limit_type */
private Byte limitType;
/** 每秒可处理请求数, 数据库字段:exec_count_per_second */
private Integer execCountPerSecond;
/** 返回的错误码, 数据库字段:limit_code */
private String limitCode;
/** 返回的错误信息, 数据库字段:limit_msg */
private String limitMsg;
/** 令牌桶容量, 数据库字段:token_bucket_count */
private Integer tokenBucketCount;
/** 限流开启状态,1:开启,0关闭, 数据库字段:limit_status */
private Byte limitStatus;
/** 顺序,值小的优先执行, 数据库字段:order_index */
private Integer orderIndex;
/** 数据库字段:gmt_create */
private Date gmtCreate;
/** 数据库字段:gmt_modified */
private Date gmtModified;
}

@ -0,0 +1,67 @@
package com.gitee.sop.gateway.manager;
import com.alibaba.fastjson.JSON;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.sop.gateway.mapper.ConfigLimitMapper;
import com.gitee.sop.gatewaycommon.bean.ChannelMsg;
import com.gitee.sop.gatewaycommon.bean.ConfigLimitDto;
import com.gitee.sop.gatewaycommon.manager.DefaultLimitConfigManager;
import com.gitee.sop.gatewaycommon.manager.ZookeeperContext;
import com.gitee.sop.gatewaycommon.util.MyBeanUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import javax.annotation.PostConstruct;
/**
* 限流配置管理
* @author tanghc
*/
@Slf4j
public class DbLimitConfigManager extends DefaultLimitConfigManager {
@Autowired
ConfigLimitMapper configLimitMapper;
@Autowired
Environment environment;
@Override
public void load() {
Query query = new Query();
configLimitMapper.list(query)
.stream()
.forEach(configLimit -> putVal(configLimit));
}
protected void putVal(Object object) {
ConfigLimitDto configLimitDto = new ConfigLimitDto();
MyBeanUtil.copyPropertiesIgnoreNull(object, configLimitDto);
this.update(configLimitDto);
}
@PostConstruct
protected void after() throws Exception {
ZookeeperContext.setEnvironment(environment);
String path = ZookeeperContext.getLimitConfigChannelPath();
ZookeeperContext.listenPath(path, nodeCache -> {
String nodeData = new String(nodeCache.getCurrentData().getData());
ChannelMsg channelMsg = JSON.parseObject(nodeData, ChannelMsg.class);
final ConfigLimitDto configLimitDto = JSON.parseObject(channelMsg.getData(), ConfigLimitDto.class);
switch (channelMsg.getOperation()) {
case "reload":
log.info("重新加载限流配置信息,configLimitDto:{}", configLimitDto);
load();
break;
case "update":
log.info("更新限流配置信息,configLimitDto:{}", configLimitDto);
update(configLimitDto);
break;
}
});
}
}

@ -12,5 +12,6 @@ public class ManagerInitializer {
apiConfig.setIsvManager(new DbIsvManager());
apiConfig.setIsvRoutePermissionManager(new DbIsvRoutePermissionManager());
apiConfig.setRouteConfigManager(new DbRouteConfigManager());
apiConfig.setLimitConfigManager(new DbLimitConfigManager());
}
}

@ -0,0 +1,11 @@
package com.gitee.sop.gateway.mapper;
import com.gitee.fastmybatis.core.mapper.CrudMapper;
import com.gitee.sop.gateway.entity.ConfigLimit;
/**
* @author tanghc
*/
public interface ConfigLimitMapper extends CrudMapper<ConfigLimit, Long> {
}
Loading…
Cancel
Save