快速搭建一個網(wǎng)關服務,動態(tài)路由、鑒權的流程

插: 前些天發(fā)現(xiàn)了一個巨牛的人工智能學習網(wǎng)站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉(zhuǎn)到網(wǎng)站。
堅持不懈,越努力越幸運,大家一起學習鴨~~~

前言

本文記錄一下我是如何使用Gateway搭建網(wǎng)關服務及實現(xiàn)動態(tài)路由的,幫助大家學習如何快速搭建一個網(wǎng)關服務,了解路由相關配置,鑒權的流程及業(yè)務處理,有興趣的一定看到最后,非常適合沒接觸過網(wǎng)關服務的同學當作入門教程。

搭建服務

框架

SpringBoot 2.1

<parent>   
<groupId>org.springframework.boot</groupId>  
 <artifactId>spring-boot-starter-parent</artifactId>  
 <version>2.1.0.RELEASE</version>
</parent>

Spring-cloud-gateway-core

<dependency>  
 <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-gateway-core</artifactId>
</dependency>

common-lang3

<dependency>   
<groupId>org.apache.commons</groupId>  
 <artifactId>commons-lang3</artifactId>
</dependency>
路由配置

網(wǎng)關作為請求統(tǒng)一入口,路由就相當于是每個業(yè)務系統(tǒng)的入口,通過路由規(guī)則可以匹配到對應微服務的入口,將請求命中到對應的業(yè)務系統(tǒng)中

server:  
  port: 8080
spring: 
   cloud:    
      gateway:     
         enabled: true      
          routes:    
              - id: demo-server
              uri: http://localhost:8081 
              predicates:
                  - Path=/demo-server/**
             filters:
                - StripPrefix= 1

routes

image.png
解讀配置
  • 現(xiàn)在有一個服務demo-server部署在本機,地址和端口為127.0.0.1:8081,所以路由配置uri為http://localhost:8081
  • 使用網(wǎng)關服務路由到此服務,predicates -Path=/demo-server/**,網(wǎng)關服務的端口為8080,啟動網(wǎng)關服務,訪問localhost:8080/demo-server,路由斷言就會將請求路由到demo-server
  • 直接訪問demo-server的接口localhost:8081/api/test,通過網(wǎng)關的訪問地址則為localhost:8080/demo-server/api/test,predicates配置將請求斷言到此路由,filters-StripPrefix=1代表將地址中/后的第一個截取,所以demo-server就截取掉了

使用gateway通過配置文件即可完成路由的配置,非常方便,我們只要充分的了解配置項的含義及規(guī)則就可以了;但是這些配置如果要修改則需要重啟服務,重啟網(wǎng)關服務會導致整個系統(tǒng)不可用,這一點是無法接受的,下面介紹如何通過Nacos實現(xiàn)動態(tài)路由

動態(tài)路由

使用nacos結(jié)合gateway-server實現(xiàn)動態(tài)路由,我們需要先部署一個nacos服務,可以使用docker部署或下載源碼在本地啟動,具體操作可以參考官方文檔即可

Nacos配置
image.png

groupId: 使用網(wǎng)關服務名稱即可

dataId: routes

配置格式:json


[{      
    "id":   "xxx-server",
          "order":  1,  #優(yōu)先級     
       "predicates":  [{     #路由斷言 
        "args":  {              
            "pattern":   "/xxx-server/**"          
        },
                  "name":   "Path"      
    }],
          "filters": [{       #過濾規(guī)則
        "args":  {              
            "parts":  0         #k8s服務內(nèi)部訪問容器為http://xxx-server/xxx-server的話,配置0即可
        },
                  "name":   "StripPrefix"      #截取的開始索引   
    }],
          "uri":   "http://localhost:8080/xxx-server"  #目標地址
}]

json格式配置項與yaml中對應,需要了解配置在json中的寫法

比對一下json配置與yaml配置
{
    "id":"demo-server",
    "predicates":[
        {
            "args":{
                "pattern":"/demo-server/**"
            },
            "name":"Path"
        }    ],
    "filters":[ 
       {
            "args":{ 
               "parts":1
            }, 
           "name":"StripPrefix"
        }    ], 
   "uri":"http://localhost:8081"}
server:  
  port: 8080
spring: 
   cloud:    
      gateway:     
         enabled: true      
          routes:    
              - id: demo-server
              uri: http://localhost:8081 
              predicates:
                  - Path=/demo-server/**
             filters:
                - StripPrefix= 1
代碼實現(xiàn)

Nacos實現(xiàn)動態(tài)路由的方式核心就是通過Nacos配置監(jiān)聽,配置發(fā)生改變后執(zhí)行網(wǎng)關相關api創(chuàng)建路由


image.png

@Component
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {

    private static final Logger LOGGER = LoggerFactory.getLogger(NacosDynamicRouteService.class);

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;

    private ApplicationEventPublisher applicationEventPublisher;

    /** 路由id */
    private static List<String> routeIds = Lists.newArrayList();

    /**
     * 監(jiān)聽nacos路由配置,動態(tài)改變路由
     * @param configInfo
     */
    @NacosConfigListener(dataId = "routes", groupId = "gateway-server")
    public void routeConfigListener(String configInfo) {
        clearRoute();
        try {
            List<RouteDefinition> gatewayRouteDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
            for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
                addRoute(routeDefinition);
            }
            publish();
            LOGGER.info("Dynamic Routing Publish Success");
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
        
    }


    /**
     * 清空路由
     */
    private void clearRoute() {
        for (String id : routeIds) {
            routeDefinitionWriter.delete(Mono.just(id)).subscribe();
        }
        routeIds.clear();
    }

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

    /**
     * 添加路由
     * 
     * @param definition
     */
    private void addRoute(RouteDefinition definition) {
        try {
            routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            routeIds.add(definition.getId());
        } catch (Exception e) {
            LOGGER.error(e.getMessage(), e);
        }
    }

    /**
     * 發(fā)布路由、使路由生效
     */
    private void publish() {
        this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
    }
}
過濾器

gateway提供GlobalFilter及Ordered兩個接口用來定義過濾器,我們自定義過濾器只需要實現(xiàn)這個兩個接口即可

  • GlobalFilter filter() 實現(xiàn)過濾器業(yè)務
  • Ordered getOrder() 定義過濾器執(zhí)行順序

通常一個網(wǎng)關服務的過濾主要包含 鑒權(是否登錄、是否黑名單、是否免登錄接口...) 限流(ip限流等等)功能,我們今天簡單介紹鑒權過濾器的流程實現(xiàn)

鑒權過濾器

需要實現(xiàn)鑒權過濾器,我們先得了解登錄及鑒權流程,如下圖所示

image.png

由圖可知,我們鑒權過濾核心就是驗證token是否有效,所以我們網(wǎng)關服務需要與業(yè)務系統(tǒng)在同一個redis庫,先給網(wǎng)關添加redis依賴及配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
  redis:
    host: redis-server
    port: 6379
    password:
    database: 0
代碼實現(xiàn)
  1. 定義過濾器AuthFilter
  2. 獲取請求對象 從請求頭或參數(shù)或cookie中獲取token(支持多種方式傳token對于客戶端更加友好,比如部分web下載請求會新建一個頁面,在請求頭中傳token處理起來比較麻煩)
  3. 沒有token,返回401
  4. 有token,查詢redis是否有效
  5. 無效則返回401,有效則完成驗證放行
  6. 重置token過期時間、添加內(nèi)部請求頭信息方便業(yè)務系統(tǒng)權限處理
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private static final String TOKEN_HEADER_KEY = "auth_token";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.獲取請求對象
        ServerHttpRequest request = exchange.getRequest();
        // 2.獲取token
        String token = getToken(request);
        ServerHttpResponse response = exchange.getResponse();
        if (StringUtils.isBlank(token)) {
            // 3.token為空 返回401
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        // 4.驗證token是否有效
        String userId = getUserIdByToken(token);
        if (StringUtils.isBlank(userId)) {
            // 5.token無效 返回401
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        // token有效,后續(xù)業(yè)務處理
        // 從寫請求頭,方便業(yè)務系統(tǒng)從請求頭獲取用戶id進行權限相關處理
        ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
        request = builder.header("user_id", userId).build();
        // 延長緩存過期時間-token緩存用戶如果一直在操作就會一直重置過期
        // 這樣避免用戶操作過程中突然過期影響業(yè)務操作及體驗,只有用戶操作間隔時間大于緩存過期時間才會過期
        resetTokenExpirationTime(token, userId);
        // 完成驗證
        return chain.filter(exchange);
    }


    @Override
    public int getOrder() {
        // 優(yōu)先級 越小越優(yōu)先
        return 0;
    }

    /**
     * 從redis中獲取用戶id
     * 在登錄操作時候 登陸成功會生成一個token, redis得key為auth_token:token 值為用戶id
     *
     * @param token
     * @return
     */
    private String getUserIdByToken(String token) {
        String redisKey = String.join(":", "auth_token", token);
        return redisTemplate.opsForValue().get(redisKey);
    }

    /**
     * 重置token過期時間
     *
     * @param token
     * @param userId
     */
    private void resetTokenExpirationTime(String token, String userId) {
        String redisKey = String.join(":", "auth_token", token);
        redisTemplate.opsForValue().set(redisKey, userId, 2, TimeUnit.HOURS);
    }


    /**
     * 獲取token
     *
     * @param request
     * @return
     */
    private static String getToken(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        // 從請求頭獲取token
        String token = headers.getFirst(TOKEN_HEADER_KEY);
        if (StringUtils.isBlank(token)) {
            // 請求頭無token則從url獲取token
            token = request.getQueryParams().getFirst(TOKEN_HEADER_KEY);
        }
        if (StringUtils.isBlank(token)) {
            // 請求頭和url都沒有token則從cookies獲取
            HttpCookie cookie = request.getCookies().getFirst(TOKEN_HEADER_KEY);
            if (cookie != null) {
                token = cookie.getValue();
            }
        }
        return token;
    }
}

總結(jié)

Gateway通過配置項可以實現(xiàn)路由功能,整合Nacos及配置監(jiān)聽可以實現(xiàn)動態(tài)路由,實現(xiàn)GlobalFilter, Ordered兩個接口可以快速實現(xiàn)一個過濾器,文中也詳細的介紹了登錄后的請求鑒權流程,如果有不清楚地方可以評論區(qū)見咯。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容