vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》自定义授权

自定义授权

在本章中,我们将为 Spring Security 的密钥授权 API 编写一些自定义实现。完成此操作后,我们将使用对自定义实现的理解来了解 Spring Security 的授权架构是如何工作的。

在本章中,我们将涵盖以下主题:

  • Gaining an understanding of how authorization works
  • Writing a custom SecurityMetaDataSource backed by a database instead of antMatchers() methods
  • Creating a custom SpEL expression
  • Implementing a custom PermissionEvaluator object that allows our permissions to be encapsulated

授权请求

在身份验证过程中,Spring Security 提供了一个 o.s.s.web.access.intercept.FilterSecurityInterceptor servlet 过滤器,它负责决定是否接受或拒绝特定请求。在调用过滤器时,主体已经通过身份验证,因此系统知道有效用户已登录;请记住,我们实现了 List<GrantedAuthority> getAuthorities() 方法,返回主体的权限列表,在 第 3 章,自定义身份验证。通常,授权过程将使用来自此方法的信息(由 Authentication 接口定义)来确定,对于特定请求,该请求是否应该被允许。

请记住,授权是一个二元决策——用户要么有权访问受保护的资源,要么没有。在授权方面没有歧义。

智能的面向对象设计在 Spring Security 框架中无处不在,授权决策管理也不例外。

在 Spring Security 中,o.s.s.access.AccessDecisionManager 接口指定了两个简单且合乎逻辑的方法,它们可以明智地适应请求的处理决策流程,如下所示:

  • Supports: This logical operation actually comprises two methods that allow the AccessDecisionManager implementation to report whether or not it supports the current request.
  • Decide: This allows the AccessDecisionManager implementation to verify, based on the request context and security configuration, whether or not access should be allowed and the request accepted. The Decide method actually has no return value, and instead reports the denial of a request by throwing an exception to indicate rejection.

特定类型的异常可以进一步规定应用程序为解决授权决定而采取的行动。 o.s.s.access.AccessDeniedException 接口是授权领域中最常见的异常,值得过滤器链进行特殊处理。

AccessDecisionManager 的实现可以使用标准 Spring bean 绑定和引用完全配置。默认的 AccessDecisionManager 实现提供了基于 AccessDecisionVoter 和投票聚合的访问授权机制。

投票者是授权序列中的参与者,其工作是评估以下任何或所有事物:

  • The context of the request for a secured resource (such as a URL requesting an IP address)
  • The credentials (if any) presented by the user
  • The secured resource being accessed
  • The configuration parameters of the system, and the resource itself

AccessDecisionManager 实现还负责将请求的资源的访问声明(在代码中称为 o.s.s.access.ConfigAttribute 接口的实现)传递给投票者。在 Web URL 的情况下,投票者将获得有关资源访问声明的信息。如果我们查看我们非常基本的配置文件的 URL 拦截声明,我们会看到 ROLE_USER 被声明为用户尝试访问的资源的访问配置,如下所示:

    .antMatchers("/**").hasRole("USER");

根据选民的知识,它将决定用户是否应该有权访问资源。 Spring Security 允许投票者做出三个决定之一,其逻辑定义映射到接口中的常量,如下表所示:

决策类型

说明

授予 (ACCESS_GRANTED)

选民建议授予对资源的访问权限。

拒绝 (ACCESS_DENIED)

投票者建议拒绝访问该资源。

弃权 (ACCESS_ABSTAIN)

投票者对资源的访问权投弃权票(不做决定)。这可能由于多种原因而发生,例如:

  • The voter doesn't have conclusive information
  • The voter can't decide on a request of this type

正如您可能已经从访问决策相关的对象和接口的设计中猜到的那样,Spring Security 的这一部分被设计为可以应用于非 Web 域中的身份验证和访问控制场景。当我们在本章后面讨论方法级别的安全性时,我们会遇到选民和访问决策管理器。

当我们把这些放在一起时,Web 请求的默认授权检查的整体流程类似于下图:

读书笔记《spring-security-third-edition》自定义授权

我们可以看到 ConfigAttribute 的抽象允许将数据从配置声明(保留在 o.s.s.web.access.intercept.DefaultFilterinvocationSecurityMetadataSource 接口中)传递给负责的投票者用于在 ConfigAttribute 上进行操作,而无需任何干预类需要了解 ConfigAttribute 的内容。这种关注点分离为构建新类型的安全声明(例如我们将在方法安全中看到的声明)提供了坚实的基础,同时使用了相同的访问决策模式。

访问决策聚合的配置

Spring Security 确实允许在安全命名空间中配置 AccessDecisionManager<http> 元素上的 access-decision-manager-ref 属性允许您指定对 AccessDecisionManager 实现的 Spring bean 引用. Spring Security 附带了这个接口的三个实现,都在 o.s.s.access.vote 包中,如下所示:

类名

说明

基于肯定的

如果任何选民授予访问权限,则立即授予访问权限,而不管之前的拒绝如何。

基于共识

多数票(授予或拒绝)支配 AccessDecisionManager 的决定。平局和空票(仅包含弃权)的处理是可配置的。

UnanimousBased

所有选民都必须授予访问权限,否则访问将被拒绝。

配置基于一致的访问决策管理器

如果我们想修改我们的应用程序以使用访问决策管理器,我们需要进行两次修改。为此,我们将 accessDecisionManager 条目添加到 SecurityConfig.java 文件中的 http 元素中,如下所示:

    //src/main/java/com/packtpub/springsecurity/configuration/
    SecurityConfig.java

    http.authorizeRequests()
         .anyRequest()
         .authenticated()
         .accessDecisionManager(accessDecisionManager());

这是一个标准的 Spring bean 引用,因此它应该对应于 bean 的 id 属性。然后我们可以定义 UnanimousBased bean,如以下代码片段所示。请注意,我们不会在练习中实际使用此配置:

//src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java

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

   return new UnanimousBased(decisionVoters);
}

您可能想知道 decisionVoters 属性是关于什么的。这个属性是自动配置的,直到我们声明了我们自己的 AccessDecisionManager。默认的 AccessDecisionManager 类要求我们声明投票者列表,这些投票者会被咨询以做出身份验证决策。这里列出的两个投票者是安全命名空间配置提供的默认值。

Spring Security 并没有提供各种各样的选民,但实现一个新的选民将是微不足道的。正如我们将在本章后面看到的那样,在大多数情况下,不需要创建自定义投票器,因为这通常可以使用自定义表达式甚至自定义 o.s.s.access.PermissionEvaluator 来实现。

我们在这里引用的两个选民实现如下:

类名

说明

示例

o.s.s.access.vote.RoleVoter

检查用户是否具有匹配的声明角色。期望该属性定义以逗号分隔的名称列表。前缀是预期的,但可以选择配置。

access="ROLE_USER,ROLE_ADMIN"

o.s.s.access.vote.AuthenticatedVoter

支持允许通配符匹配的特殊声明:

  • IS_AUTHENTICATED_FULLY allows access if a fresh username and password are supplied.
  • IS_AUTHENTICATED_REMEMBERED allows access if the user has authenticated with the remember-me functionality.
  • IS_AUTHENTICATED_ANONYMOUSLY allows access if the user is anonymous

access="IS_AUTHENTICATED_ANONYMOUSLY"

基于表达式的请求授权

如您所料,SpEL 处理由不同的 Voter 实现提供,o.s.s.web.access.expression.WebExpressionVoter,它了解如何评估 SpEL 表达式。 WebExpressionVoter 类为此目的依赖于 SecurityExpressionHandler 接口的实现。 SecurityExpressionHandler 接口负责评估表达式和提供表达式中引用的特定于安全性的方法。此接口的默认实现公开了 o.s.s.web.access.expression.WebSecurityExpressionRoot 类中定义的方法。

这些类之间的流程和关系如下图所示:

读书笔记《spring-security-third-edition》自定义授权

现在我们知道了如何请求授权,让我们通过一些关键接口的自定义实现来巩固我们的理解。

自定义请求授权

Spring Security 授权的真正强大之处在于它对自定义需求的适应性。让我们探索一些有助于加强我们对整体架构的理解的场景。

动态定义对 URL 的访问控制

Spring Security 提供了几种将 ConfigAttribute 对象映射到资源的方法。例如,antMatchers() 方法确保开发人员可以轻松地限制对其 Web 应用程序中特定 HTTP 请求的访问。在幕后,o.s.s.acess.SecurityMetadataSource 的实现由这些映射填充并被查询以确定需要什么才能被授权发出任何给定的 HTTP 请求。

虽然 antMatchers() 方法非常简单,但有时可能需要提供一种自定义机制来确定 URL 映射。这方面的一个例子可能是应用程序需要能够动态地提供访问控制规则。让我们演示如何将我们的 URL 授权配置移动到数据库中。

配置 RequestConfigMappingService

第一步是能够从数据库中获取必要的信息。这将替换从我们的安全 bean 配置中读取 antMatchers() 方法的逻辑。为了做到这一点,本章的示例代码包含 JpaRequestConfigMappingService,它将从表示为 RequestConfigMapping 的数据库中获取 ant 模式和表达式的映射。比较简单的实现如下:

    // src/main/java/com/packtpub/springsecurity/web/access/intercept/
   JpaRequestConfigMappingService.java

    @Repository("requestConfigMappingService")
    public class JpaRequestConfigMappingService
    implements RequestConfigMappingService {
       @Autowired
   private SecurityFilterMetadataRepository securityFilterMetadataRepository;


   @Autowired
   public JpaRequestConfigMappingService(
           SecurityFilterMetadataRepository sfmr
   ) {
       this.securityFilterMetadataRepository = sfmr;
   }


   @Override
   public List<RequestConfigMapping> getRequestConfigMappings() {
       List<RequestConfigMapping> rcm =
           securityFilterMetadataRepository
               .findAll()
               .stream()
               .sorted((m1, m2) -> {
               return m1.getSortOrder() - m2.getSortOrder()
               })
               .map(md -> {
                   return new RequestConfigMapping(
                            new AntPathRequestMatcher 
                             (md.getAntPattern()),
                             new SecurityConfig 
                             (md.getExpression()));
              }).collect(toList());
       return rcm;
   }
}

重要的是要注意,就像 antMatchers() 方法一样,顺序很重要。因此,我们确保结果按 sort_order 列排序。该服务创建一个 AntRequestMatcher 并将其与 SecurityConfig 相关联,该实例是 ConfigAttribute 的一个实例。这将提供 HTTP 请求到 ConfigAttribute 对象的映射,Spring Security 可以使用这些对象来保护我们的 URL。

我们需要创建一个域对象以供 JPA 映射到,如下所示:

// src/main/java/com/packtpub/springsecurity/domain/SecurityFilterMetadata.java

@Entity
@Table(name = "security_filtermetadata")
public class SecurityFilterMetadata implements Serializable {


   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Integer id;
   private String antPattern;
   private String expression;
   private Integer sortOrder;


... setters / getters ...
}

最后,我们需要创建一个 Spring Data 存储库对象,如下所示:

    // src/main/java/com/packtpub/springsecurity/repository/
    SecurityFilterMetadataRepository.java

   public interface SecurityFilterMetadataRepository
   extends JpaRepository<SecurityFilterMetadata, Integer> {}

为了使新服务正常工作,我们需要使用模式和访问控制映射来初始化我们的数据库。就像服务实现一样,我们的模式相当简单:

// src/main/resources/schema.sql

...
create table security_filtermetadata (
 id         INTEGER GENERATED BY DEFAULT AS IDENTITY,
 ant_pattern VARCHAR(1024) NOT NULL unique,
 expression VARCHAR(1024) NOT NULL,
 sort_order INTEGER NOT NULL,
 PRIMARY KEY (id) 
);

然后,我们可以使用 SecurityConfig.java 文件中的相同 antMatchers() 映射来生成 schema.sql 文件:

// src/main/resources/data.sql

-- Security Filter Metadata --

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (110, '/admin/h2/**','permitAll',10);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (115, '/','permitAll',15);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (120, '/login/*','permitAll',20);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (140, '/logout','permitAll',30);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (130, '/signup/*','permitAll',40);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (150, '/errors/**','permitAll',50);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (160, '/admin/**','hasRole("ADMIN")',60);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (160, '/events/','hasRole("ADMIN")',60);

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (170, '/**','hasRole("USER")',70);
此时,您的代码 应该以 chapter13.00-calendar 开头。

自定义 SecurityMetadataSource 实现

为了让 Spring Security 知道我们的 URL 映射,我们需要提供一个自定义的 FilterInvocationSecurityMetadataSource 实现。 FilterInvocationSecurityMetadataSource 包扩展了 SecurityMetadataSource 接口,给定特定的 HTTP 请求,该接口为 Spring Security 提供了确定是否应授予访问权限所需的信息。让我们看看如何利用我们的 RequestConfigMappingService 接口来实现 SecurityMetadataSource 接口:

    //src/main/java/com/packtpub/springsecurity/web/access/intercept/
    FilterInvocationServiceSecurityMetadataSource.java

    @Component("filterInvocationServiceSecurityMetadataSource")
    public class FilterInvocationServiceSecurityMetadataSource implements
    FilterInvocationSecurityMetadataSource, InitializingBean{
           … constructor and member variables omitted ...


       public Collection<ConfigAttribute> getAllConfigAttributes() {
           return this.delegate.getAllConfigAttributes();
       }


       public Collection<ConfigAttribute> getAttributes(Object object) {
           return this.delegate.getAttributes(object);
       }


       public boolean supports(Class<?> clazz) {
           return this.delegate.supports(clazz);
       }


       public void afterPropertiesSet() throws Exception {
       List<RequestConfigMapping> requestConfigMappings =
       requestConfigMappingService.getRequestConfigMappings();
       LinkedHashMap requestMap = new 
       LinkedHashMap(requestConfigMappings.size());
       for(RequestConfigMapping requestConfigMapping 
       requestConfigMappings) {
           RequestMatcher matcher = 
               requestConfigMapping.getMatcher();
           Collection<ConfigAttribute> attributes =
                   requestConfigMapping.getAttributes();
           requestMap.put(matcher,attributes);
       }
           this.delegate =
           new 
           ExpressionBasedFilterInvocationSecurityMetadataSource
          (requestMap,expressionHandler);
       }
    }

我们可以使用我们的 RequestConfigMappingService 接口来创建映射到 ConfigAttribute 对象的 RequestMatcher 对象的映射。然后我们委托 ExpressionBasedFilterInvocationSecurityMetadataSource 的实例来完成所有工作。为简单起见,当前的实现将需要重新启动应用程序以获取更改。但是,通过一些小的更改,我们可以避免这种不便。

注册自定义 SecurityMetadataSource

现在,剩下的就是让我们配置FilterInvocationServiceSecurityMetadataSource。 唯一的问题是Spring Security 不支持直接配置自定义的FilterInvocationServiceSecurityMetadataSource 接口。这并不太难,因此我们将在 SecurityConfig 文件中将这个 SecurityMetadataSource 注册到我们的 FilterSecurityInterceptor 中:

    // src/main/java/com/packtpub/springsecurity/configuration/
    SecurityConfig.java

   @Override
    public void configure(final WebSecurity web) throws Exception {
       ...
       final HttpSecurity http = getHttp();
       web.postBuildAction(() -> {
       FilterSecurityInterceptor fsi = http.getSharedObject
       (FilterSecurityInterceptor.class);
       fsi.setSecurityMetadataSource(metadataSource);
       web.securityInterceptor(fsi);
       });
    }

这设置了我们的自定义 SecurityMetadataSource 接口,其中 FilterSecurityInterceptor 对象作为默认元数据源。

删除我们的 antMatchers() 方法

现在数据库正在用于映射我们的安全配置,我们可以从 SecurityConfig.java 文件中删除 antMatchers() 方法。继续删除它们,使配置看起来类似于以下代码片段:

    // src/main/java/com/packtpub/springsecurity/configuration/
    SecurityConfig.java

    @Override
    protected void configure(HttpSecurity http) throws Exception {


    // No interceptor methods
    // http.authorizeRequests()
    //     .antMatchers("/").permitAll()
         ...


    http.formLogin()
         ...


    http.logout()
         ...
如果你甚至使用一个 http antMatchers 表达式,则不会调用自定义表达式处理程序。

您现在应该能够启动应用程序并进行测试,以确保我们的 URL 得到应有的保护。我们的用户不会注意到差异,但我们知道我们的 URL 映射现在保存在数据库中。

您的代码现在应该看起来像 第 13.01 章-日历

创建自定义表达式

o.s.s.access.expression.SecurityExpresssionHandler 接口是 Spring Security 抽象 Spring 表达式的创建和初始化方式的方式。就像 SecurityMetadataSource 接口一样,有一个实现用于为 Web 请求创建表达式并为保护方法创建表达式。在本节中,我们将探讨如何轻松添加新表达式。

配置自定义 SecurityExpressionRoot

假设我们想要支持一个名为 isLocal 的自定义 Web 表达式,如果主机是 localhost,它将返回 true,否则返回 false。这种新方法可用于为我们的 SQL 控制台提供额外的安全性,确保它只能从部署 Web 应用程序的同一台机器上访问。

这是一个人为的示例,它没有增加任何安全优势,因为主机来自 HTTP 请求的标头。这意味着恶意用户可以注入一个标明主机是 localhost 的标头,即使他们正在请求外部域。

我们看到的所有表达式都是可用的,因为 SecurityExpressionHandler 接口通过 o.s.s.access.expression.SecurityExpressionRoot 的实例使它们可用。打开这个对象,你会发现我们在 Spring 表达式中使用的方法和属性(即 hasRolehasPermission 等),这两种方法都是通用的网络和方法安全。子类提供特定于 Web 和方法表达式的方法。例如,o.s.s.web.access.expression.WebSecurityExpressionRoot 为 Web 请求提供了 hasIpAddress 方法。

要创建自定义 Web SecurityExpressionhandler,我们首先需要创建 WebSecurityExpressionRoot 的子类,该子类定义我们的 isLocal 方法,如下所示:

    //src/main/java/com/packtpub/springsecurity/web/access/expression/
    CustomWebSecurityExpressionRoot.java

    public class CustomWebSecurityExpressionRoot extends
     WebSecurityExpressionRoot {

      public CustomWebSecurityExpressionRoot(Authentication a, 
      FilterInvocation fi) {
       super(a, fi);
       }


      public boolean isLocal() {
            return "localhost".equals(request.getServerName());
       }
   }
需要注意的是 getServerName() 返回在 Host 标头值。这意味着恶意用户可以将不同的值注入到标头中以绕过约束。但是,大多数应用程序服务器和代理可以强制执行 Host 标头。请在利用这种方法之前阅读相应的文档,以确保恶意用户不会注入 Host 标头值以绕过此类约束。

配置自定义 SecurityExpressionHandler

为了使我们的新方法可用,我们需要创建一个使用我们新根对象的自定义 SecurityExpressionHandler 接口。这就像扩展 WebSecurityExpressionHandler 一样简单,如下:

    //src/main/java/com/packtpub/springsecurity/web/access/expression/
    CustomWebSecurityExpressionHandler.java

    @Component
    public class CustomWebSecurityExpressionHandler extends  
           DefaultWebSecurityExpressionHandler {
       private final AuthenticationTrustResolver trustResolver =
       new AuthenticationTrustResolverImpl();

       protected SecurityExpressionOperations
       createSecurityExpressionRoot(Authentication authentication, 
       FilterInvocation fi)    
    {
          WebSecurityExpressionRoot root = new 
          CustomWebSecurityExpressionRoot(authentication, fi);
           root.setPermissionEvaluator(getPermissionEvaluator());
           root.setTrustResolver(trustResolver);
           root.setRoleHierarchy(getRoleHierarchy());
         return root;
       }
    }

我们执行与超类相同的步骤,不同之处在于我们使用包含新方法的 CustomWebSecurityExpressionRoot,CustomWebSecurityExpressionRoot 成为我们 SpEL 表达式的根。

有关更多详细信息,请参阅 Spring 参考中的 SpEL 文档,网址为 http://static.springsource.org/spring /docs/current/spring-framework-reference/html/expressions.html

配置和使用 CustomWebSecurityExpressionHandler

下面我们来看一下配置CustomWebSecurityExpressionHandler的步骤:

  1. We now need to configure CustomWebSecurityExpressionHandler. Fortunately, this can be done easily using the Spring Security namespace configuration support. Add the following configuration to the SecurityConfig.java file:
    // src/main/java/com/packtpub/springsecurity/configuration/
    SecurityConfig.java

    http.authorizeRequests()
       .expressionHandler(customWebSecurityExpressionHandler);
  1. Now, let's update our initialization SQL query to use the new expression. Update the data.sql file so that it requires the user to be ROLE_ADMIN and requested from the local machine. You will notice that we are able to write local instead of isLocal, since SpEL supports Java Bean conventions:
       // src/main/resources/data.sql

      insert into security_filtermetadata(id,ant_pattern,expression,sort_order) 
      values (160, '/admin/**','local and hasRole("ADMIN")',60);
  1. Restart the application and access the H2 console using localhost:8443/admin/h2 and [email protected]/admin1 to see the admin console. If the H2 console is accessed using 127.0.0.1:8443/admin/h2 and [email protected] admin1, the access denied page will be displayed.
您的代码应如下所示 第 13.02 章-日历

CustomWebSecurityExpressionHandler 的替代方案

另一种使用自定义表达式而不是使用 CustomWebSecurityExpressionHandler 接口的方法是添加一个 @Component web,如下所示:

    // src/main/java/com/packtpub/springsecurity/web/access/expression/
    CustomWebExpression.java

    @Component
     public class CustomWebExpression {
       public boolean isLocal(Authentication authentication,
                          HttpServletRequest request) {
       return "localhost".equals(request.getServerName());
   }
}

现在,让我们更新我们的初始化 SQL 查询以使用新的表达式。你会注意到我们可以直接引用 @Component ,因为 SpEL 支持 Java Bean 约定:

// src/main/resources/data.sql

insert into security_filtermetadata(id,ant_pattern,expression,sort_order) values (160, '/admin/**','@customWebExpression.isLocal(authentication, request) and hasRole("ADMIN")',60);

方法安全性如何工作?

方法安全的访问决策机制——是否允许给定请求——在概念上与 Web 请求访问的访问决策逻辑相同。 AccessDecisionManager 轮询一组 AccessDecisionVoters,每一个都可以决定授予或拒绝访问,或弃权。 AccessDecisionManager 的具体实现聚合了投票者的决策,并得出一个允许方法调用的总体决策。

Web 请求访问决策不那么复杂,因为 servlet 过滤器的可用性使得安全请求的拦截(和汇总拒绝)相对简单。由于方法调用可以在任何地方发生,包括 Spring Security 未直接配置的代码区域,Spring Security 设计人员选择使用 Spring 管理的 AOP 方法来识别、评估和保护方法调用。

以下高级流程说明了参与方法调用授权决策的主要参与者:

读书笔记《spring-security-third-edition》自定义授权

我们可以看到 Spring Security 的 o.s.s.access.intercept.aopalliance.MethodSecurityInterceptor 被标准 Spring AOP 运行时调用,以拦截感兴趣的方法调用。从这里开始,根据前面的流程图,是否允许方法调用的逻辑相对简单。

此时,我们可能想知道方法安全特性的性能。显然,MethodSecurityInterceptor 不能为应用程序中的每个方法调用调用——那么方法或类上的注解如何导致 AOP 拦截呢?

首先,默认情况下,所有 Spring 管理的 bean 都不会调用 AOP 代理。相反,如果在 Spring Security 配置中定义了 @EnableGlobalMethodSecurity,则将注册一个标准 Spring AOP o.s.beans.factory.config.BeanPostProcessor,它将内省 AOP 配置以查看如果任何 AOP 顾问指示需要代理(和拦截)。这个工作流是标准的 Spring AOP 处理(称为 AOP 自动代理),并且本身没有任何特定于 Spring Security 的功能。所有注册的BeanPostProcessor都是在spring ApplicationContext初始化时运行的,毕竟Spring bean的配置已经发生了。

AOP 自动代理功能查询所有已注册的 PointcutAdvisor 以查看是否有 AOP 切入点可以解析应该应用 AOP 建议的方法调用。 Spring Security 实现了 o.s.s.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor 类,它检查所有配置的方法安全性并设置适当的 AOP 拦截。请注意,只有声明了方法安全规则的接口或类才会被 AOP 代理!

请注意,强烈建议在接口上声明 AOP 规则(和其他安全注释),而不是在实现类上。类的使用,虽然可以通过 Spring 使用 CGLIB 代理,但可能会意外地改变应用程序的行为,并且通常在语义上不如接口上的安全声明(通过 AOP)正确。 MethodSecurityMetadataSourceAdvisor 将影响具有 AOP 建议的方法的决定委托给一个 o.s.s.access.method.MethodSecurityMetadataSource 实例。不同形式的方法安全注解各有各的 MethodSecurityMetadataSource 实现,用于依次自省每个方法和类,并添加运行时执行的AOP通知。

下图说明了这个过程是如何发生的:

读书笔记《spring-security-third-edition》自定义授权

根据应用程序中配置的 Spring bean 的数量以及您拥有的安全方法注释的数量,添加方法安全代理可能会增加初始化 ApplicationContext 所需的时间。但是,一旦初始化 Spring 上下文,对单个代理 bean 的性能影响可以忽略不计。

现在我们已经了解了如何使用 AOP 来应用 Spring Security,让我们通过创建自定义的 PermissionEvaluator 来加强对 Spring Security 授权的掌握。

创建自定义 PermissionEvaluator

在上一章中,我们演示了我们可以使用 Spring Security 的内置 PermissionEvaluator 实现 AclPermissionEvaluator 来限制对我们应用程序的访问。虽然功能强大,但这通常比必要的复杂。我们还发现了 SpEL 如何制定能够保护我们的应用程序的复杂表达式。虽然简单,但使用复杂表达式的缺点之一是逻辑不集中。幸运的是,我们可以轻松地创建一个自定义的 PermissionEvaluator,它能够集中我们的授权逻辑,同时仍然避免使用 ACL 的复杂性。

CalendarPermissionEvaluator

我们的自定义 PermissionEvaluator 的简化版本不包含任何验证,如下所示:

//src/main/java/com/packtpub/springsecurity/access/CalendarPermissionEvaluator.java

public final class CalendarPermissionEvaluator implements PermissionEvaluator {
   private final EventDao eventDao;


   public CalendarPermissionEvaluator(EventDao eventDao) {
       this.eventDao = eventDao;
   }


   public boolean hasPermission(Authentication authentication, Object 
   targetDomainObject, Object permission) {
       // should do instanceof check since could be any domain object
       return hasPermission(authentication, (Event) targetDomainObject, permission);
   }


   public boolean hasPermission(Authentication authentication, 
   Serializable targetId, String targetType,
           Object permission) {
       // missing validation and checking of the targetType
       Event event = eventDao.getEvent((Integer)targetId);
       return hasPermission(authentication, event, permission);
   }


   private boolean hasPermission(Authentication authentication, 
   Event event, Object permission) {
       if(event == null) {
           return true;
       }
       String currentUserEmail = authentication.getName();
       String ownerEmail = extractEmail(event.getOwner());
       if("write".equals(permission)) {
           return currentUserEmail.equals(ownerEmail);
       } else if("read".equals(permission)) {
           String attendeeEmail = 
           extractEmail(event.getAttendee());
           return currentUserEmail.equals(attendeeEmail) || 
           currentUserEmail.equals(ownerEmail);
       }
       throw new IllegalArgumentException("permission 
       "+permission+" is not supported.");
   }


   private String extractEmail(CalendarUser user) {
       if(user == null) {
           return null;
       }
       return user.getEmail();
   }
}

逻辑与我们已经使用过的 Spring 表达式非常相似,只是它区分了读写访问。如果当前用户的用户名与 Event 对象的所有者的电子邮件匹配,则授予读取和写入访问权限。如果当前用户的电子邮件与与会者的电子邮件匹配,则授予读取权限。否则,拒绝访问。

应该注意的是,单个PermissionEvaluator 用于每个域对象。因此,在实际情况下,我们必须首先执行 instanceof 检查。例如,如果我们还保护我们的 CalendarUser 对象,则可以将这些对象传递到同一个实例中。有关这些细微更改的完整示例,请参阅本书中包含的示例代码 .

配置 CalendarPermissionEvaluator

然后,我们可以利用本章提供的 CustomAuthorizationConfig.java 配置来提供使用我们的 CalendarPermissionEvaluatorExpressionHandler,如下所示:

 //src/main/java/com/packtpub/springsecurity/configuration/
 CustomAuthorizationConfig.java


@Bean
public DefaultMethodSecurityExpressionHandler defaultExpressionHandler(EventDao eventDao){
   DefaultMethodSecurityExpressionHandler deh = new DefaultMethodSecurityExpressionHandler();
   deh.setPermissionEvaluator(
           new CalendarPermissionEvaluator(eventDao));
   return deh;
}

配置应该类似于 第 12 章访问控制列表,只是我们现在使用 CalendarPermissionEvalulator 类而不是 AclPermissionEvaluator

接下来,我们通过将以下配置添加到 SecurityConfig.java 来通知 Spring Security 使用我们自定义的 ExpressionHandler

    //src/main/java/com/packtpub/springsecurity/configuration/SecurityConfig.java
    http.authorizeRequests().expressionHandler
    (customWebSecurityExpressionHandler);

在配置中,我们确保启用 prePostEnabled 并将配置指向我们的 ExpressionHandler 定义。再一次,配置应该看起来与我们的配置非常相似 第 11 章细粒度访问控制

保护我们的 CalendarService

最后,我们可以使用 @PostAuthorize 注释来保护我们的 CalendarService getEvent(int eventId) 方法。您会注意到这一步与我们在 第 1 章不安全应用程序剖析,我们只更改了 PermissionEvaluator 的实现:

    //src/main/java/com/packtpub/springsecurity/service/CalendarService.java

    @PostAuthorize("hasPermission(returnObject,'read')")
    Event getEvent(int eventId);

如果您还没有这样做,请重新启动应用程序,以用户名/密码 [email protected]/admin1 登录,然后访问电话会议事件 (events/101 ) 使用欢迎页面上的链接。将显示拒绝访问页面。但是,我们希望像 ROLE_ADMIN 用户一样能够访问所有事件。

自定义 PermissionEvaluator 的好处

由于只有一个方法受到保护,因此更新注释以检查用户是否具有 ROLE_ADMIN 角色或具有权限将是微不足道的。但是,如果我们保护了所有使用事件的服务方法,那将变得非常麻烦。相反,我们可以只更新我们的 CalendarPermissionEvaluator。进行以下更改:

private boolean hasPermission(Authentication authentication, Event event, Object permission) {
   if(event == null) {
       return true;
   }
   GrantedAuthority adminRole =
           new SimpleGrantedAuthority("ROLE_ADMIN");
   if(authentication.getAuthorities().contains(adminRole)) {
       return true;
   }
   ...
}

现在,重新启动应用程序并重复前面的练习。这一次,电话会议事件将成功显示。你可以看到封装我们的授权逻辑的能力是非常有益的。但是,有时扩展表达式本身可能很有用。

您的代码应如下所示 第 13.03 章-日历

概括

读完本章,你应该对 Spring Security 授权对 HTTP 请求和方法的工作原理有一个深刻的理解。有了这些知识以及提供的具体示例,您还应该知道如何扩展授权以满足您的需求。具体来说,在本章中,我们介绍了 HTTP 请求和方法的 Spring Security 授权架构。我们还演示了如何从数据库配置安全 URL。

我们还看到了如何创建自定义 PermissionEvaluator 对象和自定义 Spring Security 表达式。

在下一章中,我们将探讨 Spring Security 如何执行会话管理。我们还将了解如何使用它来限制对我们应用程序的访问。