插件开发版| Authing 结合 APISIX 实现统一可配置 API 权限网关 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Authing
V2EX    API

插件开发版| Authing 结合 APISIX 实现统一可配置 API 权限网关

  •  
  •   Authing 2023-03-24 17:22:46 +08:00 2091 次点击
    这是一个创建于 933 天前的主题,其中的信息可能已经有所发展或是发生改变。

    当开发者在构建网站、移动设备或物联网应用程序时,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 插件https://github.com/fehu-asia/authing-apisix-python-agent

    Java Adapter: https://github.com/fehu-asia/authing-apisix-java-adapter

    Java 插件https://github.com/fehu-asia/authing-apisix-java-agent

    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 官网:www.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": "http://192.168.1.123:8080/isAllow","user_pool_id": "124u2353h2t24he2u349382u152","user_pool_secret": "6435462313i5412njburh2u34"}"

    4.4 APISIX 插件开发和部署

    • 4.4.1 建立插件工程目录

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

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

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

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

    https://apisix.apache.org/docs/apisix/external-plugin/

    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

    package cn.authing.apisix.adapter.controller; 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 [email protected] */ @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 访问控制以及更加场景化的权限方案。

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     3728 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 35ms UTC 04:16 PVG 12:16 LAX 21:16 JFK 00:16
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86