vlambda博客
学习文章列表

读书笔记《spring-security-third-edition》细粒度的访问控制

细粒度的访问控制

在本章中,我们将首先研究两种实现细粒度授权的方法——可能会影响应用程序页面部分的授权。接下来,我们将了解 Spring Security 通过方法注释和使用基于接口的代理来完成 AOP 来保护业务层的方法。然后,我们将回顾基于注释的安全性的一项有趣功能,该功能允许对数据集合进行基于角色的过滤。最后,我们将看看基于类的代理与基于接口的代理有何不同。

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

  • Configuring and experimenting with different methods of performing in-page authorization checks on content, given the security context of a user request
  • Performing configuration and code annotation to make caller preauthorization a key part of our application's business-tier security
  • Several alternative approaches to implement method-level security, and reviewing the pros and cons of each type
  • Implementing data-based filters on collections and arrays using method-level annotations
  • Implementing method-level security on our Spring MVC controllers to avoid configuring antMatcher() methods and <intercept-url> elements

Gradle 依赖项

根据您决定使用的功能,可能需要许多可选的依赖项。其中许多依赖项被注释为 Spring Boot 已将它们包含在 starter 父级中。你会发现我们的 build.gradle 文件已经包含了以下所有依赖:

    //build.gradle
    // Required for JSR-250 based security:
    // JSR-250 Annotations

    compile ('javax.annotation:javax.annotation-api:1.3')

    // Already provided by Spring Boot
    // compile('cglib:cglib-nodep')
    // Already provided by Spring Boot
    // Required for protect-pointcut
    // compile('org.aspectj:aspectjweaver')

集成 Spring 表达式语言 (SpEL)

Spring Security 利用 Spring Expression Language (SpEL) 集成来轻松表达各种授权要求。如果您还记得,我们已经在 第 2 章,Spring Security 入门,当我们定义 antMatcher() 方法时:

    .antMatchers("/events/").hasRole("ADMIN")

Spring Security 提供了一个 o.s.s.access.expression.SecurityExpressionRoot 对象,该对象提供了可供使用的方法和对象,以便做出访问控制决策。例如,可以使用的方法之一是 hasRole 方法,它接受一个字符串。这对应于访问属性的值(在前面的代码片段中)。事实上,还有许多其他表达式可用,如下表所示:

表达

说明

hasRole(字符串角色)

hasAuthority(字符串角色)

如果当前用户具有指定的权限,则返回 true

hasAnyRole(字符串...角色)

hasAnyAuthority(String... authority)

如果当前用户具有任何指定的权限,则返回 true

校长

允许访问当前 Authentication 对象的主体属性。如第 3 章中所述,自定义身份验证 ,这通常是 UserDetails 的一个实例。

认证

SecurityContextHolder类的getContext()方法返回的SecurityContext接口获取当前Authentication对象。

全部许可

总是返回 true

全部拒绝

总是返回 false

isAnonymous()

如果当前主体是匿名的(未经过身份验证),则返回 true。

isRememberMe()

如果当前主体已使用记住我功能进行身份验证,则返回 true

isAuthenticated()

如果用户不是匿名用户(也就是说,他们已通过身份验证),则返回 true

isFullyAuthenticated()

如果用户通过记住我以外的方式进行身份验证,则返回 true

hasPermission(对象目标,对象权限)

如果用户有权访问给定权限的指定对象,则返回 true

hasPermission(String targetId, String targetType, Object权限)

如果用户有权访问给定类型和权限的指定标识符,则返回 true

我们在以下代码片段中提供了一些使用这些 SpEL 表达式的示例。请记住,我们将在本章和下一章中更详细地介绍:

    // allow users with ROLE_ADMIN

    hasRole('ADMIN')

    // allow users that do not have the ROLE_ADMIN

     !hasRole('ADMIN')

    // allow users that have ROLE_ADMIN or ROLE_ROOT and
    // did not use the remember me feature to login

    fullyAuthenticated() and hasAnyRole('ADMIN','ROOT')

    // allow if Authentication.getName() equals admin

    authentication.name == 'admin'

WebSecurityExpressionRoot 类

o.s.s.web.access.expression.WebSecurityExpressionRoot 类为我们提供了一些额外的属性。这些属性以及已经提到的标准属性在 antMatchers() 方法的 access 属性和 access 属性中可用><sec:authorize> 标签,我们稍后会讨论:

表达

说明

请求

当前的 HttpServletRequest 方法。

hasIpAddress(String...ipAddress)

如果当前 IP 地址与 ipAddress 值匹配,则返回 true。这可以是准确的 IP 地址或 IP 地址/网络掩码。

使用请求属性

request 属性是不言自明的,但我们在以下代码中提供了一些示例。请记住,这些示例中的任何一个都可以放在 antMatchers() 方法的访问属性或 <sec:authorize> 元素的访问属性中:

    // allows only HTTP GETrequest.method == 'GET'
    // allow anyone to perform a GET, but
    // other methods require ROLE_ADMIN

    request.method == 'GET' ? permitAll : hasRole('ADMIN')

使用 hasIpAddress 方法

hasIpAddress 方法不像 request 属性那么明确。 hasIpAddress 将很容易匹配一个确切的 IP 地址;例如,如果当前用户的 IP 地址为 192.168.1.93,则以下代码将允许访问:

    hasIpAddress('192.168.1.93')

然而,这并不是那么有用。相反,我们可以定义以下代码,它也将匹配我们的 IP 地址和子网中的任何其他 IP 地址:

    hasIpAddress('192.168.1.0/24')

问题是:这是如何计算的?关键是要了解如何计算网络地址及其掩码。要了解如何做到这一点,我们可以看一个具体的例子。我们从 Linux 终端启动 ifconfig 以查看我们的网络信息(Windows 用户可以使用在命令提示符中输入 ipconfig /all):

$ ifconfig
wlan0     Link encap:Ethernet HWaddr a0:88:b4:8b:26:64
inet addr:192.168.1.93 Bcast:192.168.1.255 Mask:255.255.255.0

看看下面的图表:

读书笔记《spring-security-third-edition》细粒度的访问控制

我们可以看到掩码的前三个八位字节是 255。这意味着我们的 IP 地址 的前三个八位字节属于网络地址。在我们的计算中,这意味着剩余的八位字节为 0:

读书笔记《spring-security-third-edition》细粒度的访问控制

然后我们可以通过首先将每个八位字节转换为二进制数来计算掩码,然后计算有多少个。在我们的例子中,我们得到 24

这意味着我们的 IP 地址将匹配 192.168.1.0/24。关于网络掩码的更多信息的一个很好的站点是 Cisco 的文档,可从 http://www.cisco.com/c/en/us/support/docs/ip/routing-information-protocol-rip/13788-3.html .

MethodSecurityExpressionRoot 类

方法 SpEL 表达式还提供了一些可以通过 o.s.s.access.expression.method.MethodSecurityExpressionRoot 类使用的附加属性:

表达

说明

目标

this 或当前受保护的对象。

返回对象

指被注解的方法返回的对象。

过滤器对象

可以与 @PreFilter@PostFilter 一起用于集合或数组,以仅包含与表达式匹配的元素。 filterObject 对象表示集合或数组的循环变量。

#

方法的任何参数都可以通过在参数名称前加上 # 来引用。例如,可以使用 #id 引用名为 id 的方法参数。

如果这些表达式的描述显得有点简短,请不要担心;我们将在本章后面介绍一些示例。

我们希望您对 Spring Security 的 SpEL 支持有一个不错的了解。要了解有关 SpEL 的更多信息,请参阅位于 https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html

页面级授权

页面级授权是指基于特定用户请求的上下文的应用程序功能的可用性。与我们在 第 2 章中探讨的粗粒度授权不同, Spring Security 入门,细粒度授权通常是指页面部分的选择性可用性,而不是完全限制对页面的访问。大多数现实世界的应用程序将花费大量时间在细粒度授权计划的细节上。

Spring Security 为我们提供了以下三种方式的选择性展示功能:

  • Spring Security JSP tag libraries allow conditional access declarations to be placed within a page declaration itself, using the standard JSP tag library syntax.
  • Thymeleaf Spring Security tag libraries allow conditional access declarations to be placed within a page declaration itself, using the standard Thymeleaf tag library syntax.
  • Checking user authorization in an MVC application's controller layer allows the controller to make an access decision and bind the results of the decision to the model data provided to the view. This approach relies on standard JSTL conditional page rendering and data binding, and is slightly more complicated than Spring Security tag libraries; however, it is more in line with the standard web application MVC logical design.

在为 Web 应用程序开发细粒度授权模型时,这些方法中的任何一种都是完全有效的。让我们探讨如何通过 JBCP 日历用例实现每种方法。

使用 Thymeleaf Spring Security 标签库进行条件渲染

Thymeleaf Spring Security 标签库中最常用的功能是根据授权规则有条件地渲染页面的一部分。这是通过 < sec:authorize*> 标记,其功能类似于核心 JSTL 库中的 <if> 标记,因为标记的主体将根据标记属性中提供的条件呈现.我们已经看到了一个非常简短的演示,演示了如何使用 Spring Security 标记库来限制用户未登录时对内容的查看。

基于 URL 访问规则的条件渲染

Spring Security 标记库提供了基于已在安全配置文件中定义的现有 URL 授权规则呈现内容的功能。这是通过使用 authorizeRequests() 方法和 antMatchers() 方法完成的。

如果有多个 HTTP 元素,则 authorizeRequests() 方法使用当前匹配的 HTTP 元素的规则。

例如,我们可以确保 All Events 链接仅在适当的时候显示,也就是说,对于管理员用户——回想一下我们之前定义的访问规则是如下:

    .antMatchers("/events/").hasRole("ADMIN")

更新 header.html 文件以利用此信息并有条件地呈现到 All Events 页面的链接:

//src/main/resources/templates/fragments/header.html

<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
...
<li sec:authorize-url="/events/">
<a id="navEventsLink" th:href="@{/events/}">All Events</a></li>

这将确保标签的内容不会显示,除非用户有足够的权限访问指定的 URL。可以使用 HTTP 方法进一步限定授权检查,方法是在 URL 之前包含方法属性,如下所示:

    <li sec:authorize-url="GET /events/">
    <a id="navEventsLink" th:href="@{/events/}">All Events</a></li>

使用 authorize-url 属性来定义代码块的授权检查很方便,因为它从您的页面中抽象出实际授权检查的知识并将其保存在您的安全配置文件中。

请注意,HTTP 方法应与您的安全 antMatchers() 方法中指定的大小写匹配,否则它们可能与您预期的不匹配。此外,请注意 URL 应始终相对于 Web 应用程序上下文根(正如您的 URL 访问规则一样)。

出于许多目的,只有在允许用户看见。请记住,标签不仅需要围绕链接;如果用户无权提交它,它甚至可以包围整个表单。

使用 SpEL 进行条件渲染

<sec> 标记与 SpEL 表达式结合使用时,可以使用另一种更灵活的方法来控制 JSP 内容的显示。让我们回顾一下我们在 第 2 章中学到的内容,入门弹簧安全。我们可以通过更改我们的 header.html 文件对任何未经身份验证的用户隐藏 My Events 链接,如下所示:

    //src/main/resources/templates/fragments/header.html

    <li sec:authorize="isAuthenticated()"> 
    <a id="navMyEventsLink" th:href="@{/events/my}">My Events</a></li>

SpEL 评估由与 antMatchers() 方法访问声明规则中使用的表达式相同的代码在幕后执行(假设表达式已配置)。因此,可以从使用 <sec> 标记构建的表达式中访问同一组内置函数和属性。

这两种利用 <sec> 标记的方法都基于安全授权规则提供了对页面内容显示的强大、细粒度的控制。

继续并启动 JBCP 日历应用程序。访问 https://localhost:8443 并使用用户 [email protected] 和密码 user1 登录。您将观察到 My Events 链接已显示,但 All Events 链接已隐藏。注销并以用户 [email protected] 的身份使用密码
admin1 登录。现在两个链接都是可见的。

您应该从以下代码开始 第 11.01 章-日历

使用控制器逻辑有条件地呈现内容

在本节中,我们将演示如何使用基于 Java 的代码来确定是否应该渲染某些内容。我们可以选择仅向用户名包含 user 的用户显示 Welcome 页面上的 Create Event 链接。这将对未以管理员身份登录的用户隐藏 Welcome 页面上的 Create Event 链接。

本章示例代码中的欢迎控制器已更新为使用从方法名称派生的名为 showCreateLink 的属性填充模型,如下所示:

//src/main/java/com/packtpub/springsecurity/web/controllers/WelcomeController.java

    @ModelAttribute (“showCreateLink”)
    public boolean showCreateLink(Authentication authentication) {
      return authentication != null && 
      authentication.getName().contains("user");
    }

你可能注意到 Spring MVC 可以自动为我们获取 Authentication 对象。这是因为 Spring Security 将我们当前的 Authentication 对象映射到 HttpServletRequest.getPrincipal() 方法。由于 Spring MVC 会自动将 java.security.Principal 类型的任何对象解析为 HttpServletRequest.getPrincipal() 的值,因此将 Authentication 指定为我们控制器的参数是访问当前 Authentication 对象的简单方法。我们还可以通过指定 Principal 类型的参数来将代码与 Spring Security 分离。然而,我们在这个场景中选择了 Authentication 来帮助演示一切是如何连接的。

如果我们在另一个不知道如何执行此操作的框架中工作,我们可以使用 SecurityContextHolder 类获取 Authentication 对象,就像我们在 第 3 章自定义身份验证。还要注意,如果我们不使用 Spring MVC,我们可以直接设置 HttpServletRequest 属性,而不是在模型上填充它。然后,我们在请求中填充的属性将可用于我们的 JSP,就像在 Spring MVC 中使用 ModelAndView 对象时一样。

接下来,我们需要使用 index.html 文件中的 HttpServletRequest 属性来确定是否应该显示 Create Event 链接。更新index.html,如下:

    //src/main/resources/templates/header.html

    <li th:if="${showCreateLink}"><a id="navCreateEventLink" th:href="@{events/form}">...</li>

现在,启动应用程序,使用 [email protected] 作为用户名和 admin1 作为密码登录,然后访问 All Events 页面。您应该不再在主导航中看到 Create Events 链接(尽管它仍会出现在页面上)。

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

WebInvocationPrivilegeEvaluator 类

有时可能不会使用 JSP 编写应用程序,并且需要能够根据 URL 确定访问权限,就像我们对 <... sec:authorize-url="/events/" 所做的那样>。这可以通过使用 o.s.s.web.access.WebInvocationPrivilegeEvaluator 接口来完成,该接口与支持 JSP 标记库的接口相同。在下面的代码片段中,我们通过使用名为 showAdminLink 的属性填充我们的模型来演示它的使用。我们可以使用 @Autowired 注释获得 WebInvocationPrivilegeEvaluator

//src/main/java/com/packtpub/springsecurity/web/controllers/WelcomeController.java

    @ModelAttribute (“showAdminLink”)
    public boolean showAdminLink(Authentication authentication) {
       return webInvocationPrivilegeEvaluator.
       isAllowed("/admin/", authentication);
    }

如果您使用的框架不是由 Spring 管理的,@Autowire 将无法为您提供 WebInvocationPrivilegeEvaluator。相反,您可以使用 Spring 的 org.springframework.web.context.WebApplicationContextUtils 接口来获取 WebInvocationPrivilegeEvaluator 的实例,如下所示:

    ApplicationContext context = WebApplicationContextUtils
     .getRequiredWebApplicationContext(servletContext);
    WebInvocationPrivilegeEvaluator privEvaluator =
    context.getBean(WebInvocationPrivilegeEvaluator.class)

要试用它,请继续更新 index.html 以使用 showAdminLink 请求属性,如下所示:

//src/main/resources/templates/header.html

    <li th:if="${showAdminLink}">
     <a id="h2Link" th:href="@{admin/h2/}" target="_blank">
     H2 Database Console</a>
    ...
    </li>

重新启动应用程序并在登录之前查看 Welcome 页面。H2 链接应该不可见。以 [email protected]/admin1 身份登录,您应该会看到它。

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

配置页内授权的最佳方式是什么?

在 Spring Security 4 中 Thymeleaf Spring Security <sec> 标签的重大进步消除了许多关于在以前版本的库中使用这个标签的问题。在许多情况下,使用标签的 authorize-url 属性可以适当地将代码与授权规则的更改隔离开来。您应该在以下情况下使用标签的 authorize-url 属性:

  • The tag is preventing display functionality that can be clearly identified by a single URL
  • The contents of the tag can be unambiguously isolated to a single URL

不幸的是,在典型的应用程序中,您能够频繁使用标记的 authorize-url 属性的可能性有点低。现实情况是,应用程序通常比这复杂得多,并且在决定呈现页面的某些部分时需要更多涉及的逻辑。

使用 Thymeleaf Spring Security 标签库根据其他方法中的安全标准将渲染页面的位声明为禁区是很诱人的。但是,(在许多情况下)这不是一个好主意的原因有很多,如下所示:

  • Complex conditions beyond role membership are not supported by the tag library. For example, if our application incorporated customized attributes on the UserDetails implementation, IP filters, geolocation, and so on, none of these would be supported by the standard <sec> tag.
  • These could, however, conceivably be supported by the custom tags or using SpEL expressions. Even in this case, the page is more likely to be directly tied to business logic rather than what is typically encouraged.
  • The <sec> tag must be referenced on every page that it's used in. This leads to potential inconsistencies between the rulesets that are intended to be common, but may be spread across different physical pages. A good object-oriented system design would suggest that conditional rule evaluations be located in only one place, and logically referred to from where they should be applied.
  • It is possible (and we illustrate this using our common header page include) to encapsulate and reuse portions of pages to reduce the occurrence of this type of problem, but it is virtually impossible to eliminate in a complex application.
  • There is no way to validate the correctness of rules stated at compile time. Whereas compile-time constants can be used in typical Java-based, object-oriented systems, the tag library requires (in typical use) hardcoded role names where a simple typo might go undetected for some time.
  • To be fair, such typos could be caught easily by comprehensive functional tests on the running application, but they are far easier to test using a standard Java component unit testing techniques.
  • We can see that, although the template-based approach for conditional content rendering is convenient, there are some significant downsides.

所有这些问题都可以通过在控制器中使用代码来解决,这些代码可用于将数据推送到应用程序视图模型中。此外,在代码中执行高级授权确定可以带来重用、编译时检查以及模型、视图和控制器的适当逻辑分离的好处。

方法级安全性

到目前为止,本书的主要重点是保护 JBCP 日历应用程序面向 Web 的部分。然而,在安全系统的实际规划中,应同样注意保护允许用户访问任何系统最关键部分——其数据的服务方法。

为什么我们要分层保护?

让我们花一点时间看看为什么保护我们的方法很重要,即使我们已经保护了我们的 URL。启动 JBCP 日历应用程序。使用 [email protected] 作为用户名和 user1 作为密码登录,然后访问 All Events 页面。您将看到自定义 Access Denied 页面。现在,将 .json 添加到浏览器中 URL 的末尾,这样 URL 现在是 https://localhost:8443/events/.json。您现在将看到与 HTML All Events 页面具有相同数据的 JSON 响应。此数据应仅对管理员可见,但我们通过查找未正确配置的 URL 绕过了它。

我们还可以查看我们不拥有且未被邀请参加的活动的详细信息。将 .json 更改为 102,以便 URL 现在是 https://localhost:8443/events/102。您现在将看到 我的活动 页面上未列出的 Lunch 活动。这不应该对我们可见,因为我们不是管理员,这不是我们的活动。

如您所见,我们的 URL 规则还不够强大,无法完全保护我们的应用程序。这些漏洞利用甚至不需要利用更复杂的问题,例如容器处理 URL 规范化的方式不同。简而言之,通常有一些方法可以绕过基于 URL 的安全性。让我们看看向我们的业务层添加安全层如何帮助我们解决新的安全漏洞。

保护业务层

Spring Security 能够为应用程序中任何 Spring 管理的 bean 的调用添加一层授权(或基于授权的数据修剪)。虽然许多开发人员专注于 Web 层安全性,但业务层安全性可以说同样重要,因为恶意用户可能能够渗透 Web 层的安全性或访问通过非 UI 前端(例如 Web)公开的服务服务。

让我们检查下面的逻辑图,看看为什么我们对应用第二层安全感兴趣:

读书笔记《spring-security-third-edition》细粒度的访问控制

Spring Security 主要有以下两种保护方法的技术:

  • Preauthorization: This technique ensures that certain constraints are satisfied prior to the execution of a method that is being allowed, for example, if a user has a particular GrantedAuthority, such as ROLE_ADMIN. Failure to satisfy the declared constraints means that the method call will fail.
  • Postauthorization: This technique ensures that the calling principal still satisfies declared constraints after the method returns. This is rarely used but can provide an extra layer of security around some complex, interconnected business tier methods.

预授权和后授权技术为经典的面向对象设计中通常称为前置条件和后置条件的内容提供形式化支持。前置条件和后置条件允许开发人员通过运行时检查声明围绕方法执行的某些约束必须始终成立。在安全预授权和后授权的情况下,业务层开发人员通过将预期的运行时条件编码为接口或类 API 声明的一部分来有意识地决定特定方法的安全配置文件。正如您可能想象的那样,这需要大量的深思熟虑以避免意外后果!

添加@PreAuthorize 方法注解

我们的第一个设计决定是通过确保用户必须以 ADMIN 用户身份登录,然后才能访问 getEvents()<,从而增强业务层的方法安全性。 /kbd> 方法。这是通过在服务接口定义中的方法中添加一个简单的注释来完成的,如下所示:

    import org.springframework.security.access.prepost.PreAuthorize;
    ...
    public interface CalendarService {
       ...
     @PreAuthorize("hasRole('ADMIN')")
      List<Event> getEvents();
    }

这就是确保调用我们的 getEvents() 方法的任何人都是管理员所需的全部内容。 Spring Security 将使用运行时 Aspect Oriented Programming (AOP) 切入点在方法上执行 BeforeAdvice,并抛出 o.s.s.access.AccessDeniedException 如果不满足安全约束。

指示 Spring Security 使用方法注解

我们还需要对 SecurityConfig.java 进行一次性更改,其中我们已经获得了 Spring Security 配置的其余部分。只需在类声明中添加以下注释:

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

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

验证方法安全性

不相信这很容易吗?使用 [email protected] 作为用户名和 user1 作为密码登录,然后尝试访问 https://localhost:8443/events/.json< /kbd>。您现在应该会看到 Access Denied 页面。

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

如果您查看 Tomcat 控制台,您会看到一个很长的堆栈跟踪,从以下输出开始:

    DEBUG ExceptionTranslationFilter - Access is denied 
    (user is not anonymous); delegating to AccessDeniedHandler
    org.s.s.access.AccessDeniedException: Access is denied
    at org.s.s.access.vote.AffirmativeBased.decide
    at org.s.s.access.intercept.AbstractSecurityInterceptor.
    beforeInvocation
    at org.s.s.access.intercept.aopalliance.
    MethodSecurityInterceptor.invoke
    ...
    at $Proxy16.getEvents
    at com.packtpub.springsecurity.web.controllers.EventsController.events

基于 Access Denied 页面,以及明确指向 getEvents 方法调用的堆栈跟踪,我们可以看到用户被适当地拒绝访问业务方法,因为它缺少 ROLE_ADMINGrantedAuthority。如果您使用用户名 [email protected] 和密码 admin1 运行相同的程序,您会发现访问权限将被授予。

在我们的界面中通过一个简单的声明,我们能够确保所讨论的方法是安全的,这不是很神奇吗?但是 AOP 是如何工作的呢?

基于接口的代理

在上一节给定的示例中,Spring Security 使用基于接口的代理来保护我们的 getEvents 方法。让我们看一下发生了什么的简化伪代码,以了解其工作原理:

    DefaultCalendarService originalService = context.getBean
    (CalendarService.class)
    CalendarService secureService = new CalendarService() {
     … other methods just delegate to originalService ...
      public List<Event> getEvents() {
        if(!permitted(originalService.getEvents)) {
           throw AccessDeniedException()
          }
       return originalCalendarService.getEvents()
      }
   };

您可以看到 Spring 像往常一样创建原始的 CalendarService。但是,它指示我们的代码使用 CalendarService 的另一个实现,该实现在返回原始方法的结果之前执行安全检查。无需事先了解我们的接口就可以创建安全实现,因为 Spring 使用 Java 的 java.lang.reflect.Proxy API 来动态创建接口的新实现。注意返回的对象不再是DefaultCalendarService的实例,因为它是CalendarService的新实现,即CalendarService<的匿名实现/kbd>。这意味着我们必须针对接口进行编程才能使用安全实现,否则将发生 ClassCastException 异常。要了解有关 Spring AOP 的更多信息,请参阅位于 http://static.springsource.org/spring/docs/current/spring-framework-reference/html/aop.html#aop-introduction-proxies

除了 @PreAuthorize 注释之外,还有其他几种方法可以声明方法的安全预授权要求。我们可以检查这些不同的保护方法,然后评估它们在不同情况下的优缺点。

符合 JSR-250 的标准化规则

Java 平台的JSR-250 通用注解 定义了一系列注解,其中一些与安全相关,旨在跨 JSR-250 兼容的运行时环境移植。作为 Spring 2.x 版本的一部分,Spring Framework 与 JSR-250 兼容,包括 Spring Security 框架。

虽然 JSR-250 注释不像 Spring 原生注释那样具有表达力,但它们的好处是它们提供的声明在实现 Java EE 应用程序服务器(如 Glassfish)或面向服务的运行时框架(如 Apache Tuscany。根据您的应用程序对可移植性的需求和要求,您可能会决定以降低的特异性为代价换取代码的可移植性。

为了实现我们在第一个示例中指定的规则,我们通过执行以下步骤进行了一些更改:

  1. First, we need to update our SecurityConfig file to use the JSR-250 annotations:
        //src/main/java/com/packtpub/springsecurity/configuration/
        SecurityConfig.java

        @Configuration
        @EnableWebSecurity
        @EnableGlobalMethodSecurity(jsr250Enabled = true)
        public class SecurityConfig extends WebSecurityConfigurerAdapter {
  1. Lastly, the @PreAuthorize annotation needs to change to the @RolesAllowed annotation. As we might anticipate, the @RolesAllowed annotation does not support SpEL expressions, so we edit CalendarService as follows:
        @RolesAllowed("ROLE_ADMIN")
        List<Event> getEvents();
  1. Restart the application, log in as [email protected]/user1, and try to access http://localhost:8080/events/.json. You should see the Access Denied page again.
您的代码应如下所示: 第 11.05 章-日历

请注意,也可以使用标准 Java 5 字符串数组注释语法提供允许的 GrantedAuthority 名称列表:

    @RolesAllowed({"ROLE_USER","ROLE_ADMIN"})
    List<Event> getEvents();

JSR-250 还指定了两个附加注解,即 @PermitAll@DenyAll,它们的功能如您所料,允许和拒绝对相关方法的所有请求.

类级别的注释
请注意,方法级别的安全注释也可以应用于类级别!方法级注解(如果提供)将始终覆盖在类级别指定的注解。如果您的企业需要为整个班级规定安全策略的规范,这将很有帮助。注意将此功能与良好的注释和编码标准结合使用,以便开发人员非常清楚类及其方法的安全特性。

使用 Spring 的 @Secured 注解的方法安全性

Spring 本身提供了一种更简单的注释样式,类似于 JSR-250 @RolesAllowed 注释。 @Secured 注释在功能和语法上与 @RolesAllowed 相同。唯一显着的区别是它不需要外部依赖,不能被其他框架处理,并且必须使用 @EnableGlobalMethodSecurity 注释上的另一个属性显式启用这些注释的处理:

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

    @EnableWebSecurity(debug = true)
    @EnableGlobalMethodSecurity(securedEnabled=true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

由于 @Secured 的功能与 JSR 标准的 @RolesAllowed 注释相同,因此没有真正令人信服的理由在新代码中使用它,但您可能会在旧代码中遇到它弹簧代码。

包含方法参数的方法安全规则

从逻辑上讲,编写在约束中引用方法参数的规则对于某些类型的操作似乎是明智的。例如,限制 findForUser(int userId) 方法以满足以下约束对我们来说可能是有意义的:

  • The userId argument must be equal to the current user's ID
  • The user must be an administrator (in this case, it is valid for the user to see any event)

虽然很容易看出我们如何更改规则以将方法调用仅限于管理员,但不清楚我们将如何确定用户是否试图更改自己的密码。

幸运的是,Spring Security 方法注释使用的 SpEL 绑定支持更复杂的表达式,包括包含方法参数的表达式。您还需要确保在 SecurityConfig 文件中启用了前置和后置注解,如下所示:

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

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    Lastly, we can update our CalendarService interface as follows:
    @PreAuthorize("hasRole('ADMIN') or principal.id == #userId")  
    List<Event> findForUser(int userId);

您可以在此处看到,我们通过检查主体 ID 和 userId 方法参数 (#userId ,方法参数名称,以 # 符号为前缀)。方法参数绑定的这一强大功能可用这一事实应该可以激发您的创造力,并允许您使用一组非常精确的逻辑规则来保护方法调用。

我们的主体目前是一个实例 CalendarUser 由于自定义身份验证设置来自 第三章自定义身份验证。这意味着主体具有我们的所有属性 CalendarUser 应用程序就可以了。如果我们没有做这个定制,只有 UserDetails 对象将可用。

SpEL 变量使用哈希 (#) 前缀引用。一个重要的注意事项是,为了使方法参数名称在运行时可用,必须在编译后保留调试符号表信息。保留调试符号表信息的常用方法如下:

  • If you are using the javac compiler, you will need to include the -g flag when building your classes
  • When using the <javac> task in Ant, add the attribute debug="true"
  • In Gradle, ensure to add --debug when running the main method, or the bootRun task
  • In Maven, ensure the maven.compiler.debug=true property (the default is true)

请查阅您的编译器、构建工具或 IDE 文档以获取有关在您的环境中配置相同设置的帮助。

启动您的应用程序并尝试使用 [email protected] 作为用户名和 user1 作为密码登录。在 Welcome 页面上,请求 我的活动 ([email protected]) 链接查看 Access Denied 页面。使用 My Events ([email protected]) 再试一次,看看它是否有效。请注意,My Events 页面上显示的用户与当前登录的用户匹配。现在,尝试相同的步骤并以 [email protected]/admin1 身份登录。由于您以具有 ROLE_ADMIN 权限的用户身份登录,因此您将能够看到这两个页面。

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

包含返回值的方法安全规则

正如我们能够利用方法的参数一样,我们也可以利用方法调用的返回值。让我们更新 getEvent 方法以满足返回值的以下约束:

  • The attendee's ID must be the current user's ID
  • The owner'sID must be the current user's ID
  • The user must be an administrator (in this case, it is valid for the user to see any event)

CalendarService接口中加入如下代码:

    @PostAuthorize("hasRole('ROLE_ADMIN') or " + "principal.username ==   
    returnObject.owner.email or " +
    "principal.username == returnObject.attendee.email")
    Event getEvent(int eventId);

现在,尝试使用用户名 [email protected] 和密码 user1 登录。接下来,尝试使用 Welcome 页面上的链接访问 Lunch 事件。您现在应该看到 Access Denied 页面。如果您使用用户名 [email protected] 和密码 user2 登录,则事件将按预期显示,因为 [email protected]Lunch 活动的参与者。

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

使用基于角色的过滤保护方法数据

最后两个依赖于 Spring Security 的注解是 @PreFilter@PostFilter,它们用于将基于安全的过滤规则应用于集合或数组(与 @PostFilter仅 )。这种类型的功能称为安全修整或安全修剪,涉及在运行时使用 principal 的安全凭证来选择性地从一组对象中删除成员。如您所料,此过滤是使用注释声明中的 SpEL 表达式表示法执行的。

我们将通过 JBCP 日历的示例来工作,因为我们想要过滤 getEvents 方法以仅返回允许该用户查看的事件。为了做到这一点,我们移除所有现有的安全注解并将 @PostFilter 注解添加到我们的 CalendarService 接口中,如下所示:

    @PostFilter("principal.id == filterObject.owner.id or " + 
    "principal.id == filterObject.attendee.id")
    List<Event> getEvents();
您的代码应如下所示: 第 11.08 章-日历

移除 antMatchers() 方法,限制对 /events/URL 的访问,以便我们可以测试我们的注解。使用用户名 [email protected] 和密码 user1 登录时启动应用程序并查看 All Events 页面.您将观察到仅显示与我们的用户关联的事件。

使用 filterObject 作为引用当前事件的循环变量,Spring Security 将遍历我们的服务返回的 List 并将其修改为仅包含 <与我们的 SpEL 表达式匹配的 kbd>Event 对象。

通常,@PostFilter 方法的行为方式如下。为简洁起见,我们将集合称为方法返回值,但请注意 @PostFilter 可用于集合或数组方法返回类型。

filterObject 对象被重新绑定到集合中每个元素的 SpEL 上下文。这意味着如果您的方法返回一个包含 100 个元素的集合,则将为每个元素计算 SpEL 表达式。

SpEL 表达式必须返回一个布尔值。如果表达式的计算结果为真,则该对象将保留在集合中,而如果表达式的计算结果为假,则该对象将被删除。

在大多数情况下,您会发现集合后过滤使您免于编写样板代码的复杂性,而这些样板代码您可能无论如何都会编写。注意你理解 @PostFilter 在概念上是如何工作的;与 @PreAuthorize 不同,@PostFilter 指定方法行为而不是前提条件。一些面向对象的纯粹主义者可能会争辩说,@PostFilter 不适合作为方法注释包含在内,而这种过滤应该通过方法实现中的代码来处理。

收集过滤的安全性
请注意,从您的方法返回的实际集合将被修改!在某些情况下,这不是可取的行为,因此您应该确保您的方法返回一个可以安全修改的集合。如果返回的集合是一个 ORM 绑定的集合,这一点尤其重要,因为过滤后的修改可能会无意中持久化到 ORM 数据存储中!

Spring Security 还提供了预过滤作为集合的方法参数的功能;让我们现在尝试实现它。

使用 @PreFilter 预过滤集合

@PreFilter 注释可以应用于方法以过滤基于当前安全上下文传递到方法中的集合元素。从功能上讲,一旦它引用了一个集合,这个注解的行为就与 @PostFilter 注解完全相同,但有几个例外,如下所示:

  • The @PreFilter annotation supports only collection arguments and does not support array arguments.
  • The @PreFilter annotation takes an additional, optional filterTarget attribute which is used to specifically identify the method parameter and filter it when the annotated method has more than one argument.
  • As with @PostFilter, keep in mind that the original collection passed to the method is permanently modified. This may not be desirable behavior, so ensure that callers know that the collection's security may be trimmed after the method is invoked!

想象一下,如果我们有一个接受事件对象集合的 save 方法,并且我们只想允许保存当前登录用户拥有的事件。我们可以这样做:

    @PreFilter("principal.id == filterObject.owner.id")
    void save(Set<Event> events);

很像我们的 @PostFilter 方法,这个注解导致 Spring Security 使用循环变量 filterObject 迭代每个事件。然后它将当前用户的 ID 与事件所有者的 ID 进行比较。如果它们匹配,则保留该事件。如果它们不匹配,则丢弃结果。

比较方法授权类型

以下快速参考图表可以帮助您选择要使用的方法授权检查类型:

方法授权类型

指定为

JSR 标准

允许 SpEL 表达式

@PreAuthorize

@PostAuthorize

注解

是的

@RolesAllowed@PermitAll@DenyAll

注解

是的

@安全

注解

保护切入点

XML

大多数 Spring Security 的 Java 5 消费者可能会选择使用 JSR-250 注释以获得最大的兼容性,并在整个 IT 组织中重用他们的业务类(和相关约束)。在需要时,这些基本声明可以替换为将代码绑定到 Spring Security 实现本身的注释。

如果您在不支持注释的环境(Java 1.4 或更早版本)中使用 Spring Security,那么您的选择在一定程度上仅限于方法安全实施。即使在这种情况下,AOP 的使用也提供了一个相当丰富的环境,我们可以在其中开发基本的安全声明。

基于注释的安全性的实际考虑

需要考虑的一件事是,当返回一组实际应用程序时,可能会出现某种分页。这意味着我们的 @PreFilter@PostFilter 注释不能用作选择要返回的对象的唯一方法。相反,我们需要确保我们的查询只选择允许用户访问的数据。这意味着安全注释成为冗余检查。但是,记住本章开头的课程很重要。我们希望保护层,以防有一层能够被绕过。

概括

在本章中,我们已经涵盖了处理授权的标准 Spring Security 实现中的大部分剩余领域。我们已经掌握了足够的知识,可以彻底通过 JBCP 日历应用程序,并验证应用程序的所有层都进行了适当的授权检查,以确保恶意用户无法操纵或访问他们无权访问的数据。

我们开发了两种微授权技术,即使用 Thymeleaf Spring Security 标签库和 Spring MVC 控制器数据绑定基于授权或其他安全标准过滤页面内内容。我们还探索了几种方法来保护我们应用程序的业务层中的业务功能和数据,并支持与代码紧密集成的丰富的声明式安全模型。我们还学习了如何保护 Spring MVC 控制器以及接口和类代理对象之间的区别

至此,我们已经涵盖了您在大多数标准、安全的 Web 应用程序开发场景中可能会遇到的大部分重要 Spring Security 功能。

在下一章中,我们将讨论 Spring Security 的 ACL(域对象模型)模块。这将允许我们明确声明授权,而不是依赖现有数据。