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











 
    
评论