权限框架之jcasbin讲解

发布时间 2023-09-04 11:11:05作者: 上善若泪

1 jcasbin

1.1 前言

作为一名后台开发人员,权限这个名词应该算是特别熟悉的了。就算是java里的类也有 publicprivate权限之分。之前项目里一直使用shiro作为权限管理的框架。说实话,shiro的确挺强大的,但是它也有很多不好的地方。shiro默认的登录地址还是login.jsp,前后端分离模式使用shiro还要重写好多类;手机端存储用户信息、保持登录状态等等,对shiro来说也是一个难题。
在分布式项目里,比如电商项目,其实不太需要明确的权限划分,说白了,认为没必要做太麻烦的权限管理,一切从简。何况shiro对于springCloud等各种分布式框架来说,简直就是灾难。每个子系统里都要写点shiro的东西,慢慢的,越来越恶心。zuul网关就在这里大显身手了,控制用户的登录,鉴定用户的权限等等。zuul网关控制用户登录,鉴权以后再详说。
然后最近发现了另一个权限框架jcasbin
github地址:https://github.com/casbin/jcasbin

点击此处了解spring security权限控制使用操作

1.2 工作原理

1.2.1 PERM模型

Casbin 中, 访问控制模型被抽象为基于 PERM (Policy, Effect, Request, Matcher) 的一个文件,描述了资源与用户之间的关系。
PERM :

  • Policy:策略,定义权限的规则, p = {sub, obj, act, eft},策略一般存储到数据库,因为会有很多。
  • Effect:影响,它决定我们是否可以放行。
  • Request:访问请求, r = {sub, obj, act}。它实际上定义了我们应该提供访问控制匹配功能的参数名称和顺序。
    sub, obj, act, eft含义如下:
    • subsubject,访问实体;
    • objobject,访问的资源;
    • actaction,访问方法;
    • efteffect,策略结果,一般为空,默认指定allow,还可以定义为deny
  • Matcher:匹配规则,判断 Request 是否满足 Policy。

1.2.2 Model语法

ModelCasbin的具体访问模型,其主要以文件的形式出现,该文件常常以.conf最为后缀。

Model CONF 至少应包含四个部分: [request_definition], [policy_definition], [policy_effect], [matchers]
如果 model 使用 RBAC, 还需要添加[role_definition]部分。
Model CONF 文件可以包含注释。注释以 #开头, # 会注释该行剩余部分。

1.2.2.1 Request定义

[request_definition]
r = sub, obj, act

[request_definition] 部分用于request的定义,它明确了 e.Enforce(…) 函数中参数的含义。sub, obj, act 表示经典三元组: 访问实体 (Subject),访问资源 (Object) 和访问方法 (Action)

1.2.2.2 Policy定义

[policy_definition]
p = sub, obj, act

[policy_definition] 部分是对policy的定义,policy部分的每一行称之为一个策略规则, 每条策略规则通常以形如p, p2的policy type开头。

1.2.2.3 Policy effect定义

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

[policy_definition] 部分是对policy的定义。上面的策略效果表示如果有任何匹配的策略规则 允许,最终效果是允许 (aka allow-override)
p.eft 是策略的效果,它可以 允许 或 否定。 它是可选的,默认值是 允许。 因为我们没有在上面指定它,所以它使用默认值。
在这个表达式中,some 是一个关键字,表示策略中是否存在符合后续条件的策略项。where 是一个关键字,用于指定条件。eft 属性表示策略的效果,通常用于表示允许访问。

1.2.2.4 角色定义

[role_definition]
g = _, _

这个部分定义了角色的结构
g = _, _ 表示角色使用通配符 _ 表示,表示不关心角色的具体值

1.2.2.5 匹配器

[matchers] 是策略匹配程序的定义。匹配程序是表达式。它定义了如何根据请求评估策略规则。

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

上述匹配器是最简单的,这意味着请求中的主题、对象和行动应该与政策规则中的匹配。

  • g(r.sub, p.sub):表示通过匹配器函数 g 来匹配主体属性 r.sub 和策略项的主体属性 p.sub。这里使用的是通配符 "_",表示不关心具体的角色值。
  • r.obj == p.obj:表示匹配对象属性,要求请求的对象属性 r.obj 和策略项的对象属性 p.obj 相等。
  • r.act == p.act:表示匹配动作属性,要求请求的动作属性 r.act 和策略项的动作属性 p.act 相等。

注意:g()函数和==运算符在匹配器中有不同的作用。

  • g(r.sub, p.sub)g()函数是一个特殊的函数,用于在匹配器中进行角色匹配。在这里,g()函数将匹配请求中的主体属性(r.sub)和策略项中的主体属性(p.sub)。这里的g()函数可以根据需求进行定制,例如使用角色继承关系等。通常情况下,使用通配符_来表示不关心角色的具体值。
  • r.obj == p.obj和r.act == p.act==是相等比较运算符,用于比较请求中的对象属性(r.obj)和策略项中的对象属性(p.obj),以及请求中的动作属性(r.act)和策略项中的动作属性(p.act)。它们要求两边的操作数在值上完全相等才会返回true。
  • g() 函数用于角色匹配,而==运算符用于属性值的比较。这两者在匹配器中的作用是不同的

1.2.2.6 完整model.conf

# Request定义
[request_definition]
r = sub, obj, act

# 策略定义
[policy_definition]
p = sub, obj, act

# 角色定义
[role_definition]
g = _, _

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

# 匹配器定义
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

1.2.3 policy.csv

policy.csv文件就相当于一张权限表。

p, zhangsan, data1, read

1.3 准备

1.3.1 mavan依赖

<dependency>
    <groupId>org.casbin</groupId>
    <artifactId>jcasbin</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.casbin</groupId>
    <artifactId>jdbc-adapter</artifactId>
    <version>2.0.0</version>
</dependency>

1.3.2 配置文件

jcasbin把用户的角色、权限信息(访问路径)放置在配置文件里,然后通过输入流读取配置文件。主要有两个配置文件:model.conf 和 policy.csv。
在这里插入图片描述
其实也可以读取数据库的角色权限配置。所以我们可以把关于数据库的信息提取出来,可以进行动态设置。

@Configuration
@ConfigurationProperties(prefix = "org.jcasbin")
public class EnforcerConfigProperties {
 
  private String url;
  
  private String driverClassName;
  
  private String username;
  
  private String password;
  
  private String modelPath;
 
}

这样我们就可以在application.properties里进行相关配置了。model.conf是固定的文件。policy.csv的内容是可以从数据库读取的。

org.jcasbin.url=jdbc:mysql://localhost:3306/casbin?useSSL=false
org.jcasbin.driver-class-name=com.mysql.cj.jdbc.Driver
org.jcasbin.username=root
org.jcasbin.password=root
# 绝对路径
org.jcasbin.model-path=D:/Work/Project/IDEAWorkSpace/idea2023/swagger-api-demo/conf/authz_model.conf
# 绝对路径
org.jcasbin.policy-path=D:/Work/Project/IDEAWorkSpace/idea2023/swagger-api-demo/conf/policy.csv

1.2.3 读取权限信息进行初始化

我们要对 Enforcer 这个类初始化,加载配置文件里的信息。所以我们写一个类实现InitializingBean,在容器加载的时候就初始化这个类,方便后续的使用。

@Component
public class EnforcerFactory implements InitializingBean {
 
  private static Enforcer enforcer;
 
  @Autowired
  private EnforcerConfigProperties enforcerConfigProperties;
  private static EnforcerConfigProperties config;
  
  @Override
  public void afterPropertiesSet() throws Exception {
    config = enforcerConfigProperties;
    //从数据库读取策略
    //JDBCAdapter jdbcAdapter = new JDBCAdapter(config.getDriverClassName(),config.getUrl(),config.getUsername(),config.getPassword());
    //enforcer = new Enforcer(config.getModelPath(), jdbcAdapter);
    //从配置文件读取策略
    enforcer = new Enforcer(config.getModelPath(), config.getPolicyPath());
    enforcer.loadPolicy();//Load the policy from DB.
  }
  
  /**
   * 添加权限
   * @param policy
   * @return
   */
  public static boolean addPolicy(Policy policy){
    boolean addPolicy = enforcer.addPolicy(policy.getSub(),policy.getObj(),policy.getAct());
    enforcer.savePolicy();
    
    return addPolicy;
  }
  
  /**
   * 删除权限
   * @param policy
   * @return
   */
  public static boolean removePolicy(Policy policy){
    boolean removePolicy = enforcer.removePolicy(policy.getSub(),policy.getObj(),policy.getAct());
    enforcer.savePolicy();
    
    return removePolicy;
  }
  
  public static Enforcer getEnforcer(){
    return enforcer;
  }
  
}

在这个类里,我们注入写好的配置类,然后转为静态的,在afterPropertiesSet方法里实例化Enforcer并加载policy(策略,角色权限/url对应关系)。

同时又写了两个方法,用来添加和删除policy,Policy是自定的一个类,对官方使用的集合/数组进行了封装。

public class Policy {
  /**想要访问资源的用户 或者角色*/
  private String sub;
  
  /**将要访问的资源,可以使用 * 作为通配符,例如/user/* */
  private String obj;
  
  /**用户对资源执行的操作。HTTP方法,GET、POST、PUT、DELETE等,可以使用 * 作为通配符*/
  private String act;
 
}

1.4 使用

1.4.1 权限控制

jcasbin的权限控制非常简单,自定义一个过滤器,if判断就可以搞定,没错,就这么简单。

@WebFilter(urlPatterns = "/*" , filterName = "JCasbinAuthzFilter")
@Order(Ordered.HIGHEST_PRECEDENCE)//执行顺序,最高级别最先执行,int从小到大
public class JCasbinAuthzFilter implements Filter {
  
  private static final Logger log = LoggerFactory.getLogger(JCasbinAuthzFilter.class);
 
  private static Enforcer enforcer;
 
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
  }
 
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
      throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        
        String user = request.getParameter("username");
        String path = request.getRequestURI();
        String method = request.getMethod();
 
        enforcer = EnforcerFactory.getEnforcer();
        if (path.contains("anon")) {
          chain.doFilter(request, response);
    }else if (enforcer.enforce(user, path, method)) {
      chain.doFilter(request, response);
    } else {
      log.info("无权访问");
      Map<String, Object> result = new HashMap<String, Object>();
            result.put("code", "1001");
            result.put("msg", "用户权限不足");
            result.put("data",null);
            response.setCharacterEncoding("UTF-8");
            response.setContentType(application/json);
            response.getWriter().write(JSONObject.toJSONString(result,SerializerFeature.WriteMapNullValue));
      }
    
  }
 
  @Override
  public void destroy() {
    
  }
 
}

主要是用 enforcer.enforce(user, path, method) 这个方法对用户、访问资源、方式进行匹配。这里的逻辑可以根据自己的业务来实现。在这个过滤器之前还可以添加一个判断用户是否登录的过滤器。

1.4.2 添加删除权限

对于权限的操作,直接调用上面写好的 EnforcerFactory 里对应的方法即可。并且,可以达到同步的效果。就是不用重启服务器或者其他任何操作,添加或删除用户权限后,用户对应的访问就会收到影响。


@RestController
public class PerController {

    @PutMapping("/anon/role/per")
    public String addPer(){
        EnforcerFactory.addPolicy(new Policy("alice", "/user/list", "*"));
        return "OK";
    }

    @DeleteMapping("/anon/role/per")
    public String deletePer(){
        EnforcerFactory.removePolicy(new Policy("alice", "/user/list", "*"));
        return "OK";
    }
}