业务系统中基于角色的访问控制RBAC

RBAC 基础知识

业务系统权限控制的基本形式是基于角色的访问控制(RBAC)。简化后,模型如下所示:

 

  • 主题(系统用户)有多个角色。
  • 一个角色由一组权限组成。
  • 权限由一组操作组成

具体来说,让我们看一下Redmine中的一个例子。

  • 默认情况下,可以为用户分配“管理员”、“开发者”和“报告者”角色。
  • “报告者”角色具有“添加问题”权限。
  • 具有“添加问题”权限的用户可以“创建新问题”。

该模型在 Redmine 中表示如下。

 

Redmine 允许将单个用户分配给具有不同角色的多个项目,因此模型是将上面的 Member 实体夹在中间。通过少量应用 RBAC 原则,您可以以有组织的思维方式适应复杂的业务需求。
我没有看到权限表,但它们在角色表的权限列中以逗号分隔。这就是J-Walk所谓的 SQL 反模式设计。我想这是Redmine插件端可以自由添加权限的借口。

RBAC 看起来是一个非常合理的设计,但似乎很少有人真正看到这样设计的业务系统。相信一定有人对一个让组织=角色或角色=权限的设计有想法。此外,似乎很少有框架能够正确支持 RBAC。

不针对 RBAC 进行设计时的问题

组织=角色

您可以想象为什么这不是一个好主意,组织经常变化。如果这是一个角色,每次都会需要大量的数据维护。客户经常通过组织名称向我们询问类似角色的要求,但请务必设计一个单独的角色并将其与组织映射。

角色 = 特权

没有滚动等效的模式。为用户分配权限可能会让人不知所措。由于我们还没有找到一组称为角色的业务权限,如果我们想要满足 C2 覆盖,我们将需要测试尽可能多的权限组合。

特权=操作

一个权限可以控制多个操作。如后面将要描述的,拥有一定的权限A不仅允许访问某个URL,而且它还可以用于不同的链接和按钮来到达那个URL。让我们将您想要控制权限的操作分组并将它们设计为权限。

用Java实现

Java 在 Servlet API 中也有一个角色机制,可以在业务应用程序中使用。但是...角色通常需要灵活的操作。另一个常见的要求是管理员希望能够创建和删除新的。然后,像 JSR250 和 Servlet API 一样,将角色名称附加到注解中,或者像 isUserInRole 这样将其作为参数传递,ロール名过于死板和不方便。根据 RBAC 的定义,自然能够传递“特权”。

因此,我创建了一个基于 Ninjaframework 的应用程序示例,它使用 JSR250 注解并使用权限名称而不是角色名称作为要传递给它们的参数。

示例说明

作为初始数据,准备了三种角色,并赋予以下权限。

权威
行政人员读问题、写问题、管理用户
开发商读问题,写问题
来宾阅读问题

登录时获取授权

一个现实的设计是在登录时从用户的角色中获取权限,并将它们与用户信息一起存储。因此,会话作为登录信息保存的 DTO 实现如下。

@Data
@RequiredArgsConstructor
public class UserPrincipal implements Principal {
    @NonNull
    private String name;
    @NonNull
    private Set<String> permissions;
}

在登录时,这将转换为权限并提供给会话。

User user = em
    .createQuery("SELECT u FROM User u LEFT JOIN FETCH u.roles r LEFT JOIN FETCH r.permissions p WHERE u.account = :account", User.class)
    .setParameter("account", account)
    .getSingleResult();

    session.put("account", account);
    Set<Permission> permissions = new HashSet<>();
    user.getRoles().stream()
        .map(Role::getPermissions)
        .forEach(permissionsInRole -> permissions.addAll(permissionsInRole));

    session.put("permissions", permissions.stream().map(Permission::getName)
        .collect(Collectors.joining(",")));

Ninjaframework 会话存储在 cookie 中,因此应该只输入诸如 ↑ 之类的字符串(这就是不使用 Ninjaframework 的原因)。

方法权限

然后,在调度到控制器方法时,我们检查我们拥有的权限以及附加到操作方法的权限。

从身份验证过滤器中的会话恢复 UserPrincipal。

Session session = context.getSession();
if (session.get("account") == null) {
    return Results.redirect("/login");
}
Set<String> permissions = new HashSet<>();

if (session.get("permissions") != null) {
    permissions = ImmutableSet.copyOf(Splitter.on(',').split(session.get("permissions")));
}
context.setAttribute("principal", new UserPrincipal(session.get("account"), permissions));

然后授权过滤器可以检查 UserPrincipal 是否有权访问该方法。假设需要的权限写在控制器方法中如下所示的JSR250注解中。

    @UnitOfWork
    @RolesAllowed("readIssue")
    public Result list(Context context) {
        EntityManager em = entityManagerProvider.get();
        List<Issue> issues = em
                .createQuery("SELECT i FROM Issue i", Issue.class)
                .getResultList();

        Map<String, Object> bindings = VariablesHelper.create(context);
        bindings.put("issues", issues);
        return Results.html().render(bindings);
    }

    @UnitOfWork
    @RolesAllowed("writeIssue")
    public Result newIssue(Context context) {
        return Results.html().render(VariablesHelper.create(context));
    }

授权检查如下所示:

Method method = context.getRoute().getControllerMethod();
RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
PermitAll permitAll = method.getAnnotation(PermitAll.class);

UserPrincipal principal = (UserPrincipal) context.getAttribute("principal");

if (permitAll != null ||
        (rolesAllowed != null && contains(principal.getPermissions(), rolesAllowed.value()))) {
    return filterChain.next(context);
} else {
    return Results.forbidden().html().template("views/403forbidden.ftl.html");
}

现在您所要做的就是如上所述注释您的控制器方法。

在视图层按权限分发

这不是使用权限的唯一方法,但它也将用于区分菜单和按钮。您还可以检查您是否拥有来自 UserPrincipal 的必要权限,并在视图端单独发布它们。

Ninjaframework 的默认视图是 Freemarker,所以你可以这样写:

<a href="#" class="header item">
    RBAC Example
</a>
<#if principal??>
    <a href="/" class="item">Home</a>
</#if>
<#if principal?? && principal.permissions?seq_contains("readIssue")>
    <a href="/issues/" class="item">Issue</a>
</#if>
<#if principal?? && principal.permissions?seq_contains("manageUser")>
    <a href="/users/" class="item">Users</a>
</#if>

当您拥有“管理员”权限时

当您没有“管理员”权限时

我能够像这样整理出来。

如何运行示例

% git clone https://github.com/kawasima/rbac-example.git
% cd rbac-example
% mvn compile
% mvn waitt:run

您可以使用 .
哦,这个叫waitt的插件是2015年最火的插件之一,不用Spring Boot或者IDE的付费功能,不用打包web应用就可以启动。请尝试一下。

春季安全

Spring security 允许您实现 RBAC。但是,由于篇幅限制,我将省略它。

概括

尽管权限控制是业务系统中最常见的需求,但很难找到任何可以清楚地解释如何设计它的东西,而现实是有很多系统被设计成难以测试和操作的想法。我们希望本文对您的设计有所帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值