前言介绍
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
项目启动,加载资源权限,以及增加订阅,具体查看
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整合成一个类,具体可参考
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"
}
]
}
}
具体的使用方法可参照官方文档