写点什么

k8s 上运行我们的 springboot 服务之——cloud gateway

用户头像
柠檬
关注
发布于: 2020 年 05 月 23 日
k8s上运行我们的springboot服务之——cloud gateway

定义:

在微服务架构里,服务的粒度被进一步细分,各个业务服务可以被独立的设计、开发、测试、部署和管理。这时,各个独立部署单元可以用不同的开发测试团队维护,可以使用不同的编程语言和技术平台进行设计,这就要求必须使用一种语言和平 台无关的服务协议作为各个单元间的通讯方式

网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。



API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施



在系统划分中,我们一般会划分成:

1)业务系统:用户使用(pc web,app,小程序等),op(业务后台管理系统)

2)中台系统:对外服务(主要是提供给业务系统使用,中台之间也会很少调用最好不要调用),op(中台运维管理系统)



在中台划分中,我们一般又会分成:

1)用户基本信息中台

2)系统权限中台

3)用户权限中台

4)会员中台

5)订单中台

6)支付中台

7)商品中台

8)数据同步中台

等等比较多,这里就要看服务边界和服务划分粒度了。这是个校验活,也是一个技术活。



在开发中,我们一般会怎么做:

1)使用微服务的技术思想

2)一个人或者一个小团队去开发一个中台服务或者业务应用,关起门来按照需求把自己的东西做好独立部署成一个系统

3)开发时本地自测

4)测试环境,测试人员测试

等等,还有很多,这不是我们的重点这里不详述



问题:

1)我们独立运行的服务越来越多,服务调用就越来越麻烦了

2)我们需要满足本地开发调试的需要

3)我们还要满足通过nacos调用的需要

4)我们也要满足在k8s中,服务调用的需要



gateway能做什么:

1)服务通过gateway去请求各个服务,我们在调用方不需要去配置其他各个服务,只需要配置gateway的服务就行了

2)gateway能把一些服务校验,服务攻击预防,服务是否能被调用等放到这里处理了,不把请求直接发到我们真正的服务上

3)根据不同的环境(local,nacos,k8s)去构造不同的请求路径转发我们的请求方便我们使用

4)限流,错误处理

等等



我的gateway做了说明:

1)所有的远程服务可以通过yml配置,也可以通过redis获得(有了op后就可以通过页面动态维护了)

2)根据不同环境的需求(在yml中简单的配置),从redis获得服务信息去构造不同的请求地址,服务调用时方便转发

3)判断某个服务是否允许被调用

4)内网白名单校验

5)业务系统是否被授权

6)用户访问权限校验,这里我们使用统一的大用户中台,用户权限调用用户权限中台来完成

7)支持https

8)是否是简单的路由,gateway不只能满足我们系统的路由,也可以用来我们路由百度,google等不适我们开发的系统

等等

可以看看封面的流程图



gateway 的核心GatewayFilter

@Component
public class GatewayFilter implements GlobalFilter, Ordered {
@Value("${frame.gateway.support:false}")
String gatewaySupport;
@Autowired
Gateway2RedisService gateway2RedisService;
@Autowired
WhiteListService whiteListService;
@Value("${frame.white.support:false}")
String whiteSupport;

@Value("${frame.allowable.support:false}")
boolean allowableSupport;

/**
* 简单路由
*/
@Value("${frame.simpleRoute.support:false}")
boolean simpleRouteSupport;
@Value("${frame.jwt.support:false}")
boolean jwtSupport;

@Value("${frame.shiro.support:false}")
boolean shiroSupport;


@Value("${frame.releaseServer.support:false}")
boolean releaseServerSupport;

private static final String LOCALHOST_NAME = "localhost";
/**
* 开发local本地环境以http或者https开头,http包含了https
*/
private static final String INTERNAL_SERVICE_PREFIX = "http://";

/**
* istio环境以www开头
*/
private static final String EXTERNAL_SERVICE_PREFIX = "www.";
/**
* nacos 环境以lb开头
*/
private static final String NACOS_SERVICE_PREFIX = "lb://";
/**
* 去掉https请求路径的后缀,主要是istio环境下
*/
private static final String HTTPS_SUFFIX_PORT = ":443";
@Autowired
ServerService serverService;
@Autowired
AuthorizedService authorizedService;
@Autowired
UserCommonService userCommonService;
@Autowired
IAuthorityCheckClient iAuthorityCheckClient;

/**
* @describe:1、直接能够访问的接口都是一些常用、不重要、不需要校验的接口 考虑到很多情况都是遭遇到非信任系统的调用攻击,所以需要校验发起调用的服务是否已被授权调用
* 2、同一个网络环境的内部服务间调用通过白名单校验,白名单校验通过,就可以直接访问
* 外网用户的访问先到这里经过层层考验再转发到对应的服务
* 3、基于多种情况的考虑,如果是jwt校验不需要shiro校验,需调用jwt校验服务。
* 如果是需要shiro校验,那么必须开启jwt校验,需要调用一个服务即校验jwt也要校验shiro,
* <p>
* 1、是否需要gateway支持====>2、白名单校验=====>3、sysId是否被授权====>4、是否被允许调用======>5、权限校验=======>6、服务访问
* @param: [exchange, chain]
* @return: reactor.core.publisher.Mono<java.lang.Void>
* @author: lvmoney /XXXXXX科技有限公司
* 2019/8/20 11:40
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse serverHttpResponse = exchange.getResponse();
//1、gateway start
if (!SupportUtil.support(gatewaySupport)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.GATEWAY_SUPPORT_ERROR))));
} else if (gatewaySupport.equals(BaseConstant.SUPPORT_FALSE)) {
return chain.filter(exchange);
}
//2、是否是简单的路由 start
Route route = exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
ServerInfo serverInfo = getServerInfo(route);
if (!SupportUtil.support(simpleRouteSupport)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.SIMPLE_ROUTE_SUPPORT_ERROR))));
} else if (isSimpleRoute(simpleRouteSupport, serverInfo)) {
// 在这里做判断
return chain.filter(exchange);
}
//3、白名单 start
if (!SupportUtil.support(whiteSupport)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.WHITE_SUPPORT_ERROR))));
} else if (!isWhite(exchange, serverInfo)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.GATEWAY_WHITE_CHECK_ERROR))));
}
//4、是否被允许调用 start
String realPath = realPath(exchange);
if (!isRelease(realPath, serverInfo)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.GATEWAY_INTERNAL_CHECK_ERROR))));
}
String token = exchange.getRequest().getHeaders().getFirst(BaseConstant.AUTHORIZATION_TOKEN_KEY);
UserVo userVo = null;
try {
//由于gateway和普通的servlet请求不同,通过throw的方式抛错不得行,所以需做如下处理
userVo = userCommonService.getUserVo(token);
} catch (Exception e) {
userVo = null;
}
//5、系统id是否被允许访问 start
if (ObjectUtils.isEmpty(userVo)) {
token = "";
}
if (!SupportUtil.support(allowableSupport)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.ALLOWABLE_SUPPORT_ERROR))));
} else if (BaseConstant.SUPPORT_TRUE_BOOL == allowableSupport) {
boolean result = isAllowable(token, serverInfo, realPath);
if (!result) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.ALLOWABLE_SYS_ID_NOT_EXIST))));
}
}
//6、jwt校验 start
if (!SupportUtil.support(jwtSupport)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.JWT_SUPPORT_ERROR))));
} else if (!isJwt(realPath, token, serverInfo)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.TOKEN_CHECK_ERROR))));
}
//7、shiro校验 start
if (!SupportUtil.support(shiroSupport)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.SHIRO_SUPPORT_ERROR))));
} else if (!isShiro(realPath, token, serverInfo)) {
serverHttpResponse.setStatusCode(HttpStatus.CONFLICT);
return serverHttpResponse.writeWith(Flux.just(ExceptionUtil.filterExceptionHandle(serverHttpResponse, new BusinessException(GatewayException.Proxy.SHIRO_CHECK_ERROR))));
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return -9999;
}

/**
* @describe:通过路由规则获得真正的请求的地址
* @param: [exchange]
* @return: java.lang.String
* @author: lvmoney /XXXXXX科技有限公司
* 2019/8/15 11:49
*/
private String realPath(ServerWebExchange exchange) {

Route route = exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
String routeId = route.getId();
RouteDefinition routeDefinition = gateway2RedisService.getRouteDefinition(routeId);
String path = exchange.getRequest().getPath().toString();
String newPath = "";
final String[] skip = {""};
routeDefinition.getFilters().stream()
.filter(
e -> e.getName().equals(GatewayConstant.ROUTE_DEFINITION_FILTER)).limit(1).forEach(ex -> ex.getArgs().entrySet().forEach(map -> skip[0] = map.getValue()));
Long step = Long.parseLong(skip[0]);
newPath = "/" + (String) Arrays.stream(StringUtils.tokenizeToStringArray(path, "/")).skip(step).collect(Collectors.joining("/"));
newPath = newPath + (newPath.length() > 1 && path.endsWith("/") ? "/" : "");
return newPath;
}

/**
* @describe: 1、判断请求ip是否在白名单,该校验用于内网环境服务可信任调度,白名单内的ip可访问
* 2、如果是外部可访问的服务,那么该服务只能被外网访问,也就是白名单外的ip访问
* 3、根据ServerInfo中的internalService进行判断
* 4、需要白名单支持,但是服务名称不在redis中,说明该服务不需要白名单校验
* @param: [exchange]
* @return: boolean
* @author: lvmoney /XXXXXX科技有限公司
* 2019/8/20 10:20
*/
private boolean isWhite(ServerWebExchange exchange, ServerInfo serverInfo) {
String serverName = serverInfo.getServerName();
if (whiteSupport.equals(BaseConstant.SUPPORT_FALSE)) {
//不需要白名单支持
return true;
} else if (BaseConstant.SUPPORT_TRUE.equals(gatewaySupport)
&& !whiteListService.isExist(serverName)) {
//需要白名单支持,但是服务名称不在redis中,说明该服务不需要白名单校验
return true;
}
String internalService = serverInfo.getInternalService();
if (InternalService.external.getValue().equals(internalService)) {
//判断请求ip是否在白名单,该校验用于内网环境服务可信任调度,白名单内的ip可访问
ServerHttpRequest request = exchange.getRequest();
String ip = request.getURI().getHost();
if (LOCALHOST_NAME.equals(ip) || ip.equals(BaseConstant.LOCALHOST_IP)) {
ip = IpUtil.getLocalhostIp();
}
WhiteListVo whiteListVo = whiteListService.getWhiteList(serverName);
boolean result = false;
Set<String> white = whiteListVo.getNetworkSegment();
for (
String e : white) {
if (IpUtil.isInRange(ip, e)) {
result = true;
break;
}

}
return result;
} else if (InternalService.internal.getValue().equals(internalService)) {
//如果是外部可访问的服务,那么该服务只能被外网访问,也就是白名单外的ip访问
ServerHttpRequest request = exchange.getRequest();
String ip = request.getURI().getHost();
if (LOCALHOST_NAME.equals(ip) || ip.equals(BaseConstant.LOCALHOST_IP)) {
ip = IpUtil.getLocalhostIp();
}
WhiteListVo whiteListVo = whiteListService.getWhiteList(serverName);
boolean result = true;
Set<String> white = whiteListVo.getNetworkSegment();
for (String e : white) {
if (IpUtil.isInRange(ip, e)) {
result = false;
break;
}
}
return result;
} else {
return false;
}


}

/**
* 通过白名单校验后,判断访问的接口是否被允许其他服务调用
* 1、首先判断是否支持该校验
* 2、根据ServerInfo的internalService去判断是否是内网服务
* 3、判断是否被允许访问
*
* @param realPath:
* @param serverInfo:
* @throws
* @return: boolean
* @author: lvmoney /XXXXXX科技有限公司
* @date: 2019/9/17 17:05
*/
private boolean isRelease(String realPath, ServerInfo serverInfo) {
if (!releaseServerSupport) {
//如果不需要支持直接返回true
return true;
} else {
String internalService = serverInfo.getInternalService();
if (InternalService.external.getValue().equals(internalService)) {
//如果是外部c端可访问的全部放开
return true;
} else if (InternalService.internal.getValue().equals(internalService)) {
//处理内部服务的相互调用
Set<String> releaseServer = serverInfo.getReleaseServer();
if (releaseServer.contains(realPath)) {
//如果请求的url在被公布的里面返回true
return true;
}
return false;
} else {
return false;
}
}
}

/**
* 根据请求的地址获得服务信息
*
* @param route:
* @throws
* @return: com.zhy.common.vo.ServerInfo
* @author: lvmoney /XXXXXX科技有限公司
* @date: 2019/9/18 14:56
*/
private ServerInfo getServerInfo(Route route) {
String routeUrl = route.getUri().toString();

if (routeUrl.contains(LOCALHOST_NAME)) {
String ip = IpUtil.getLocalhostIp();
routeUrl = routeUrl.replace(LOCALHOST_NAME, ip);
}
if (routeUrl.contains(HTTPS_SUFFIX_PORT)) {
routeUrl = routeUrl.replaceAll(HTTPS_SUFFIX_PORT, "");
}
ServerInfo serverInfo = new ServerInfo();
if (routeUrl.startsWith(INTERNAL_SERVICE_PREFIX)) {
//本地开发测试环境
serverInfo = serverService.getServerInfo(routeUrl);

} else if (routeUrl.startsWith(EXTERNAL_SERVICE_PREFIX)) {
//istio 环境
serverInfo = serverService.getServerInfo(route.getUri());
} else if (routeUrl.startsWith(NACOS_SERVICE_PREFIX)) {
//nacos 环境
serverInfo = serverService.getServerInfo(routeUrl);

}

return serverInfo;
}

/**
* 1、从redis获得请求的路径是否不需要校验
* 2、当前用户token校验,调用远程服务
*
* @param path:
* @param token:
* @param serverInfo:
* @throws
* @return: boolean
* @author: lvmoney /XXXXXX科技有限公司
* @date: 2020/3/8 16:08
*/
private boolean isJwt(String path, String token, ServerInfo serverInfo) {
if (!jwtSupport) {
//如果不需要支持直接返回true
return true;
} else {
if (serverInfo.getNotToken().contains(path)) {
return true;
}
if (StringUtils.isEmpty(token)) {
return false;
}
ApiResult<JwtCheckVo> apiResult = iAuthorityCheckClient.checkToken(token);

if (apiResult.getData().isResult()) {
return true;
}
}
return false;
}

/**
* 1、从redis获得请求的路径是否不需要校验
* 2、判断当前用户是否被允许调用请求的url,调用远程服务
*
* @param path:
* @param token:
* @param serverInfo:
* @throws
* @return: boolean
* @author: lvmoney /XXXXXX科技有限公司
* @date: 2020/3/8 16:09
*/
private boolean isShiro(String path, String token, ServerInfo serverInfo) {
if (!shiroSupport) {
//如果不需要支持直接返回true
return true;
} else {
if (serverInfo.getNotAuthority().contains(path)) {
return true;
}
if (StringUtils.isEmpty(token)) {
return false;
}
ShiroCheckAo shiroCheckAo = new ShiroCheckAo(path, token);
ApiResult<ShiroCheckVo> apiResult = iAuthorityCheckClient.checkAuthority(shiroCheckAo);
if (apiResult.getData().isResult()) {
return true;
}
}
return false;
}

/**
* 从redis中校验是否被sysId能访问
* 如果是不需要权限校验的url,直接返回校验通过。
*
* @param token:
* @param serverInfo:
* @throws
* @return: boolean
* @author: lvmoney /XXXXXX科技有限公司
* @date: 2020/3/8 16:10
*/
private boolean isAllowable(String token, ServerInfo serverInfo, String path) {
//如果用户没有登录且在在不需要鉴权的数据中
if (serverInfo.getNotAuthority().contains(path) || serverInfo.getNotToken().contains(path)) {
return true;
}
String serverName = serverInfo.getServerName();
AuthorizedVo authorizedVo = authorizedService.getSysIdByServer(serverName);
if (StringUtils.isEmpty(token)) {
return false;
}
UserVo userVo = JwtUtil.getUserVo(token);
return authorizedVo.getSysId().contains(userVo.getSysId());
}

/**
* 判断某个路由是否是简单的route,没有Serverinfo 的route被认为是简单的route
* 如果serverinfo 的simpleRoute=false也被认为是简单的route
* 如果是直接路由,如果不是就行后续的各种校验
*
* @param simpleRouteSupport:
* @param serverInfo:
* @throws
* @return: boolean
* @author: lvmoney /XXXXXX科技有限公司
* @date: 2020/3/14 21:24
*/
private boolean isSimpleRoute(boolean simpleRouteSupport, ServerInfo serverInfo) {
if (simpleRouteSupport) {
if (ObjectUtils.isEmpty(serverInfo)) {
return true;
} else if (serverInfo.isSimpleRoute()) {
return true;
} else {
return false;
}
} else {
if (ObjectUtils.isEmpty(serverInfo)) {
return true;
}
}
return false;
}


}

我觉得配合 k8s 的etcd、DestinationRule、VirtualService、Gateway使用更好一些



具体的落地实现详见:https://gitee.com/lvmoney/zhy-frame-parent



发布于: 2020 年 05 月 23 日阅读数: 64
用户头像

柠檬

关注

人生尚未成功,朋友仍需努力 2020.05.21 加入

长期从事微服务,中台等后台开发和架构设计。一些见解和实现可查看https://gitee.com/lvmoney/zhy-frame-parent

评论

发布
暂无评论
k8s上运行我们的springboot服务之——cloud gateway