产物网关 artifact-gateway:基于 spring-cloud-gateway,支持 OAuth2 authorization_code、Basic 等协议

仓库地址

原理分析

Mirrors 路由配置与 AbstractGatewayFilterFactory

1. 路由全景图

flowchart TB
    subgraph Client["客户端请求"]
        BROWSER["🌐 浏览器<br/>(无 Authorization 头)"]
        API_CLIENT["🔧 API 客户端<br/>(带 Authorization: Basic ... 头)"]
        OAUTH_CB["🔄 OAuth2 回调<br/>(GitLab 重定向回 /oauth2-gitlab/callback)"]
    end

    subgraph SCG["Spring Cloud Gateway 路由匹配"]
        direction TB

        R1["路由 1: oauth2-gitlab-callback<br/>━━━━━━━━━━━━━━━━━━<br/>Path=/oauth2-gitlab/callback<br/>→ OAuth2AuthorizationCode<br/> uri: no://op"]
        R2["路由 2: mirrors-basic<br/>━━━━━━━━━━━━━━━━━━<br/>Path=/service/rest/repository/browse/**<br/>Header=Authorization, Basic .+<br/>→ RewritePath → DelegateBasicAuth → StaticFileServer<br/> uri: no://op"]
        R3["路由 3: mirrors-oauth2<br/>━━━━━━━━━━━━━━━━━━<br/>Path=/service/rest/repository/browse/**<br/>→ RewritePath → OAuth2AuthorizationCode → StaticFileServer<br/> uri: no://op"]

        R1 --> R2 --> R3
    end

    BROWSER -->|"GET /service/rest/repository/browse/"| SCG
    API_CLIENT -->|"GET /service/rest/repository/browse/<br/>Authorization: Basic base64(token:)"| SCG
    OAUTH_CB -->|"GET /oauth2-gitlab/callback?code=...&state=..."| SCG

    subgraph Legend["图例"]
        L1["🔒 认证过滤器"]
        L2["✂️ 路径重写"]
        L3["📁 静态文件服务"]
    end

    style R1 fill:#ffe6cc,stroke:#d79b00
    style R2 fill:#d5e8d4,stroke:#82b366
    style R3 fill:#dae8fc,stroke:#6c8ebf

2. mirrors-basic 路由详细流程 (Basic Auth 委托)

sequenceDiagram
    participant Client as API Client
    participant GW as Spring Cloud Gateway
    participant RP as RewritePath Filter
    participant DBA as DelegateBasicAuth Filter
    participant SFS as StaticFileServer Filter
    participant GitLab as GitLab API<br/>(authUrl)

    Client->>GW: GET /service/rest/repository/browse/kubernetes/<br/>Authorization: Basic base64(glpat-xxxx:)

    Note over GW,RP: Route matched: mirrors-basic<br/>Predicates: Path + Header=Authorization: Basic .+

    GW->>RP: 1️⃣ RewritePath
    Note over RP: regexp: /service/rest/repository/browse/?(?<remaining>.*)<br/>replacement: /${remaining}<br/>→ path becomes /kubernetes/

    GW->>DBA: 2️⃣ DelegateBasicAuth (order=-10)
    Note over DBA: 提取 Authorization: Basic ...<br/>passDecodedOnly=true → Base64解码<br/>passPasswordOnly=true → 只取密码部分(glpat-xxxx)<br/>forwardHeaderName=PRIVATE-TOKEN

    DBA->>GitLab: GET https://gitlab.xuxiaowei.com.cn/api/v4/user<br/>PRIVATE-TOKEN: glpat-xxxx
    GitLab-->>DBA: 200 OK (包含 marker: "active")

    Note over DBA: 检查响应体是否包含 marker<br/>✅ 认证通过 → 继续

    GW->>SFS: 3️⃣ StaticFileServer (order=10)
    Note over SFS: baseDirectory: ./blobs/<br/>查找文件: ./blobs/kubernetes/

    SFS-->>Client: 200 OK — 目录列表 HTML 或文件内容

3. mirrors-oauth2 路由详细流程 (OAuth2 授权码)

sequenceDiagram
    participant Browser as 浏览器
    participant GW as Spring Cloud Gateway
    participant RP as RewritePath Filter
    participant OAuth as OAuth2AuthorizationCode Filter
    participant GitLab as GitLab OAuth2

    Browser->>GW: GET /service/rest/repository/browse/
    Note over GW: Route matched: mirrors-oauth2<br/>(无 Authorization header, 由 mirrors-basic 漏下)

    GW->>RP: 1️⃣ RewritePath (同上)
    GW->>OAuth: 2️⃣ OAuth2AuthorizationCode (order=-10)

    Note over OAuth: ❌ 无 oauth2_access_token cookie<br/>生成 32字节随机 state<br/>设置 cookie: oauth2_state + oauth2_original_url<br/>

    OAuth-->>Browser: 302 → GitLab /oauth/authorize<br/>?response_type=code&client_id=...&redirect_uri=...&scope=read_user&state=...

    Browser->>GitLab: GET /oauth/authorize (用户登录)
    GitLab-->>Browser: 302 → /oauth2-gitlab/callback?code=...&state=...

    Browser->>GW: GET /oauth2-gitlab/callback?code=...&state=...
    Note over GW: Route matched: oauth2-gitlab-callback

    GW->>OAuth: OAuth2AuthorizationCode

    Note over OAuth: ✅ 验证 state<br/>POST tokenUrl → 获取 access_token<br/>GET userInfoUri → 验证 token 有效 (含 marker)<br/>AES-256-GCM 加密 access_token<br/>RSA 签名 JWT → 设置 cookie: oauth2_access_token<br/>清除 state + original_url cookie

    OAuth-->>Browser: 302 → 原始 URL

    Browser->>GW: GET /service/rest/repository/browse/ (再次)
    GW->>RP: RewritePath
    GW->>OAuth: OAuth2AuthorizationCode

    Note over OAuth: ✅ 找到 oauth2_access_token cookie<br/>验证 JWT RSA 签名<br/>解密 access_token<br/>设置 Header: Authorization: Bearer <token><br/>

    GW->>GW: StaticFileServer
    Note over GW: 返回目录列表或文件

4. AbstractGatewayFilterFactory 类层次结构

classDiagram
    class AbstractGatewayFilterFactory~T~ {
        <<Spring Cloud Gateway>>
        +apply(T config) GatewayFilter
        #getConfigClass() Class~T~
        +name() String
        +shortcutFieldOrder() List~String~
    }

    class Ordered {
        <<interface>>
        +getOrder() int
    }

    class BasicAuthGatewayFilterFactory {
        -username: String
        -password: String
        +apply(Config) GatewayFilter
        +getOrder() -10
    }

    class DelegateBasicAuthGatewayFilterFactory {
        -authUrl: String
        -marker: String
        -authHeaderName: String
        -forwardHeaderName: String
        -passDecodedOnly: boolean
        -passPasswordOnly: boolean
        -webClient: WebClient
        +apply(Config) GatewayFilter
        +getOrder() -10
    }

    class DelegateBearerAuthGatewayFilterFactory {
        -authUrl: String
        -marker: String
        -authHeaderName: String
        -forwardHeaderName: String
        -webClient: WebClient
        +apply(Config) GatewayFilter
        +getOrder() -10
    }

    class OAuth2AuthorizationCodeGatewayFilterFactory {
        -authorizeUrl: String
        -tokenUrl: String
        -clientId: String
        -clientSecret: String
        -redirectUri: String
        -scopes: String
        -userInfoUri: String
        -jwtPrivateKey: RSAPrivateKey
        -jwtPublicKey: RSAPublicKey
        -encryptionSecret: String
        -excludePatterns: List~String~
        -webClient: WebClient
        +apply(Config) GatewayFilter
        +getOrder() -10
    }

    class StaticFileServerGatewayFilterFactory {
        -baseDirectory: String
        -docUrl: String
        -docText: String
        -description: String
        +apply(Config) GatewayFilter
        +getOrder() 10
    }

    class WriteResponseToFileGatewayFilterFactory {
        -directory: String
        -returnFilePath: boolean
        -useDatePrefix: boolean
        +apply(Config) GatewayFilter
        +getOrder() WRITE_RESPONSE_FILTER_ORDER-1
    }

    AbstractGatewayFilterFactory~T~ <|-- BasicAuthGatewayFilterFactory
    AbstractGatewayFilterFactory~T~ <|-- DelegateBasicAuthGatewayFilterFactory
    AbstractGatewayFilterFactory~T~ <|-- DelegateBearerAuthGatewayFilterFactory
    AbstractGatewayFilterFactory~T~ <|-- OAuth2AuthorizationCodeGatewayFilterFactory
    AbstractGatewayFilterFactory~T~ <|-- StaticFileServerGatewayFilterFactory
    AbstractGatewayFilterFactory~T~ <|-- WriteResponseToFileGatewayFilterFactory

    Ordered <|.. BasicAuthGatewayFilterFactory
    Ordered <|.. DelegateBasicAuthGatewayFilterFactory
    Ordered <|.. DelegateBearerAuthGatewayFilterFactory
    Ordered <|.. OAuth2AuthorizationCodeGatewayFilterFactory
    Ordered <|.. StaticFileServerGatewayFilterFactory
    Ordered <|.. WriteResponseToFileGatewayFilterFactory

    note for AbstractGatewayFilterFactory~T~ "泛型参数 T 是内部 Config 类<br/>通过 shortcutFieldOrder() 支持<br/>YAML 简化参数传递"

5. AbstractGatewayFilterFactory 核心原理

%%{init: {'flowchart': {'wrappingWidth': 450, 'nodeSpacing': 25, 'rankSpacing': 50}}}%%
flowchart TB
    subgraph Config["🟡 YAML 配置 (application-mirrors.yaml)"]
        YAML["filters:<br/>  - name: DelegateBasicAuth<br/>    args:<br/>      authUrl: https://...<br/>      marker: active"]
    end

    subgraph Loading["🟢 Spring Cloud Gateway 加载过程"]
        L1["1. GatewayAutoConfiguration 扫描所有<br/>GatewayFilterFactory Bean"]
        L2["2. RouteDefinitionRouteLocator 解析<br/>RouteDefinition"]
        L3["3. 根据 filter name 查找对应的<br/>GatewayFilterFactory Bean"]
        L4["4. 调用 normalize() → shortcutFieldOrder()<br/>将 args Map 按序映射到 Config 对象"]
        L5["5. 调用 apply(Config) 返回 GatewayFilter"]
    end

    subgraph Runtime["🔵 请求处理流程"]
        R1["请求到达 Gateway"]
        R2["RoutePredicateHandlerMapping<br/>匹配路由 predicates"]
        R3["FilteringWebHandler 构建<br/>Filter Chain"]
        R4["按 Order 排序所有 GatewayFilter<br/>(包括 GlobalFilter)"]
        R5["链式调用 filter(ServerWebExchange, GatewayFilterChain)"]
    end

    subgraph FilterChain["🟣 OrderedGatewayFilter 包装"]
        FC1["AbstractGatewayFilterFactory.apply()<br/>内部调用 OrderedGatewayFilter 包装"]
        FC2["将实现的 Ordered.getOrder()<br/>传递给 OrderedGatewayFilter"]
        FC3["确保 filter chain 按 order 值<br/>从小到大依次执行"]
    end

    subgraph Execution["🔴 过滤器执行顺序"]
        E1["LogWebFilter<br/>(order=HIGHEST_PRECEDENCE)<br/>↓"]
        E2["认证过滤器<br/>BasicAuth / DelegateBasicAuth<br/>DelegateBearer / OAuth2AuthorizationCode<br/>(order=-10)<br/>↓"]
        E3["业务过滤器<br/>StaticFileServer / WriteResponseToFile<br/>(order=10 / WRITE_RESPONSE-1)<br/>↓"]
        E4["NettyRoutingFilter<br/>(实际 HTTP 请求 — 本例 uri=no://op 跳过)"]
    end

    Config --> Loading
    Loading --> Runtime
    Runtime --> FilterChain
    FilterChain --> Execution

    style Config fill:#fff2cc,stroke:#d6b656
    style Loading fill:#d5e8d4,stroke:#82b366
    style Runtime fill:#dae8fc,stroke:#6c8ebf
    style FilterChain fill:#e1d5e7,stroke:#9673a6
    style Execution fill:#f8cecc,stroke:#b85450

6. YAML 配置到运行时对象的映射原理

%%{init: {'flowchart': {'wrappingWidth': 450, 'nodeSpacing': 25, 'rankSpacing': 50}}}%%
flowchart LR
    subgraph YAML["application-mirrors.yaml"]
        Y1["name: DelegateBasicAuth<br/>args:<br/>  authUrl: https://...<br/>  marker: active<br/>  authHeaderName: Authorization<br/>  forwardHeaderName: PRIVATE-TOKEN<br/>  passDecodedOnly: true<br/>  passPasswordOnly: true"]
    end

    subgraph NORM["normalize() 方法"]
        N1["1. 读取 shortcutFieldOrder()<br/>   = ['authUrl', 'marker']"]
        N2["2. 若 args 是 Map:<br/>   按 key 直接映射到 Config 字段"]
        N3["3. 若 args 是 List:<br/>   按 shortcutFieldOrder 索引<br/>   依次映射到对应字段"]
        N4["4. 类型转换 (String→boolean 等)"]
    end

    subgraph CONFIG["Config 对象 (内部类)"]
        C1["authUrl = 'https://...'<br/>marker = 'active'<br/>authHeaderName = 'Authorization'<br/>forwardHeaderName = 'PRIVATE-TOKEN'<br/>passDecodedOnly = true<br/>passPasswordOnly = true"]
    end

    subgraph FILTER["apply(Config)"]
        F1["return OrderedGatewayFilter(<br/>  (exchange, chain) → {<br/>    // 1. 提取认证头<br/>    // 2. 调用外部服务验证<br/>    // 3. 成功→chain.filter()<br/>    // 4. 失败→401<br/>  },<br/>  getOrder() = -10<br/>)"]
    end

    YAML --> NORM --> CONFIG --> FILTER

    style YAML fill:#fff2cc,stroke:#d6b656
    style NORM fill:#d5e8d4,stroke:#82b366
    style CONFIG fill:#dae8fc,stroke:#6c8ebf
    style FILTER fill:#f8cecc,stroke:#b85450

7. 三种认证方式对比

%%{init: {'flowchart': {'wrappingWidth': 450, 'nodeSpacing': 25, 'rankSpacing': 50}}}%%
flowchart LR
    subgraph BA["🔐 DelegateBasicAuth"]
        direction TB
        BA1["输入: Authorization: Basic base64(user:pass)"]
        BA2["1. 提取 header → Base64 解码"]
        BA3["2. passDecodedOnly? → 只用解码后的值"]
        BA4["3. passPasswordOnly? → 只取密码部分 (:后)"]
        BA5["4. 设置 forwardHeaderName=PRIVATE-TOKEN"]
        BA6["5. GET authUrl → 检查响应体 marker"]
        BA7["适用: GitLab Personal Access Token"]
        BA1 --> BA2 --> BA3 --> BA4 --> BA5 --> BA6 --> BA7
    end

    subgraph DBA["🔐 DelegateBearerAuth"]
        direction TB
        DBA1["输入: Authorization: Bearer <token>"]
        DBA2["1. 提取 header → 取 Bearer 后的 token"]
        DBA3["2. 设置 forwardHeaderName=Authorization"]
        DBA4["3. GET authUrl → 检查响应体 marker"]
        DBA5["适用: OAuth2 Bearer Token 内省"]
        DBA1 --> DBA2 --> DBA3 --> DBA4 --> DBA5
    end

    subgraph OAuth["🔐 OAuth2AuthorizationCode"]
        direction TB
        OA1["无有效 token → 302 重定向到授权页"]
        OA2["回调 → 用 code 换 token"]
        OA3["token 加密后存 JWT cookie"]
        OA4["后续请求 → 解密 JWT 获取 token"]
        OA5["设置 Authorization: Bearer <token>"]
        OA6["支持 excludePatterns 白名单"]
        OA7["适用: 浏览器用户 (交互式登录)"]
        OA1 --> OA2 --> OA3 --> OA4 --> OA5 --> OA6 --> OA7
    end

    BA ~~~ DBA ~~~ OAuth

    style BA fill:#d5e8d4,stroke:#82b366
    style DBA fill:#dae8fc,stroke:#6c8ebf
    style OAuth fill:#e1d5e7,stroke:#9673a6

总结

路由匹配策略

  1. oauth2-gitlab-callback — 精确匹配 OAuth2 回调路径,由 GitLab 重定向触发
  2. mirrors-basic — 同时匹配路径 Authorization: Basic 头,为 API 客户端提供 Basic Auth 委托
  3. mirrors-oauth2 — 只匹配路径(Basic 路由匹配不上时漏下),为浏览器用户提供 OAuth2 交互式登录

AbstractGatewayFilterFactory 核心机制

  • name() 返回 Bean 名(如 DelegateBasicAuth),YAML 中通过 name 引用
  • shortcutFieldOrder() 支持 YAML 中 args 的简写形式
  • apply(Config) 是核心方法,返回 GatewayFilter 函数式接口
  • Ordered 接口配合 OrderedGatewayFilter 控制过滤器链执行顺序
  • OrderedGatewayFilter 是 Spring Cloud Gateway 的内部类,将 Ordered 的 order 值注入到过滤器链排序中
  • no://op 表示不转发到真实后端,响应由过滤器直接生成

真实使用场景

yum/rpm 仓库增加权限认证

支持 OAuth2 authorization_code 协议:用于浏览器访问

  1. 支持白名单路径

支持 Basic 协议:用于 rpm 仓库认证

支持代理本地(静态)文件