Spring Security中的自定义AccessDecisionVoters

2023/05/17

1. 概述

大多数时候,在保护Spring Web应用程序或REST API时,Spring Security提供的工具已经绰绰有余,但有时我们需要更具体的行为。

在本教程中,我们将编写一个自定义的AccessDecisionVoter,并展示如何使用它来抽象出Web应用程序的授权逻辑,并将其与应用程序的业务逻辑分离。

2. 场景

为了演示AccessDecisionVoter的工作原理,我们将实现一个具有两种用户类型(USER和ADMIN)的场景,其中USER只能在偶数分钟访问系统,而ADMIN将始终被授予访问权限。

3. AccessDecisionVoter实现

首先,我们将描述Spring提供的一些实现,这些实现将与我们的自定义投票器一起参与对授权做出最终决定。我们将了解如何实现自定义投票器。

3.1 默认的AccessDecisionVoter实现

Spring Security提供了几个AccessDecisionVoter实现。我们将在这里使用其中的一些作为我们的安全解决方案的一部分。

让我们来看看这些默认投票者实现如何以及何时投票。

AuthenticatedVoter将根据Authentication对象的身份验证级别进行投票-特别是寻找完全经过身份验证的主体、通过remember-me进行身份验证的主体或匿名的主体。

如果任何配置属性以字符串”ROLE_“开头,则RoleVoter将参与投票。如果是这样,它将在Authentication对象的GrantedAuthority列表中搜索角色。

WebExpressionVoter使我们能够使用SpEL(Spring表达式语言)通过@PreAuthorize注解来授权请求。

例如,如果我们使用Java配置:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    // ...
    .antMatchers("/").hasAnyAuthority("ROLE_USER")
    // ...
}

或者使用XML配置-我们可以在http标签中的intercept-url标签内使用SpEL:

<http use-expressions="true">
    <intercept-url pattern="/" access="hasAuthority('ROLE_USER')"/>
    <!--...-->
</http>

3.2 自定义AccessDecisionVoter实现

现在让我们创建一个自定义投票器-通过实现AccessDecisionVoter接口:

public class MinuteBasedVoter implements AccessDecisionVoter {
    // ...
}

我们必须实现的三个方法中的第一个是vote方法。vote方法是自定义投票器中最重要的部分,也是我们授权逻辑所在的地方。

vote方法可以返回三个可能的值:

  • ACCESS_GRANTED:投票者给出肯定答案
  • ACCESS_DENIED:投票者给出否定答案
  • ACCESS_ABSTAIN:投票者弃权

现在让我们实现vote方法:

@Override
public int vote(Authentication authentication, Object object, Collection collection) {
    return authentication.getAuthorities()
          .stream()
          .map(GrantedAuthority::getAuthority)
          .filter(r -> "ROLE_USER".equals(r) && LocalDateTime.now().getMinute() % 2 != 0)
          .findAny()
          .map(s -> ACCESS_DENIED)
          .orElse(ACCESS_ABSTAIN);
}

在我们的vote方法中,我们检查请求是否来自USER。如果是,并且当前为偶数分钟,我们返回ACCESS_GRANTED,否则,我们返回ACCESS_DENIED。如果请求不是来自USER,我们将弃权并返回ACCESS_ABSTAIN。

第二个方法返回投票器是否支持特定的配置属性。在我们的示例中,投票器不需要任何自定义配置属性,因此我们返回true:

@Override
public boolean supports(ConfigAttribute attribute) {
    return true;
}

第三个方法返回投票器是否可以投票给受保护的对象类型。由于我们的投票器不关心安全对象类型,因此我们返回true:

@Override
public boolean supports(Class clazz) {
    return true;
}

4. AccessDecisionManager

最终授权决策由AccessDecisionManager处理。

AbstractAccessDecisionManager包含一个AccessDecisionVoters列表-它们负责彼此独立地投票。

有三种实现用于处理投票以覆盖最常见的用例:

  • AffirmativeBased(基于肯定):如果任何AccessDecisionVoters给出赞成票,则授予访问权限
  • ConsensusBased(基于共识):如果赞成票多于反对票(忽略弃权的投票器),则授予访问权限
  • UnanimousBased(基于一致性):如果每个投票器都弃权或投赞成票,则授予访问权限

当然,你可以使用自定义决策逻辑实现自己的AccessDecisionManager。

5. 配置

在本教程的这一部分中,我们将了解使用AccessDecisionManager配置自定义AccessDecisionVoter的基于Java和基于XML的方法。

5.1 Java配置

让我们为Spring Web Security创建一个配置类:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
    // ...
}

我们定义一个AccessDecisionManager bean,它使用UnanimousBased管理器和我们自定义的投票器列表:

@Bean
public AccessDecisionManager accessDecisionManager() {
    List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(
          new WebExpressionVoter(),
          new RoleVoter(),
          new AuthenticatedVoter(),
          new MinuteBasedVoter()
    );
    return new UnanimousBased(decisionVoters);
}

最后,让我们配置Spring Security以使用上面定义的bean作为默认的AccessDecisionManager:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf()
          .disable()
          .authorizeRequests()
          .anyRequest()
          .authenticated()
          .accessDecisionManager(accessDecisionManager())
          .and()
          .formLogin()
          .permitAll()
          .and()
          .logout()
          .permitAll()
          .deleteCookies("JSESSIONID")
          .logoutSuccessUrl("/login");
    return http.build();
}

5.2 XML配置

如果使用XML配置,则需要修改spring-security.xml文件(或任何包含安全设置的文件)。

首先,你需要修改<http>标签:

<http access-decision-manager-ref="accessDecisionManager">
    <intercept-url pattern="/**" access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')"/>
    // ...
</http>

接下来,为自定义投票器创建一个bean:

<beans:bean id="minuteBasedVoter" class="cn.tuyucheng.taketoday.roles.voter.MinuteBasedVoter"/>

然后为AccessDecisionManager创建一个bean:

<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
    <beans:constructor-arg>
        <beans:list>
            <beans:bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
            <beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
            <beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
            <beans:bean class="cn.tuyucheng.taketoday.roles.voter.MinuteBasedVoter"/>
        </beans:list>
    </beans:constructor-arg>
</beans:bean>

下面是一个支持我们场景的<authentication-manager>配置:

<authentication-manager>
    <authentication-provider>
        <user-service>
            <user name="user" password="pass" authorities="ROLE_USER"/>
            <user name="admin" password="pass" authorities="ROLE_ADMIN"/>
        </user-service>
    </authentication-provider>
</authentication-manager>

如果你结合使用Java和XML配置,则可以将XML文件导入配置类:

@Configuration
@ImportResource({"classpath:spring-security.xml"})
public class XmlSecurityConfig {
    public XmlSecurityConfig() {
        super();
    }
}

6. 总结

在本教程中,我们研究了一种使用AccessDecisionVoters自定义Spring Web应用程序安全性的方法。我们看到了Spring Security提供的一些投票器,然后我们讨论了如何实现自定义的AccessDecisionVoter。

然后我们讨论了AccessDecisionManager如何做出最终的授权决策,并展示了如何在所有投票器投票后使用Spring提供的实现来做出这个决定。

然后我们通过Java和XML使用AccessDecisionManager配置了一个AccessDecisionVoters列表。

当项目在本地运行时,可以在以下位置访问登录页面:

http://localhost:8082/login

USER的凭据是”user”和”pass”,ADMIN的凭据是”admin”和”pass”。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章