Gateway搭建记录

1. 简介

这里的Gateway指的是Spring Cloud Gateway。Gateway的基本作用是路由,将请求交给相应的服务进行处理,这里我们对gateway提出了更多的要求:认证、鉴权、熔断限流。

  • 请求到来之后,一般都需要判断该用户是否登录,如果没有登录则禁止访问,也就是认证(Authentication)功能;

  • 请求到来之后,判断该用户是否有权访问、操作资源,如果没有,则禁止访问,也就是鉴权(Authorization)功能;

  • 当流量过大后,能够进行熔断、限流措施,保护系统,spring Cloud Gateway通过整合Sentinel,提供了该功能;

在之前单体的Auth中,采用了Interceptor进行拦截完成了这2大功能,在微服务中,虽然也可以使用,但interceptor维护起来相对没有在统一的服务(gateway)中简单一些。

Spring Cloud Gateway采用WebFlux框架,本身是一个SpringBoot应用,它几个核心的概念:

  • Route

    网关配置的基本组成模块,一个 Route 由路由 ID,转发 URI(服务的路径),一组 Predicate 以一组 Filter 构成。如果Predicate为真,则路由匹配,则转发到该URI。

  • Predicate

    表示路由的匹配条件,可以用来匹配请求的各种属性,如请求路径、方法、header 等。一个 Route 可以包含多个子 Predicates,多个子 Predicates 最终会合并成一个。

  • Filter

    Filter与SpringBoot的Filter类似,包括了处理请求和响应的逻辑,可以分为 pre 和 post 两个阶段。多个 Filter 在 pre 阶段会按优先级高到低顺序执行,post 阶段则是反向执行。Gateway包括2类Filter:全局Filter与路由Filter。

过程如下:

spring-cloud-gateway-简介

这个过程与SpirngMVC的过程有些相似。

2. 基本用法

2.1 依赖

这里的配置相对多一些:

 <!-- 网关 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!-- nacos 服务发现 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- nacos 配置中心-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!-- sentinel -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- sentinel gateway 扩展-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
<!-- sentinel的nacos数据源-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

2.2 配置

如果是静态配置gateway,则仅配置application.yml

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: order-server
          uri: lb://order-server
          predicates:
            - Path=/order/**
          filters:
            - StripPrefix=1

这里使用的是动态配置,需要在bootstrap.yml中增加

nacos:
  config:
    gateway-route:
      data-id: gateway-route
      group: DEFAULT_GROUP
    gateway-flow-rule:
      data-id: gateway-flow-rule
      group: DEFAULT_GROUP

主要是routes部分,通过id、uri、predicates、filters配置了一条Route。

这里使用了Path断言,Path包含/order时,路由到lb://order-server上,而这里的order-server,是需要配置中心的,故而,这里gateway也需要注册进nacos。

后边的filters是将是将路由上的/path删除掉的意思,如访问gateway时是http://localhost:7878/order/test,而gateway转发后就是http://localhost:7878/test

以上是静态的路由方式,Nacos有配置中心的功能,能否通过Nacos来对路由进行配置,这样不同重启gateway就能完成对路由的修改,这也就是下边的动态路由。

详细的predicate参考官网

3. 动态路由

在前面的Nacos中,我们对Nacos动态配置做了简单的描述,这里以此来做个示例。过程如下:

  • 通过nacos创建动态路由配置项(json格式)
  • gateway服务中,监听该配置,并注册handler
  • 当Nacos中配置发生改变时,回调用该handler,将动态路由注册进gateway

参考:动态路由配置

3.1 Nacos配置项

创建gateway-route配置项,格式选择JSON:

[
     {
        "id": "auth-server",
        "uri": "lb://auth-server",
        "predicates": [
            {
                "name": "Path",
                "args": {
                     "pattern": "/auth/**"
                }
            }
        ],
        "filters": [
            {
                "name": "StripPrefix",
                "args":{
                    "parts": 1
                }
            }
        ]
    },
    {
        "id": "order-server",
        "uri": "lb://order-server",
        "predicates": [
            {
                "name": "Path",
                "args": {
                     "pattern": "/order/**"
                }
            }
        ],
        "filters": [
            {
                "name": "StripPrefix",
                "args":{
                    "parts": 1
                }
            }
        ]
    }
]

可以看出,yml的配置与json的配置在predicates(断言)与filters方面是有差异的。

注意:这里为对比依旧使用的"Path"断言,后边Auth中,修改为了“Host”断言。

3.2 监听动态配置

这里为了抽象了DynamicConfigManager来进行监听,抽象出DynamicConfigHandler统一各监听的处理。

@Slf4j
@Component
public class DynamicConfigManager {

    private ConfigService configService;

    @Value("${spring.cloud.nacos.discovery.server-addr}")
    private String serverAddr;

    @Value("${spring.cloud.nacos.discovery.namespace}")
    private String namespace;

    @Autowired
    private List<DynamicConfigHandler> dynamicConfigHandlerList;

    @PostConstruct
    public void init(){
        createConfigService();
        registerListener();
    }

    private boolean createConfigService() {
        try{
            Properties properties = new Properties();
            properties.setProperty("serverAddr", serverAddr);
            properties.setProperty("namespace", namespace);
            configService = NacosFactory.createConfigService(properties);
        } catch (Exception e) {
            log.error("初始化动态配置失败",e);
            return false;
        }
        return true;
    }

    private boolean registerListener(){
        if(configService == null){
            return false;
        }
        for (DynamicConfigHandler dynamicConfigHandler : dynamicConfigHandlerList) {
            NacosConfigKey configKey = dynamicConfigHandler.getConfigKey();
            try {
                String firstConfigInfo = configService.getConfigAndSignListener(configKey.getDataId(), configKey.getGroup(), 5000, new Listener() {
                    @Override
                    public Executor getExecutor() {
                        return null;
                    }

                    @Override
                    public void receiveConfigInfo(String configInfo) {
                        log.info("接收dataId:{} group:{}的配置:\r\n{}", configKey.getDataId(), configKey.getGroup(), configInfo);
                        dynamicConfigHandler.handle(configInfo);
                    }
                });
                dynamicConfigHandler.handle(firstConfigInfo);
            } catch (NacosException e) {
                e.printStackTrace();
            }
        }
        return true;
    }
}

3.3 动态配置Handler

public interface DynamicConfigHandler {
    /**
     * 获取配置在配置中心的key:dataId、group
     * @return
     */
    NacosConfigKey getConfigKey();

    /**
     * 处理推送的配置
     * @param configInfo 配置中心推送的配置,json
     * @return
     */
    boolean handle(String configInfo);
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class NacosConfigKey {
    private String dataId;
    private String group;
}

动态路由的配置如下:

@Slf4j
@Service
public class DynamicRouteConfigHandler implements ApplicationEventPublisherAware, DynamicConfigHandler {

    @Value("${nacos.config.gateway-route.data-id}")
    private String dataId;

    @Value("${nacos.config.gateway-route.group}")
    private String group;

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
    @Autowired
    private RouteDefinitionLocator routeDefinitionLocator;

    /**
     * 发布事件
     */
    @Autowired
    private ApplicationEventPublisher publisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }

    @Override
    public NacosConfigKey getConfigKey() {
        return new NacosConfigKey(dataId, group);
    }

    @Override
    public boolean handle(String configInfo) {
        List<RouteDefinition> definitionList = null;
        try {
            definitionList = JSONUtil.toList(configInfo, RouteDefinition.class);
        }catch (JSONException e){
            e.printStackTrace();
            return false;
        }
        return updateRouteList(definitionList);
    }

    /**
     * 更新路由列表
     * 先删除已有的,再新增新的
     * @param definitions
     * @return
     */
    private Boolean updateRouteList(List<RouteDefinition> definitions) {
        List<RouteDefinition> routeDefinitionsExits =  routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();
        deleteRouteList(routeDefinitionsExits);
        return addRouteList(definitions);
    }

    /**
     * 增加路由列表
     * @param definitionList
     * @return
     */
    private Boolean addRouteList(List<RouteDefinition> definitionList){
        if(definitionList==null || definitionList.isEmpty()){
            return false;
        }
        for(RouteDefinition definition : definitionList){
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        }
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return true;
    }

    /**
     * 删除路由列表
     * @param definitionList
     * @return
     */
    private Boolean deleteRouteList(List<RouteDefinition> definitionList){
        if(definitionList==null || definitionList.isEmpty()){
            return false;
        }
        for (RouteDefinition definition : definitionList) {
            this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
        }
        return true;
    }
}

4. 熔断限流:Sentinel

熔断限流是依靠Sentinel提供的功能,下边先来看看Sentinel,然后再把它整合进gateway

4.1 Sentinel简介

参考:官网工作流程

  • 概念

    资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。使用 Sentinel 来进行资源保护,主要分为几个步骤:

    1. 定义资源
    2. 定义规则
    3. 检验规则是否生效

    先把可能需要保护的资源定义好(埋点),之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。

    在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:

    • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
    • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
    • StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
    • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
    • AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
    • DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
    • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

    总体框架如下:

    sentinel框架图

4.2 Sentinel基本使用

参考官网示例,基本用法需要先定义资源、在定义规则,最后确定一下是否生效即可。

  • 定义资源

    资源有多种定义方式,这里只写注解方式:

    @Service
    public class QueryService {
        private static final String KEY="query";
    
        @SentinelResource(value = KEY,blockHandler ="blockHandlerMethod",
                fallback = "fallbackMethod")
        public String query(String name) {
            if(name.equals("3")){
                throw new RuntimeException("3 error");
            }
            return "begin query method, name= " + name;
        }
    
        public String blockHandlerMethod(String name, BlockException e){
            e.printStackTrace();
            return "blockHandlerMethod for Query : " + name;
        }
    
        public String fallbackMethod(String name, Throwable e){
            e.printStackTrace();
            return "fallbackMethod for Query : " + name;
        }
    }
    

    通过 @SentinelResource 注解定义资源并配置 blockHandlerfallback 函数来进行限流之后的处理。blockHandler 函数会在原方法被限流/降级/系统保护的时候调用,而 fallback 函数会针对所有类型的异常。

  • 定义规则

    Sentinel中,可以定制5种规则:

    • 流量控制规则 FlowRule
    • 熔断降级规则 DegradeRule
    • 访问控制规则 AuthorityRule
    • 系统保护规则 SystemRule
    • 热点规则 ParamFlowRule
    @Component
    public class SentinelConfig {
        private static final String KEY="query";
        @PostConstruct
        private void init(){
            initDegradeRule();
            initFlowQpsRule();
            initSystemRule();
            initAuthorityRule();
            initParamFlowRule();
        }
    
        //熔断降级规则
        private void initDegradeRule(){
            List<DegradeRule> rules=new ArrayList<>();
            DegradeRule rule=new DegradeRule();
            rule.setResource(KEY);
            // 80s内调用接口出现 异常 ,次数超过5的时候, 进行熔断
            rule.setCount(5);
            rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
            rule.setTimeWindow(80);
            rules.add(rule);
            DegradeRuleManager.loadRules(rules);
        }
    
        //流量控制规则
        private void initFlowQpsRule() {
            List<FlowRule> rules = new ArrayList<>();
            FlowRule rule = new FlowRule(KEY);
            rule.setCount(20);
            rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
            rule.setLimitApp("default");
            rules.add(rule);
            FlowRuleManager.loadRules(rules);
        }
    
        //系统保护规则
        private void initSystemRule() {
            List<SystemRule> rules = new ArrayList<>();
            SystemRule rule = new SystemRule();
            rule.setHighestSystemLoad(10);
            rules.add(rule);
            SystemRuleManager.loadRules(rules);
        }
    
        //黑白名单控制
        private void initAuthorityRule(){
            List<AuthorityRule> rules=new ArrayList<>();
    
            AuthorityRule rule = new AuthorityRule();
            rule.setResource(KEY);
            rule.setStrategy(RuleConstant.AUTHORITY_BLACK);
            rule.setLimitApp("nacos-consumer");
            rules.add(rule);
            AuthorityRuleManager.loadRules(rules);
        }
    
        //热点参数规则
        private void initParamFlowRule(){
            ParamFlowRule rule = new ParamFlowRule(KEY)
                    .setParamIdx(0)
                    .setCount(20);    
            ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf("4"))
                    .setClassType(String.class.getName())
                    .setCount(2);
            rule.setParamFlowItemList(Collections.singletonList(item));
            ParamFlowRuleManager.loadRules(Collections.singletonList(rule));
        }
    }
    

4.3 网关动态限流

参考:Sentinel 整合 Gateway + Nacos

参考:网关限流

从依赖上看,网关限流,需要sentinel-spring-cloud-gateway-adapte,动态限流需要sentinel-datasource-nacos

将gateway限流的规则配置到nacos中,在nacos中修改配置后,限流规则自动改变。

Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。

Dashbord以界面的方式来配置规则,这些规则配置并没有持久化,生产环境中需要配合配置中心来使用:

Sentinel生产环境

一般情况下如此,但我们的Nacos本身就有Dashboard,这里也就不需要Sentinel Dashborad进行配置了,我们直接从Nacos的Dashborad进行配置

  • nacos中动态配置

    创建gateway-flow-rule配置项:

    [
        {
            "resource": "order-server",
            "resourceMode": 0,
            "count": 100,
            "intervalSec": 5
        },
         {
            "resource": "order-api",
            "resourceMode": 1,
            "pattern": "/order/oms/order/create",
            "count": 1,
            "intervalSec": 3
        }
    ]
    

    对gateway的限流有2种,一种是对网关的route进行限流(resourceMode=0),另外一种是对自定义的route进行限流(resourceMode=1),如下:

    • resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
    • resourceMode:规则是针对 API Gateway 的 route(RESOURCE_MODE_ROUTE_ID)还是用户在 Sentinel 中定义的 API 分组(RESOURCE_MODE_CUSTOM_API_NAME),默认是 route。
    • pattern: 自定义API所对应的path
    • count:限流阈值
    • intervalSec:统计时间窗口,单位是秒,默认是 1 秒。
  • gateway的bootstrap.yml配置

    nacos:
      config:
        gateway-route:
          data-id: gateway-route
          group: DEFAULT_GROUP
        gateway-flow-rule:
          data-id: gateway-flow-rule
          group: DEFAULT_GROUP
    
  • 动态配置Handler

    这一部分与动态路由的Handler处理一致:

    @Component
    public class DynamicFlowRuleConfigHandler implements DynamicConfigHandler {
    
        @Value("${nacos.config.gateway-flow-rule.data-id}")
        private String dataId;
    
        @Value("${nacos.config.gateway-flow-rule.group}")
        private String group;
    
        @Override
        public NacosConfigKey getConfigKey() {
            return new NacosConfigKey(dataId, group);
        }
    
        @Override
        public boolean handle(String configInfo) {
            List<GatewayFlowRule> gatewayFlowRules = null;
            try {
                gatewayFlowRules = JSONUtil.toList(configInfo, GatewayFlowRule.class);
            } catch (JSONException e){
                e.printStackTrace();
                return false;
            }
            if(gatewayFlowRules.isEmpty()){
                addDefaultGatewaySentinelRule();
            } else {
                addGatewaySentinelRuleList(gatewayFlowRules);
            }
            return true;
        }
    
        public void addGatewaySentinelRuleList(List<GatewayFlowRule> gatewayFlowRules) {
            Set<com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule> rules = new HashSet<>();
            Set<ApiDefinition> definitions = new HashSet<>();
            for (GatewayFlowRule sentinelConfig : gatewayFlowRules) {
                com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule flowRule = new com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule(sentinelConfig.getResource())
                                            .setResourceMode(sentinelConfig.getResourceMode())
                                            .setCount(sentinelConfig.getCount())  // 限流阈值
                                            .setIntervalSec(sentinelConfig.getIntervalSec());
                rules.add(flowRule);
                if (sentinelConfig.getResourceMode() == 1) {  //自定义API模式
                    ApiDefinition apiDefinition = new ApiDefinition(sentinelConfig.getResource())
                                                    .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                                                        ApiPathPredicateItem apiPathPredicateItem = new ApiPathPredicateItem().setPattern(sentinelConfig.getPattern());
                                                        if(sentinelConfig.getPattern().contains("**")){
                                                            apiPathPredicateItem.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX);
                                                        }
                                                        add(apiPathPredicateItem);
                                                    }});
                    definitions.add(apiDefinition);
                }
            }
            GatewayApiDefinitionManager.loadApiDefinitions(definitions);
            GatewayRuleManager.loadRules(rules);
        }
    
        //配置删除,初始化一个默认规则
        public void addDefaultGatewaySentinelRule() {
            Set<com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule> rules = new HashSet<>();
            rules.add(new com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule("no-rule")
                    .setResourceMode(0)
                    .setCount(10000000)  // 限流阈值
                    .setIntervalSec(1));  // 统计时间窗口,单位是秒,默认是 1 秒
            GatewayRuleManager.loadRules(rules);
        }
    }
    

5. 认证、鉴权

认证与鉴权需要配合鉴权中心来完成,鉴权中心放到后边章节来整理

5.1 认证

认证过程较为简单,验证request的cookie中是否携带token即可。这里采用Gateway GlobalFilter的方式,直接从redis缓存中判断是否有该token,并将userId取出,添加到request的header中,这样在后续服务中直接使用该userId即可。

具体实现如下:

@Slf4j
@Component
public class AuthenticationFilter implements GlobalFilter, Ordered {

    @Autowired
    private GatewayConfig gatewayConfig;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public int getOrder() {
        return 3;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        // 判断路由是不需要认证
        String domainPath = request.getURI().getAuthority()+request.getURI().getPath();
        if(gatewayConfig.getIgnoreList().contains(domainPath)){
            return chain.filter(exchange);
        }

        // 获取token
        HttpCookie cookie = exchange.getRequest().getCookies().getFirst("token");
        String token = cookie==null ? "" : cookie.getValue();
        // 从缓存缓存中获取token
        Long userId = StrUtil.isBlank(token) ? null : TokenUtil.getToken(redisTemplate, token);
        if(userId == null){
            // 重定向到login
            String uri = request.getURI().getAuthority();
            Map<String, String> clientMap = gatewayConfig.getClientMap();
            String clientId = clientMap.get(uri);
            if(clientId == null){
                MyResponseBody body = MyResponseBody.fail(MyResponseCode.BAD_REQUEST, "域名未注册");
                return respond(response, body);
            }
            String loginURL = gatewayConfig.getLoginURL() + "?clientId="+clientId;
            return redirect(response, loginURL);
        } else {
            // 设置header,携带userId
            try{
                ServerHttpRequest newHttpRequest = RequestUtil.createHttpRequest(request,
                        RequestUtil.createHttpHeadersConsumer(USER_HEADER, userId.toString()));
                return chain.filter(exchange.mutate().request(newHttpRequest).build());
            } catch (Exception e){
                e.printStackTrace();
            }
        }
        return chain.filter(exchange);
    }

    private Mono<Void> respond(ServerHttpResponse response, MyResponseBody body) {
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.setStatusCode(HttpStatus.OK);
        JSONObject jsonObject = JSONUtil.parseObj(body);
        byte[] bits = jsonObject.toString().getBytes();
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        return response.writeWith(Mono.just(buffer));
    }

    private Mono<Void> redirect(ServerHttpResponse response, String location){
        response.setStatusCode(HttpStatus.SEE_OTHER);
        response.getHeaders().set("Location", location);
        return response.setComplete();
    }
}

5.2 鉴权

鉴权的逻辑是,判断用户(user)在该组织(org)下,是否有访问该应用(app)的资源(path)的权限(permission)。

共有5个元素,先通过f(orgId, app, path)求出访问这个path所需要的permissin,然后通过f(user, org, app)求出用户所拥有的permission,这两个permission进行对比即可。

前者需要建立path与permission的关联,后者要建立user-role-permission的关联。后者的关联是常见的,前者的却有不同实现方式。

  1. 通过注解将关联直接写到业务服务的controller上,这样需要在业务服务中进行判断;
  2. 将path与permission的关系存储到数据库中,这样可以在gateway中进行读取;

第1中方式运维上相对简单,但管理上不太严格,与之前单体的方案相同,再者对于不同产品线,鉴权的逻辑可能有所不同,这种灵活一点;

第2中方案运维上相对复杂,写完代码需要将路由入库,管理更规范一些;

考虑到目前的编码习惯,这里选择的第1种方式。

实现上将这个放到了common中,并增加了app.authorization配置项,某些不需要鉴权的服务,可以配置为false。

@Component
@ConditionalOnProperty(prefix = "app", name = "authorization", havingValue = "true")
public class AuthorizationInterceptor implements HandlerInterceptor  {

    @Autowired
    UserPermissionService userPermissionService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        if(handler instanceof HandlerMethod){
            // 路由是否有权限要求
            HandlerMethod h = (HandlerMethod)handler;
            OrgPermission authPermission = h.getMethodAnnotation(OrgPermission.class);
            if(authPermission == null){
                return true;
            }
            String[] needPermissionList = authPermission.value();

            // 从header中获取useId
            Long userId = UserUtil.getUserId();
            if(userId == 0){
                throw new AuthenticatedException();
            }

            // 从path中获取orgId
            Map<String, String> pathVarsMap = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
            if(pathVarsMap == null){
                return true;
            }
            String param = pathVarsMap.get("orgId");
            if(param == null){
                return true;
            }
            Long orgId = Long.valueOf(param);
            if(orgId == 0L){
                return true;
            }

            // 从auth获取user在该org中的具有的permission
            List<String> permissionList = userPermissionService.getUserPermissionList(orgId, userId);
            Set<String> hasPermList =  new HashSet<>(permissionList);

            // 判断用户是否具有路由所要求的权限
            for (String perm: needPermissionList) {
                if(hasPermList.contains(perm)){
                    return true;
                }
            }
            throw new ForbiddenException();
        }
        return true;
    }
}

6. 结语

这样gateway就整理结束了,包含着动态路由、熔断限流、认证鉴权等3大部分的功能。

这一段可替代的方案是nginx,nginx本身就有反向代理(动态路由)、负载均衡的功能,认证、熔断限流都可可以通过模块来增加。

与nginx相比,gateway更适合的nacos体系,它可以注入到nacos中,通过服务名来访问;nginx更适合K8S的场景,知道service即可,不必关注Pod信息。

# 微服务 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×