Spring Security中基于接口的权限控制

最近接触Spring Security这个东西,想使用Spring Security+JWT+Oauth2实现基于接口的权限控制,看了网上的一些例子,一般都是在方法上加上:

@PreAuthorize("hasRole('ADMIN')")

或者

@PreAuthorize("hasAuthority('ROLE_ADMIN')")

这样如果当前用户具有ADMIN角色,则可以访问该接口。这种情况一般涉及到两个类Role和User,代码大致如下:

@Entity
public class User implements UserDetails, Serializable {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   @Column(nullable = false,  unique = true)
   private String username;

   @Column
   private String password;

   @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
   @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
         inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
   private List<Role> authorities;

   public User() {
   }

   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
      return authorities;
   }

   public void setAuthorities(List<Role> authorities) {
      this.authorities = authorities;
   }

   ......省略各种get,set接口
}

@Entity
 public class Role implements GrantedAuthority {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;

  @Column(nullable = false)
  private String name;

  @Override
  public String getAuthority() {
     return name;
  }
  
  ......省略各种get,set接口
}

当我们在接口上加标注:@PreAuthorize("hasRole('ADMIN')")时,流程大概是这样:

先会去调用到User类的getAuthorities接口,取出authorities,类型为List<Role> ,然后调用每个Role实例的getAuthority接口,该接口返回Role名称,比如“ADMIN”,只要其中某个Role返回了“ADMIN”,即可停止遍历,表示当前用户具备了访问该接口的权限,放行。

这样在数据库里面就会有user、role、user_role表,分表存储用户信息、角色信息以及用户和角色关联(多对多)信息。如果每个接口对应一个role,那实际上作为角色的role在我们看来跟以往的权限(permission)对应了,即实际上是user和permission的关系,只是叫做role罢了。于是就会在所有具有ADMIN角色才能访问的接口上加上标注:@PreAuthorize("hasRole('ADMIN')")

这样的话,我们在编写接口代码的时候,就要把这个标注写上去,让具备ADMIN角色的用户可以访问之,那如果某天我不想让ADMIN用户访问这个接口呢,我该怎么办?要么我需要回收该用户的ADMIN角色,要么我得去修改接口标注。如果该角色确实只对应一个接口的权限,那回收倒是没有问题,但Boss要你实现一个角色拥有多个权限,实际上会在多个接口上做了同样的标注,回收角色后你会发现其他一些本来该用户可以访问的接口现在访问不了了,头大吧,想来想去你只能去改代码了,去修改某个特定接口的标注。

基于RBAC的权限控制系统里面,一个用户可以有多个角色,一个角色有多个权限,因此我们最好是按照RBAC的方式来做,这样的话,一个接口就对应一个权限,一个角色具有N个权限,一个用户可以有M个角色。这个也好理解,不过怎么实现修改用户角色、权限的时候不需要修改接口标注呢?一般来说,这种修改都是管理员通过操作后台管理界面来实现的。

我们这里的方法就是添加permission表和role_permission,即权限表、角色权限关系表。权限类的代码很简单,跟数据库字段对应,不过你得注意下,这个类跟Role类一样,都实现了GrantedAuthority接口:

@Entity
public class Permission implements GrantedAuthority {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Integer id;

   @Column(nullable = false)
   private String name;
 
   @Override
   public String getAuthority() {
     return name;
   }
 ......省略各种get,set接口
}

之前提到过,是否具有访问接口权限是由Role类的getAuthority的返回值来判断的,这些Role实例又是从User对象的getAuthorities接口获取的,那么我们就想到,我们通过修改User类的getAuthorities接口,让它直接返回List <Permission>而不是List <Role>,getAuthorities接口的原型是返Collection<? extends GrantedAuthority>,因为Permission和Role类都实现了GrantedAuthority的接口,因此返回Permission和Role列表都能满足要求。于是Role类改成这样:

@Entity
public class Role implements GrantedAuthority {

@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "role_permission", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"),
         inverseJoinColumns = @JoinColumn(name = "permission_id", referencedColumnName = "id"))
private List<Permission> authorities;
 
  //Add new method
  public List<Permission> getAuthorities() {
   return authorities;
  }

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Integer id;

 @Column(nullable = false)
 private String name;

 @Override
 public String getAuthority() {
    return name;
 }

  ......省略各种get,set接口

}

User类改成:

@Entity 
public class User implements UserDetails, Serializable {
......


@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
   //Translate Roles to Permissions (for better METHOD security)
       List<Permission>  permission_authorities = new ArrayList<>();
       for (Role role: authorities ) {
           for (Permission permission: role.getAuthorities())
           {
               permission_authorities.add(permission);
           }
       }
       return  permission_authorities;
       //return authorities;
  }
  .....省略各种get,set接口
}

这样最终会调用到的是Permission类的getAuthority接口而不是Role类的接口。这样一来,我们就可以规定,每个接口的权限名称、对应的数据库里面的权限名称、接口函数名称这三者相同,只要遵守这个约定去写接口标注和接口名称,从界面添加接口对应的权限时也使用同样的名称,就可以达到无论如何修改用户和角色、权限关系时,都不需要修改代码标注,而且实现了RBAC方式的权限控制系统。示例代码如下:

发表评论

电子邮件地址不会被公开。