APISIX Java自定义插件完成接口的权限认证

前言介绍

PS:本项目源码是根据公司实际的业务场景而开发的,只具有参考意义。

使用的中间件有

Redis :使用其Watch订阅功能,RedisAdapter数据源转接器

Casbin:用的是官方的Restfull 的conf,并做了对应业务的改动,下面的介绍中会有体现。

项目背景

由于请求的权限是由外部系统配置和生成,并且数据都是不规范化的,所以采用redis中中间件,用于储存外部系统的权限数据,并用redis的订阅功能,给casbin发送权限变化的通知,casbin收到通知后,会自动调用loadPolicy方法,重新拉取redis的权限数据。

流程图

项目结构

代码介绍

ApplicationDestroyService

项目关闭,调用关闭Redis连接池

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ApplicationDestroyService implements DisposableBean {

    private final Logger logger = LoggerFactory.getLogger(ApplicationDestroyService.class);

    @Override
    public void destroy() throws Exception {
        logger.info("Application is closed !");
        EnforcerFactory.getRedisWatcherAndAdapter().close();
    }
}

EnforcerFactory

项目启动,加载资源权限,以及增加订阅,具体查看

Casbin Watcher 官方文档

import org.casbin.jcasbin.main.Enforcer;
import org.casbin.jcasbin.main.SyncedEnforcer;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class EnforcerFactory implements InitializingBean {

    private static Enforcer enforcer;

    private static RedisWatcherAndAdapter redisWatcherAndAdapter;

    @Value("${casbin.conf.mode}")
    private String modelPath;

    @Value("${redis.topic.watcher}")
    private String watcherTopic;

    @Value("${redis.topic.api}")
    private String apiTopic;

    @Value("${redis.ip}")
    private String redisIp;

    @Value("${redis.port}")
    private Integer redisPort;

    @Override
    public void afterPropertiesSet() throws Exception {
        redisWatcherAndAdapter = new RedisWatcherAndAdapter(redisIp, redisPort,watcherTopic,apiTopic);
        enforcer = new SyncedEnforcer(modelPath, redisWatcherAndAdapter);
        enforcer.setWatcher(redisWatcherAndAdapter);
    }

    public static RedisWatcherAndAdapter getRedisWatcherAndAdapter(){
        return redisWatcherAndAdapter;
    }

    public static Enforcer getEnforcer(){
        return enforcer;
    }

}

RedisWatcherAndAdapter

将Casbin  Redis watcher 和 Adapter整合成一个类,具体可参考

Casbin adapters介绍


import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import org.casbin.adapter.domain.CasbinRule;
import org.casbin.jcasbin.model.Assertion;
import org.casbin.jcasbin.model.Model;
import org.casbin.jcasbin.persist.Adapter;
import org.casbin.jcasbin.persist.Helper;
import org.casbin.jcasbin.persist.Watcher;
import org.casbin.watcher.SubThread;
import org.springframework.util.CollectionUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.*;
import java.util.function.Consumer;

public class RedisWatcherAndAdapter implements Watcher, Adapter {
    private Runnable updateCallback;
    private final JedisPool jedisPool;
    private final String localId;
    private final String redisChannelName;
    private SubThread subThread;

    private final String key;

    public RedisWatcherAndAdapter(String redisIp, int redisPort, String redisChannelName, int timeout, String password,String adapterKey) {
        this.jedisPool = new JedisPool(new JedisPoolConfig(), redisIp, redisPort, timeout, password);
        this.localId = UUID.randomUUID().toString();
        this.redisChannelName = redisChannelName;
        this.key = adapterKey;
        this.startSub();
    }

    public RedisWatcherAndAdapter(String redisIp, int redisPort, String redisChannelName,String adapterKey) {
        this(redisIp, redisPort, redisChannelName, 2000, (String)null,adapterKey);
    }

    public void setUpdateCallback(Runnable runnable) {
        this.updateCallback = runnable;
        this.subThread.setUpdateCallback(runnable);
    }

    public void setUpdateCallback(Consumer<String> consumer) {
        this.subThread.setUpdateCallback(consumer);
    }

    public void update() {
        try {
            Jedis jedis = this.jedisPool.getResource();
            Throwable var2 = null;

            try {
                jedis.publish(this.redisChannelName, "Casbin policy has a new version from redis watcher: " + this.localId);
            } catch (Throwable var12) {
                var2 = var12;
                throw var12;
            } finally {
                if (jedis != null) {
                    if (var2 != null) {
                        try {
                            jedis.close();
                        } catch (Throwable var11) {
                            var2.addSuppressed(var11);
                        }
                    } else {
                        jedis.close();
                    }
                }

            }
        } catch (Exception var14) {
            var14.printStackTrace();
        }

    }

    private void startSub() {
        this.subThread = new SubThread(this.jedisPool, this.redisChannelName, this.updateCallback);
        this.subThread.start();
    }

    public void loadPolicy(Model model) {
        System.out.println("init loadPolicy :"+ DateUtil.now());
        Jedis jedis = this.jedisPool.getResource();
        Long length = jedis.hlen(this.key);
        if (length != null) {

            Set<String> policies = jedis.hkeys(this.key);
            Iterator var4 = policies.iterator();

            while(var4.hasNext()) {
                String policy = (String)var4.next();
                //此处代码做过调整,具体可以参考 org.casbin.adapter.RedisAdapter
                String resources = jedis.hget(this.key, policy);
                JSONArray array = JSONUtil.parseArray(resources);
                String permissionStr = array.getStr(1);
                if(JSONUtil.isTypeJSONArray(permissionStr)){
                    JSONUtil.parseArray(permissionStr).stream().forEach(obj->{
                        Helper.loadPolicyLine((String) obj, model);
                    });
                }
            }
        }else {
            model.model.clear();
        }
        this.jedisPool.returnResource(jedis);
        System.out.println("loadPolicy end:"+ DateUtil.now());
    }

    public void savePolicy(Model model) {
        this.extracted(model, "p");
        this.extracted(model, "g");
    }

    public void addPolicy(String sec, String ptype, List<String> rule) {
        if (!CollectionUtils.isEmpty(rule)) {
            CasbinRule line = this.savePolicyLine(ptype, rule);
            this.jedisPool.getResource().rpush(this.key, new String[]{JSONObject.toJSONString(line)});
        }
    }

    public void removePolicy(String sec, String ptype, List<String> rule) {
        if (!CollectionUtils.isEmpty(rule)) {
            CasbinRule line = this.savePolicyLine(ptype, rule);
            this.jedisPool.getResource().lrem(this.key, 1L, JSONObject.toJSONString(line));
        }
    }
    private void extracted(Model model, String type) {
        Iterator var3 = ((Map)model.model.get(type)).entrySet().iterator();

        while(var3.hasNext()) {
            Map.Entry<String, Assertion> entry = (Map.Entry)var3.next();
            String ptype = (String)entry.getKey();
            Assertion ast = (Assertion)entry.getValue();
            Iterator var7 = ast.policy.iterator();

            while(var7.hasNext()) {
                List<String> rule = (List)var7.next();
                CasbinRule line = this.savePolicyLine(ptype, rule);
                this.jedisPool.getResource().rpush(this.key, new String[]{JSONObject.toJSONString(line)});
            }
        }

    }
    public void removeFilteredPolicy(String sec, String ptype, int fieldIndex, String... fieldValues) {
        List<String> values = (List) Optional.of(Arrays.asList(fieldValues)).orElse(new ArrayList());
        throw new RuntimeException("not implement");
    }
    private CasbinRule savePolicyLine(String ptype, List<String> rule) {
        CasbinRule line = new CasbinRule();
        line.setPtype(ptype);
        if (rule.size() > 0) {
            line.setV0((String)rule.get(0));
        }

        if (rule.size() > 1) {
            line.setV1((String)rule.get(1));
        }

        if (rule.size() > 2) {
            line.setV2((String)rule.get(2));
        }

        if (rule.size() > 3) {
            line.setV3((String)rule.get(3));
        }

        if (rule.size() > 4) {
            line.setV4((String)rule.get(4));
        }

        if (rule.size() > 5) {
            line.setV5((String)rule.get(5));
        }

        return line;
    }

    public void close(){
        this.jedisPool.close();
    }
}

CheckPermissionsFilter

校验权限的拦截器,由于需要用到用户名,这边定义了接口请求中,需要在header中添加username字段。


import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
import com.chalcosteering.gateway.common.EnforcerFactory;
import org.apache.apisix.plugin.runner.HttpRequest;
import org.apache.apisix.plugin.runner.HttpResponse;
import org.apache.apisix.plugin.runner.filter.PluginFilter;
import org.apache.apisix.plugin.runner.filter.PluginFilterChain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class CheckPermissionsFilter  implements PluginFilter {

    private final Logger logger = LoggerFactory.getLogger(CheckPermissionsFilter.class);

    @Override
    public String name() {
        return "check_permissions";
    }


    @Override
    public void filter(HttpRequest request, HttpResponse response, PluginFilterChain chain) {
        try {
            logger.info("CheckPermissionsFilter is running");
            logger.info("request = {} ", JSONUtil.toJsonStr(request));
            String path = request.getPath();
            String username = request.getHeader("username");
            String method = request.getMethod().name();
            if(StrUtil.isBlank(username)){
                response.setStatusCode(403);
                response.setBody("用户不存在");
                chain.filter(request, response);
            }else {
                if(EnforcerFactory.getEnforcer().enforce(username,path,method)){
                    PluginFilter.super.filter(request, response, chain);
                }else {
                    response.setStatusCode(403);
                    response.setBody("权限不够");
                    chain.filter(request, response);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            response.setStatusCode(HttpStatus.HTTP_INTERNAL_ERROR);
            response.setBody(e.getMessage());
            chain.filter(request, response);
        }
    }
}

GatewayApplication

插件启动类,传统的springboot的启动

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication(scanBasePackages = {"com.[根据实际情况调整].gateway", "org.apache.apisix.plugin.runner"})
public class GatewayApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(GatewayApplication.class)
                .web(WebApplicationType.NONE)
                .run(args);
    }
}

MANIFEST.MF

Manifest-Version: 1.0
Main-Class: com.chalcosteering.gateway.GatewayApplication

application.yaml

logging:
  file:
    path: F:\\opt\\applogs
#    path: /usr/local/apisix/permissions/logs
  level:
    root: error
cache.config:
  expired: 3600
  capacity: 1000
#runner.sock
socket:
  file: F:\\opt\\apisix\\runner.sock
#  file: /usr/local/apisix/permissions/runner.sock

redis:
  topic:
    api: 获取API资源的Redis Key
    watcher: Casbin订阅的Redis key
  ip: 192.168.10.182
  port: 23073

casbin:
  conf:
    mode: F:\\opt\\apisix\\model.conf

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.[实际名称].gateway</groupId>
    <artifactId>gateway-plugins</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>2.7.1</spring-boot.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.apisix</groupId>
            <artifactId>apisix-runner-starter</artifactId>
            <version>0.3.0</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.10</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.casbin</groupId>
            <artifactId>jcasbin</artifactId>
            <version>1.49.0</version>
        </dependency>
        <dependency>
            <groupId>org.casbin</groupId>
            <artifactId>jcasbin-redis-watcher</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>org.casbin</groupId>
            <artifactId>redis-adapter</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Redis Casbin权限数据结构

服务器(Linux )插件运行配置

Casbin conf配置文件

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj)

修改Apisix的config.yaml文件

ext-plugin:
  cmd: ['java', '-jar', '-Xmx2g', '-Xms2g', '-Dspring.config.location=/usr/local/apisix/permissions/application.yaml', '/usr/local/apisix/permissions/gateway-plugins-1.0-SNAPSHOT.jar']
  path_for_test: "/usr/local/apisix/permissions/runner.sock"

其中path_for_test  为插件项目yaml文件中socket.file的值

执行 apisix reload指令启动即可

启动成功后,会有对应springboot的启动画面,如下图:

自定义插件的使用

在APISIX的DashBoard的路由配置中,增加如下配置即可启动

  "plugins": {
    "ext-plugin-pre-req": {
      "_meta": {
        "disable": false
      },
      "allow_degradation": true,
      "conf": [
        {
          "name": "check_permissions"
        }
      ]
    }
  }

具体的使用方法可参照官方文档

官方插件介绍

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值