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
@Componentpublic 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
版权声明: 本文为 InfoQ 作者【柠檬】的原创文章。
原文链接:【http://xie.infoq.cn/article/a9cea92738efc530cb95cb70f】。
本文遵守【CC BY-NC】协议,转载请保留原文出处及本版权声明。
柠檬
人生尚未成功,朋友仍需努力 2020.05.21 加入
长期从事微服务,中台等后台开发和架构设计。一些见解和实现可查看https://gitee.com/lvmoney/zhy-frame-parent
评论