免费注册,打造高效身份管理
博客/开发者/插件开发版|Authing 结合 APISIX 实现统一可配置 API 权限网关
插件开发版|Authing 结合 APISIX 实现统一可配置 API 权限网关
Authing 官方2023.02.20阅读 23

当开发者在构建网站、移动设备或物联网应用程序时,API 网关作为微服务架构中不可或缺的控制组件,是流量的核心进出口。通过有效的权限管控,可以实现认证授权、监控分析等功能,提高 API 的安全性、可用性、拓展性以及优化 API 性能。在上一篇文章(点击下方图片即可查看【快速启动版】)中,演示了通过 Authing 权限管理 + APISIX 实现 API 的访问控制效果,本文将教你如何实现上述能力的具体实践方法

01 关于 Authing

Authing 是国内首款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务。以「API First」作为产品基石,把身份领域所有常用功能都进行了模块化的封装,通过全场景编程语言 SDK 将所有能力 API 化提供给开发者。同时,用户可以灵活的使用 Authing 开放的 RESTful APIs 进行功能拓展,满足不同企业不同业务场景下的身份和权限管理需求

02 关于 APISIX

Apache APISIX 是一个动态、实时、高性能的 API 网关,提供负载均衡、动态上游、灰度发布、服务熔断、身份认证、可观测性等丰富的流量管理功能。Apache APISIX 不仅支持插件动态变更和热插拔,而且拥有众多实用的插件。Apache APISIX 的 OpenID Connect 插件支持 OpenID Connect 协议,用户可以使用该插件让 Apache APISIX 对接 Authing 服务,作为集中式认证网关部署于企业中。

03 业务目标

通过 Authing 权限管理 + APISIX 实现 API 的访问控制

04 如何实现

本文所涉及到的代码已经上传到 Github

Python 插件: github.com/fehu-asia/au

Java Adaptergithub.com/fehu-asia/au

Java 插件: github.com/fehu-asia/au

4.1 业务架构

系统整体包含了三大部分: Authing 服务集群、Authing 插件适配服务以及 APISIX 网关,本方案建立需要配置和开发的部分有四个部分,Authing API 权限结构配置、APISIX 插件和路由配置、APISIX 插件开发部署以及业务适配服务开发,其中业务适配服务包含了认证和授权的主要逻辑(使用单独服务承载),避免了插件的频繁更新和部署。

这里需要说明的是,之所以采用 Adapter 的方式来实现,是因为插件我们并不希望经常变动,但需求可能是无法避免的需要经常变动,所以我们将具体的鉴权逻辑放在 Adapter ,插件只实现请求转发和根据 Adapter 的返回结果决定是否放行,同时无状态的插件可以让我们实现更多的场景复用和能力扩展,例如进行鉴权结果的缓存实现,后续只需维护 Adapter 即可。

当然我们也可将具体的逻辑放在插件里。

注意,本教程只用于与 APISIX 和 Authing 进行集成,对于生产环境使用,您需要自行开发插件并保证其安全性及可用性等,本文档不承诺此插件可以用于生产环境

  • APISIX 基础环境搭建
    git clone https://github.com/apache/apisix-docker.git
    cd apisix-docker/example
    docker-compose -p docker-apisix up -d

到这里可以使用 docker ps 查看 apisix docker 进程启动状态, 随后访问 localhost:9000 可以进入 dashboard 界面进行路由和插件的配置。

 

4.2 在 Authing 对 API 进行管理

登录 Authing 官网:authing.com ,进行以下操作:

  • 4.2.1 创建应用

配置 Token 签名算法为 RS256 及校验 AccessToken 的方式为 none 。

  • 4.2.2 创建用户

进入 Authing 控制台-用户管理-用户列表-点击创建用户后,可以根据不同方式(用户名、手机号、邮箱)创建测试用户,如下图所示:

  • 4.2.3 创建 API

进入 Authing 控制台-权限管理-创建资源,可以选择创建树数据类型的资源,如下图所示:

  • 4.2.4 创建策略

进入权限管理-数据资源权限-数据策略标签,可以点击创建策略来新建数据访问策略,如下图所示。策略包含了对应的权限空间中定义的数据以及操作,创建后能够基于此策略对不同对象(用户、角色、用户组等)进行授权管理。

  • 4.2.5 API 授权

4.3 APISIX 路由和 SOCK 配置

  • 4.3.1 SOCK 配置

APISIX 使用 unix sock 与插件进程通信,因此需要配置对应的 sock 端口:

需要将宿主机上的 sock 文件挂载到容器里,插件启动的时候会在宿主机上创建这个 sock 文件,此处需要注意的是,若 APISIX 是先于插件启动的,当插件启动后,则需要重启下 APISIX 容器,确保插件先于 APISIX 启动。

文件位置: /apisix-docker/example/docker-compose.yml apisix部分

 apisix:
    image: apache/apisix:latest
    restart: always
    volumes:
      - ./apisix_log:/usr/local/apisix/logs
      - ./apisix_conf/config.yaml:/usr/local/apisix/conf/config.yaml:ro
      - /tmp/runner.sock:/tmp/runner.sock
 
  • 4.3.2 路由配置

X-API-KEY: /apisix/apisix-docker/example/apisix_conf/config.yaml

 
 curl http://127.0.0.1:9180/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
  "uri": "/*",
  "plugins": {
    "ext-plugin-pre-req": {
      "conf": [
 {
          "name": "authing_agent",
      "value": "{\"url\": \"{适配服务的访问地址}\",\"user_pool_id\": \"{用户池ID}\",\"user_pool_secret\": \"{用户池密钥}\"}"
    }
      ]
    }
  },
  "upstream": {
        "type": "roundrobin",
        "nodes": {
            "httpbin.org:80": 1
        }
    }
}'
 

ext-plugin-pre-req 是需要启用的插件类型, 在配置 conf 中需要确定两个变量:

"name": 插件名称
"value": "{"url": "适配服务的访问地址","user_pool_id": "用户池ID","user_pool_secret": "用户池密钥"}"

其中,访问地址格式为 {{domain}}:{{port}}/{{path}}

例如: "{"url": "192.168.1.123:8080/isAl","user_pool_id": "124u2353h2t24he2u349382u152","user_pool_secret": "6435462313i5412njburh2u34"}"

 

4.4 APISIX 插件开发和部署

  • 4.4.1 建立插件工程目录

git clone github.com/apache/apisi 进入目录 make setup 进入目录 make install 进入目录并修改 apisix/plugins/rewrite.py 文件,将请求参数传递到 Authing

  • 4.4.2 编写 Agent (python) 插件代码

可使用其他语言实现例如 Java、Go、Lua

之所以采用 Python 的原因是因为环境初始化比较简单,可以让开发者快速了解 APISIX 的插件的开发机制。

apisix.apache.org/docs/

from typing import Any
from apisix.runner.http.request import Request
from apisix.runner.http.response import Response
from apisix.runner.plugin.core import PluginBase
import json
import requests
import json

def isAllow(request,config):
    return requests.request("POST", 
        config.get("url"), 
        headers={
        'Content-Type': 'application/json'
        }, 
        data=json.dumps({
        "request": request,
        "pluginConfig": config
        }))



class Rewrite(PluginBase):

    def name(self) -> str:
        return "authing_agent"

    def config(self, conf: Any) -> Any:
        return conf

    def filter(self, conf: Any, request: Request, response: Response):
        # 组装 Adapter 请求参数
        authing_request = {
        "uri": request.get_uri(),
        "method": request.get_method(),
        "args":request.get_args(),
        "headers":request.get_headers(),
        "request_id":request.get_id(),
        "host":request.get_var("host"),
        "remote_addr": request.get_remote_addr(),
        "configs": request.get_configs()
        }
        # 接收 Adapter 响应判断是否放行
        authing_response = isAllow(authing_request,eval(conf))
        if authing_response.text != "ok":
            response.set_status_code(authing_response.status_code)
            response.set_body(authing_response.text)    
 
  • 4.4.3 运行 Agent 插件
 nohup make dev & #后台运行 agent 程序

 

4.5 适配器开发

  • 4.5.1 通信接口设计

启动代理 Authing 服务(自行实现对应接口,以 springboot 为例,接口结构如下)

  • 4.5.2 部分 JAVA 文件列出如下

IsAllowController.java

 
import cn.authing.apisix.adapter.entity.APISIXRquestParams;
import cn.authing.sdk.java.client.ManagementClient;
import cn.authing.sdk.java.dto.CheckPermissionDto;
import cn.authing.sdk.java.dto.CheckPermissionRespDto;
import cn.authing.sdk.java.dto.CheckPermissionsRespDto;
import cn.authing.sdk.java.model.ManagementClientOptions;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import com.google.gson.Gson;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Gao FeiHu
 * @version 1.0.0
 * @date 2022.12.22
 * @email gaofeihu@authing.cn
 */
@RestController
@Slf4j
public class IsAllowController {

    /**
     * 用户池 ID
     */
    public static String ACCESS_KEY_ID = "";
    /**
     * 用户池密钥
     */
    public static String ACCESS_KEY_SECRET = "";
    /**
     * Authing SDK
     * See
     * https://docs.authing.cn/v3/reference/
     */
    ManagementClient managementClient;

    /**
     * 初始化 ManagementClient
     *
     * @param ak  用户池 ID
     * @param aks 用户池密钥
     */
    public void init(String ak, String aks) {
        log.info("init ManagementClient ......");
        try {
            // 保存用户池 ID 和密钥
            ACCESS_KEY_ID = ak;
            ACCESS_KEY_SECRET = aks;
            // 初始化
            ManagementClientOptions options = new ManagementClientOptions();
            options.setAccessKeyId(ak);
            options.setAccessKeySecret(aks);
            managementClient = new ManagementClient(options);
        } catch (Exception e) {
            e.printStackTrace();
            System.err.println("初始化 managementClient 失败,可能无法请求!");
        }
    }

    /**
     * 是否放行
     *
     * @param apisixRquestParams 请求 body ,包含了 APISIX 插件的配置以及请求上下文
     * @param response           HttpServletResponse
     * @return 200 OK 放行
     * 403 forbidden 禁止访问
     * 500 internal server error 请求错误 可根据实际需求放行或拒绝
     */
    @PostMapping("/isAllow")
    public Object isAllow(@RequestBody APISIXRquestParams apisixRquestParams, HttpServletResponse response) {

        // 请求计时器
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        // 请求 ID 与 APISIX 一致
        String requestID = apisixRquestParams.getRequest().getRequest_id();

        log.info("{} ==> 请求入参 : {} ", requestID, new Gson().toJson(apisixRquestParams));

        try {
            // 0. 若插件为多实例用于实现不同业务逻辑,此处可对应修改为多实例模式
            if (managementClient == null || !ACCESS_KEY_ID.equals(apisixRquestParams.getPluginConfig().get("user_pool_id"))) {
                init((String) apisixRquestParams.getPluginConfig().get("user_pool_id"), (String) apisixRquestParams.getPluginConfig().get("user_pool_secret"));
            }

            // 1. 拿到 accessToken
            String authorization = (String) apisixRquestParams.getRequest().getHeaders().get("authorization");
            if (!StringUtils.hasLength(authorization)) {
                return result(response, stopWatch, requestID, HttpStatus.HTTP_UNAUTHORIZED, "HTTP_UNAUTHORIZED");
            }

            String accessToken = authorization;
            if (authorization.startsWith("Bearer")) {
                accessToken = authorization.split(" ")[1].trim();
            }


            log.info("{} ==> accessToken : {} ", requestID, accessToken);
            // 2. 解析 accessToken 拿到应用 ID 和用户 ID
            JWSObject parse = JWSObject.parse(accessToken);
            Map<String, Object> payload = parse.getPayload().toJSONObject();
            String aud = (String) payload.get("aud");
            String sub = (String) payload.get("sub");

            // 3. 校验 accessToken
            // 在线校验
            String result = onlineValidatorAccessToken(accessToken, aud);
            log.info("{} ==> accessToken 在线结果 : {} ", requestID, result);
            if (!result.contains("{\"active\":true")) {
                return result(response, stopWatch, requestID, HttpStatus.HTTP_UNAUTHORIZED, "HTTP_UNAUTHORIZED");
            }

//            // 离线校验
//            if (null == offlineValidatorAccessToken(accessToken, aud)) {
//                return result(response, stopWatch, requestID, HttpStatus.HTTP_UNAUTHORIZED, "HTTP_UNAUTHORIZED");
//            }

            // 4. 获取到 APISIX 中的请求方法,对应 Authing 权限中的 action
            String action = apisixRquestParams.getRequest().getMethod();

            // 5. 获取到 APISIX 中的请求路径
            String resource = apisixRquestParams.getRequest().getUri();

            // 6. 去 Authing 请求,判断是否有权限
            // TODO 可在此添加 Redis 对校验结果进行缓存
            CheckPermissionDto reqDto = new CheckPermissionDto();
            reqDto.setUserId(sub);
            reqDto.setNamespaceCode(aud);
            reqDto.setResources(Arrays.asList(resource.substring(1, resource.length())));
            reqDto.setAction(action);
            CheckPermissionRespDto checkPermissionRespDto = managementClient.checkPermission(reqDto);
            log.info(new Gson().toJson(checkPermissionRespDto));

            // 7. 由于我们是单个 resource 校验,所以只需要判断第一个元素即可
            List<CheckPermissionsRespDto> resultList = checkPermissionRespDto.getData().getCheckResultList();
            if (resultList.isEmpty() || resultList.get(0).getEnabled() == false) {
                return result(response, stopWatch, requestID, HttpStatus.HTTP_FORBIDDEN, "HTTP_FORBIDDEN");
            }

            return result(response, stopWatch, requestID, HttpStatus.HTTP_OK, "ok");

        } catch (Exception e) {
            e.printStackTrace();
            log.error("请求错误!", e);
            return result(response, stopWatch, requestID, HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage());
        }
    }


    public String result(HttpServletResponse response, StopWatch stopWatch, String requestID, int status, String msg) {
        stopWatch.stop();
        log.info("{} ==> 请求耗时:{} , 请求出参 : http_status_code={},msg={} ", requestID, stopWatch.getTotalTimeMillis() + "ms", status, msg);
        response.setStatus(status);
        return msg;
    }


    public String onlineValidatorAccessToken(String accessToken, String aud) {
        HashMap<String, Object> paramMap = new HashMap<>();
        paramMap.put("token", accessToken);
        paramMap.put("token_type_hint", "access_token");
        paramMap.put("client_id", aud);
        return HttpUtil.post("https://api.authing.cn/" + aud + "/oidc/token/introspection", paramMap);

    }

    public JWTClaimsSet offlineValidatorAccessToken(String accessToken, String aud) {
        try {
            ConfigurableJWTProcessor<SecurityContext> jwtProcessor =
                    new DefaultJWTProcessor<>();
            JWKSource<SecurityContext> keySource =
                    null;

            keySource = new RemoteJWKSet<>(new URL("https://api.authing.cn/" + aud + "/oidc/.well-known/jwks.json"));

            JWSAlgorithm expectedJWSAlg = JWSAlgorithm.RS256;

            JWSKeySelector<SecurityContext> keySelector =
                    new JWSVerificationKeySelector<>(expectedJWSAlg, keySource);

            jwtProcessor.setJWSKeySelector(keySelector);

            return jwtProcessor.process(accessToken, null);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        } catch (BadJOSEException e) {
            e.printStackTrace();
        } catch (JOSEException e) {
            e.printStackTrace();
        } finally {
            return null;
        }
    }
} 
 

APISIXRquestParams.java

 package cn.authing.apisix.adapter.entity;

import lombok.Data;
import lombok.ToString;

import java.util.Map;

/**
 * APISIX 请求实体类
 */
@Data
@ToString
public class APISIXRquestParams {
    /**
     * APISIX 请求上下文
     */
    APISIXRequest request;
    /**
     * 插件配置
     */
    Map<String, Object> pluginConfig;

}
 

APISIXRequest.java

 package cn.authing.apisix.adapter.entity;

import lombok.Data;
import lombok.ToString;

import java.util.Map;

@Data
@ToString
public class APISIXRequest {
    private String uri;
    private String method;
    private String request_id;
    private String host;
    private String remote_addr;
    private Map<String, Object> args;
    private Map<String, Object> headers;
    private Map<String, Object> configs;
}
 

4.6 访问测试

  • 4.6.1 未认证
  • 4.6.2 无权限
  • 4.6.3 认证通过并成功访问

404 是因为上游服务没有这个接口,但认证和 API 鉴权已经通过

05 总结

如果您需要对 API 进行细颗粒度的管理可以通过本方案来实现,我们可以在 Adapter 实现更加细粒度的 API 访问控制以及更加场景化的权限方案。

 

 

文章作者

avatar

Authing 官方

0

文章总数

authing blog rqcode
关注 Authing 公众号
随时随地发现更多内容
authing blog rqcode
添加 Authing 小助手
加入 Authing 开发者大家庭
身份顾问在线解答
当前在线
如何打造完整的身份体系?
立即沟通
authing
添加企业微信,领取行业资料
authing
authing
下载 Authing 令牌,体验快速登录认证!
免费使用
在线咨询
电话咨询