【问题标题】:How to handle the multitenancy problem in Spring Boot applications?如何处理 Spring Boot 应用程序中的多租户问题?
【发布时间】:2021-11-08 19:05:05
【问题描述】:

假设我有一个 SpringBoot 应用程序,具有以下要求。

  1. 它有一个User 类(实体)
  2. 每个用户有零个或多个Workspaces(一对多实体关系)
  3. 每个Workspace 有零个或多个WorkItems(一对多实体关系)

有一个 CRUD REST API 控制器来管理所有实体,即我们有

  1. UserController -> User 实体的 CRUD 操作
  2. WorkspaceController -> Workspace 实体的 CRUD 操作
  3. WorkItemContoller -> WorkItem 实体的 CRUD 操作

现在有一些要求......

  1. User 只能创建/编辑/删除他自己的 Workspace 实体
  2. User 只能在他自己的Workspaces 中创建/编辑/删除WorkItem 实体

此外,假设User 实体与SpringSecurity 集成,并且我们知道控制器和服务中的当前用户。

那么问题是……

实现用户权限检查的最优雅/干净/可维护的方式是什么?我们如何编写代码,在Service 类中将检查用户是否有权对给定资源执行操作。

我现在这样做的方式是有一个像这样的类,它检查每个Service调用中的权限。

class PermissionManager {
   void checkUserAllowedToUseWorkspace(User u, Workspace w);
   void checkUserAlloweToUseWorkitem(User u, WorkItem)
}

如您所见...随着作用域资源数量的增长...此类将变得超级臃肿且难以维护。

是否有人知道以干净和可维护的方式执行此范围资源访问的更好方法?

【问题讨论】:

  • 假设 JPA 和 Hibernate,使用 hibernate 过滤器来限制基于用户本身的查询结果。您可以使用 AOP 启用过滤器以应用自动过滤。然后用户只能看到(因此可以修改)自己的数据。
  • 嗨,@M.Deinum。听起来像是一种很酷的方法来限制基于关联用户实体的存储库输出。你能不能把一篇文章的链接发给我,这篇文章用一个代码示例来说明如何做到这一点?
  • callistaenterprise.se/blogg/teknik/2020/10/17/… 显示了过滤方法。而不是使用的租户,只需编写一个包含您的用户 ID 的查询(您可以使用 SecurityContextHolder 来获取当前用户。
  • 为什么不在每个与该用户无关的数据库查询中添加用户ID,无法执行操作,您可以抛出异常。
  • 您还可以从 SecurityContextHolder 获取用户,您需要在验证用户时放置该用户。

标签: java spring spring-boot multi-tenant


【解决方案1】:

最干净和可维护的解决方案是利用 Spring Security AOP 来完成任务。

您可以使用@PreAuthorize 注释,与您的PermissionManager 服务配对,利用Spring 表达式语言 的强大功能在Controller 级别允许或拒绝访问。

已经定义了一个Service 构造型来检查用户对特定资源(您的示例中的工作区)的访问:

@Service
public class PermissionManagerImpl implements PermissionManager {

    @Autowired
    private UserRepository userRepository;

   /**
   * @param authentication the current authenticated user following your authentication scheme
   * @param workspaceId the workspace (or other resource) identifier
   */
    @Override
    public boolean checkUserAllowedToUseWorkspace(Authentication authentication, Long workspaceId) {
        return authentication != null
                /* check that the `authentication` has access to the argument workspace: e.g. userRepository.findWorkspaceByUserNameAndWorkspaceId(authentication.getName(), workspaceId) != null */;
    }

}

您可以在 Controller 方法上定义基于表达式的控制策略,如下所示:

@RestController
public class WorkspaceController {

    // the DIed `permissionManagerImpl` service will be called prior to your endpoint invocation with the current `authentication` and workspace `id` injected
    @PreAuthorize("@permissionManagerImpl.checkUserAllowedToUseWorkspace(authentication, #id)")
    @RequestMapping("/workspaces/{id}")
    public List<Workspace> getWorkspaces(@PathVariable Long id) {
        // retrieve the user workspaces once authorized
    }
}

这将导致一个可读且可重用的解决方案作用于顶级资源Controller

您可以通过Expression-based Access Control in the official Spring docs了解更多信息。

【讨论】:

  • @PreAuthorize("@permissionManagerImpl.checkUserAllowedToUseWorkspace(authentication, #id)") 所以在这一行Spring会自动注入认证对象?
  • 是的,前提是您的安全配置设置正确。
【解决方案2】:

工作区实体的一些数据库查询:

// DELETE
@Transactional
void deleteByIdAndUserId(String workspaceId, String userId);

userRepository.deleteByIdAndUserId(workspaceId, userId);


// UPDATE
Optional<Workspace> workspace = workspace.findByIdAndUserId(String workspaceId, String userId);

Workspace workspace = workspace.orElseThrow(() -> new RuntimeException("You don't have an workspace under user "))

workspace.setName("MyWorkspace2");

//CREATE
User user = userRepository.findById(SecurityContextHolder...getPriciple().getId())
              .orElseThrow(() -> new RuntimeException("User can not be found by given id"));
workspaceRepository.save(WorkspaceBuilder.builder().user(user).name("firstWorkspace").build());

【讨论】:

  • 然后我在存储库代码中进行所有检查?
猜你喜欢
  • 2021-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-01-25
相关资源
最近更新 更多