Integrations

Spring Security 与众多框架和 API 集成。在本节中,我们将讨论 Spring Security 集成:

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

[TOC]

1. 并发支持

在大多数环境中,安全性存储在每个线程的基础上。这意味着当在新线程上完成工作时,SecurityContext 会丢失。Spring Security 提供了一些基础设施来帮助用户更轻松地完成此操作。Spring Security 为在多线程环境中使用 Spring Security 提供了低级抽象。事实上,这就是 Spring Security 与 AsyncContext.start(Runnable)Spring MVC Async Integration 集成的基础。

这一个章节告诉大家,如果一步一步屏蔽一些代码到后台。

1.1 DelegatingSecurityContextRunnable

Spring Security 的并发支持中最基本的构建块之一是 DelegatingSecurityContextRunnable。它包装了一个委托 Runnable,以便使用委托的指定 SecurityContext 初始化 SecurityContextHolder。然后它调用委托 Runnable 确保之后清除 SecurityContextHolder

DelegatingSecurityContextRunnable 看起来像这样:

public void run() {
try {
SecurityContextHolder.setContext(securityContext);
delegate.run();
} finally {
SecurityContextHolder.clearContext();
}
}

虽然非常简单,但它可以无缝地将 SecurityContext 从一个线程传输到另一个线程。这很重要,因为在大多数情况下,SecurityContextHolder 在每个线程的基础上起作用。例如,您可能已经使用 Spring Security 支持来保护您的一项services。您现在可以轻松地将当前线程的 SecurityContext 传输到要调用的``secured services`的线程。

您可以在下面找到如何执行此操作的示例:

Runnable originalRunnable = new Runnable() {
public void run() {
// invoke secured service
}
};
SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
new DelegatingSecurityContextRunnable(originalRunnable, context);
new Thread(wrappedRunnable).start();

上面的代码执行以下步骤:

  • 创建一个将调用我们的安全服务的 Runnable。请注意,它不知道 Spring Security
  • 从 SecurityContextHolder 中获取我们希望使用的 SecurityContext 并初始化 DelegatingSecurityContextRunnable
  • 使用 DelegatingSecurityContextRunnable 创建线程
  • 启动我们创建的线程

由于使用 SecurityContextHolder 中的 SecurityContext 创建 DelegatingSecurityContextRunnable 是很常见的,因此它有一个快捷构造函数。下面的代码与上面的代码相同:

Runnable originalRunnable = new Runnable() {
public void run() {
// invoke secured service
}
};
// 可以看源代码:在这个构造函数中,调用了SecurityContextHolder.getContext()
DelegatingSecurityContextRunnable wrappedRunnable =
new DelegatingSecurityContextRunnable(originalRunnable);
new Thread(wrappedRunnable).start();

我们的代码使用起来很简单,但它仍然需要我们使用 Spring Security 的知识。在下一节中,我们将看看如何利用 DelegatingSecurityContextExecutor 来隐藏我们正在使用 Spring Security 的事实。

1.2 DelegatingSecurityContextExecutor

在上一节中,我们发现使用 DelegatingSecurityContextRunnable 很容易,但它并不理想,因为我们必须了解 Spring Security 才能使用它。DelegatingSecurityContextExecutor 如何保护我们的代码免受我们正在使用 Spring Security 的任何了解。

DelegatingSecurityContextExecutor 的设计与 DelegatingSecurityContextRunnable 的设计非常相似,只是它接受委托 Executor 而不是委托 Runnable。

您可以在下面看到如何使用它的示例:

SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"));
context.setAuthentication(authentication);
SimpleAsyncTaskExecutor delegateExecutor =
new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
new DelegatingSecurityContextExecutor(delegateExecutor, context);
Runnable originalRunnable = new Runnable() {
public void run() {
// invoke secured service
}
};
executor.execute(originalRunnable);

该代码执行以下步骤:

  • 创建用于我们的 DelegatingSecurityContextExecutor 的 SecurityContext。请注意,在此示例中,我们只是手动创建了 SecurityContext。然而,我们在哪里或如何获得 SecurityContext 并不重要(即,如果我们愿意,我们可以从 SecurityContextHolder 获得它)。
  • 创建一个负责执行提交的 Runnables 的 delegateExecutor
  • 最后,我们创建一个 DelegatingSecurityContextExecutor,它负责用 DelegatingSecurityContextRunnable 包装任何传入执行方法的 Runnable。然后它将包装好的 Runnable 传递给 delegateExecutor。在这种情况下,相同的 SecurityContext 将用于提交给我们的 DelegatingSecurityContextExecutor 的每个 Runnable。如果我们正在运行需要由具有提升权限的用户运行的后台任务,这很好。
  • 这个时候你可能会问自己,“这如何屏蔽我的代码对 Spring Security 的任何了解?”,而不是在我们自己的代码中创建 SecurityContext 和 DelegatingSecurityContextExecutor,我们可以注入一个已经初始化的 DelegatingSecurityContextExecutor 实例。

【如何屏蔽我的代码对 Spring Security 的任何了解?】

@Autowired
private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor
public void submitRunnable() {
Runnable originalRunnable = new Runnable() {
public void run() {
// invoke secured service
}
};
executor.execute(originalRunnable);
}

现在我们的代码不知道 SecurityContext 正在传播到线程,然后运行 originalRunnable,然后清除 SecurityContextHolder。在此示例中,使用同一用户运行每个线程。

如果我们想在调用 executor.execute(Runnable) 时使用来自 SecurityContextHolder 的用户(即当前登录的用户)来处理 originalRunnable 怎么办?

这可以通过从我们的 DelegatingSecurityContextExecutor 构造函数中删除 SecurityContext 参数来完成。例如:

SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
new DelegatingSecurityContextExecutor(delegateExecutor);

现在,无论何时执行 executor.execute(Runnable),SecurityContext 首先由 SecurityContextHolder 获取,然后 SecurityContext 用于创建我们的 DelegatingSecurityContextRunnable。这意味着我们正在使用用于调用 executor.execute(Runnable) 代码的同一用户运行我们的 Runnable。

1.3 Spring Security 并发类

有关与 Java 并发 API 和 Spring Task 抽象的其他集成,请参阅 Javadoc。一旦你理解了前面的代码,它们就很容易解释了。

  • DelegatingSecurityContextCallable
  • DelegatingSecurityContextExecutor
  • DelegatingSecurityContextExecutorService
  • DelegatingSecurityContextRunnable
  • DelegatingSecurityContextScheduledExecutorService
  • DelegatingSecurityContextSchedulingTaskExecutor
  • DelegatingSecurityContextAsyncTaskExecutor
  • DelegatingSecurityContextTaskExecutor
  • DelegatingSecurityContextTaskScheduler

2. Jackson

Spring Security为持久化 Spring Security 相关类提供 Jackson 支持。这可以提高在处理分布式会话时序列化 Spring Security 相关类的性能(即 session 复制、Spring Session 等)。

要使用它,请将 SecurityJackson2Modules.getModules(ClassLoader) 注册到 ObjectMapper (jackson-databind):

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);
// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);

备注:以下 Spring Security 模块提供 Jackson 支持:

  • spring-security-core (CoreJackson2Module)
  • spring-security-web (WebJackson2Module, WebServletJackson2Module, WebServerJackson2Module)
  • spring-security-oauth2-client (OAuth2ClientJackson2Module)
  • spring-security-cas (CasJackson2Module)

3. Localization

如果您需要支持其他语言环境,您需要了解的所有内容都包含在本节中。

所有异常消息都可以本地化,包括与身份验证失败和访问被拒绝(授权失败)相关的消息。专注于开发人员或系统开发人员的异常和日志消息(包括不正确的属性、违反接口契约、使用不正确的构造函数、启动时间验证、调试级日志记录)没有本地化,而是在 Spring Security 的代码中用英语硬编码。

在 spring-security-core-xx.jar 中发布,您会发现一个 org.springframework.security 包,该包又包含一个 messages.properties 文件,以及一些常用语言的本地化版本。这应该由您的 ApplicationContext 引用,因为 Spring Security 类实现 Spring 的 MessageSourceAware 接口并期望消息解析器在应用程序上下文启动时注入依赖项。通常,您需要做的就是在应用程序上下文中注册一个 bean 来引用消息。一个例子如下所示:

<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:org/springframework/security/messages"/>
</bean>

messages.properties 根据标准资源包命名,并表示 Spring Security 消息支持的默认语言。这个默认文件是英文的。

如果您希望自定义 messages.properties 文件,或支持其他语言,您应该复制文件,相应地重命名它,然后在上面的 bean 定义中注册它。这个文件里面没有大量的消息键,因此本地化不应被视为一项重大举措。如果您执行此文件的本地化,请考虑通过记录 JIRA 任务并附加您适当命名的本地化版本的 messages.properties 来与社区分享您的工作。

Spring Security 依赖于 Spring 的本地化支持来实际查找适当的消息。为了让它工作,你必须确保来自传入请求的语言环境存储在 Spring 的 org.springframework.context.i18n.LocaleContextHolder 中。

Spring MVC 的 DispatcherServlet 会自动为您的应用程序执行此操作,但由于在此之前调用了 Spring Security 的过滤器,因此需要在调用过滤器之前设置 LocaleContextHolder 以包含正确的 Locale。

您可以自己在过滤器中执行此操作(必须在 web.xml 中的 Spring Security 过滤器之前)或者你可以使用 Spring 的 RequestContextFilter。有关使用 Spring 本地化的更多详细信息,请参阅 Spring Framework 文档。

“contacts”示例应用程序设置为使用本地化消息。

4. Servlet API 集成

4.1 Servlet 2.5+ 集成

HttpServletRequest.getRemoteUser()

HttpServletRequest.getRemoteUser() 将返回 SecurityContextHolder.getContext().getAuthentication().getName() 的结果,这通常是当前用户名。

如果您想在应用程序中显示当前用户名,这将很有用。此外,检查 this 是否为 null 可用于指示用户是否已通过身份验证或匿名。了解用户是否经过身份验证有助于确定是否应显示某些 UI 元素(即,仅当用户经过身份验证时才应显示注销链接)。

HttpServletRequest.getUserPrincipal()

HttpServletRequest.getUserPrincipal() 将返回 SecurityContextHolder.getContext().getAuthentication() 的结果。这意味着它是一个 Authentication,当使用基于用户名和密码的身份验证时,它通常是 UsernamePasswordAuthenticationToken 的一个实例。如果您需要有关用户的其他信息,这可能很有用。例如,您可能创建了一个自定义 UserDetailsService,它返回一个自定义 UserDetails,其中包含您的用户的名字和姓氏。您可以通过以下方式获取此信息:

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();

应该注意的是,在整个应用程序中执行如此多的逻辑通常是不好的做法。相反,应该集中它以减少 Spring Security 和 Servlet API 的任何耦合。

HttpServletRequest.isUserInRole(String)

HttpServletRequest.isUserInRole(String) 将确定 SecurityContextHolder.getContext().getAuthentication().getAuthorities() 是否包含带有传递给 isUserInRole(String) 的角色的 GrantedAuthority。通常,用户不应将“ROLE_”前缀传递给此方法,因为它是自动添加的。例如,如果要确定当前用户是否具有“ROLE_ADMIN”权限,可以使用以下命令:

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");

这可能有助于确定是否应显示某些 UI 组件。例如,您可能仅在当前用户是管理员时才显示管理员链接。

4.2 Servlet 3+ 集成

以下部分描述了 Spring Security 集成的 Servlet 3 方法。

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse)

HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse) 方法可用于确保用户通过身份验证。如果他们没有通过身份验证,则配置的 AuthenticationEntryPoint 将用于请求用户进行身份验证(即重定向到登录页面)。

HttpServletRequest.login(String,String)

HttpServletRequest.login(String,String) 方法可用于使用当前 AuthenticationManager 对用户进行身份验证。例如,以下将尝试使用用户名“user”和密码“password”进行身份验证:

try {
httpServletRequest.login("user","password");
} catch(ServletException ex) {
// fail to authenticate
}

如果您希望 Spring Security 处理失败的身份验证尝试,则无需捕获 ServletException。

HttpServletRequest.logout()

HttpServletRequest.logout() 方法可用于注销当前用户。

通常这意味着 SecurityContextHolder 将被清除,HttpSession 将失效,任何“记住我”身份验证都将被清理,等等。但是,配置的 LogoutHandler 实现将根据您的 Spring Security 配置而有所不同。需要注意的是,在调用 HttpServletRequest.logout() 之后,您仍然负责写出 response out。通常这将涉及重定向到欢迎页面。

AsyncContext.start(Runnable)

确保您的凭据将传播到新线程的 AsyncContext.start(Runnable) 方法。使用 Spring Security 的并发支持,Spring Security 覆盖 AsyncContext.start(Runnable) 以确保在处理 Runnable 时使用当前的 SecurityContext。例如,以下将输出当前用户的身份验证:

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
public void run() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
try {
final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
asyncResponse.setStatus(HttpServletResponse.SC_OK);
asyncResponse.getWriter().write(String.valueOf(authentication));
async.complete();
} catch(Exception ex) {
throw new RuntimeException(ex);
}
}
});

异步 Servlet 支持

Async Servlet Support

如果您使用的是基于 Java 的配置,那么您就可以开始了。如果您使用 XML 配置,则需要进行一些更新。第一步是确保您已更新 web.xml 以至少使用 3.0 架构,如下所示:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
</web-app>

接下来,您需要确保您的 springSecurityFilterChain 已设置为处理异步请求。

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

就是这样!现在 Spring Security 将确保您的 SecurityContext 也在异步请求上传播。

那么它是怎样工作的?如果您不是真的感兴趣,请随意跳过本节的其余部分,否则请继续阅读。其中大部分都内置在 Servlet 规范中,但 Spring Security 进行了一些调整以确保异步请求正常工作。在 Spring Security 3.2 之前,一旦提交了 HttpServletResponse,就会自动保存来自 SecurityContextHolder 的 SecurityContext。这可能会导致异步环境中出现问题。例如,考虑以下情况:

httpServletRequest.startAsync();
new Thread("AsyncThread") {
@Override
public void run() {
try {
// Do work
TimeUnit.SECONDS.sleep(1);
// Write to and commit the httpServletResponse
httpServletResponse.getOutputStream().flush();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}.start();

问题是 Spring Security 不知道这个线程,所以 SecurityContext 不会传播给它。这意味着当我们提交 HttpServletResponse 时,没有 SecurityContext。当 Spring Security 在提交 HttpServletResponse 时自动保存 SecurityContext 时,它会丢失我们的登录用户。

从 3.2 版开始,Spring Security 足够智能,一旦调用 HttpServletRequest.startAsync(),就不再在提交 HttpServletResponse 时自动保存 SecurityContext。

4.3 Servlet 3.1+ 集成

以下部分描述了 Spring Security 集成的 Servlet 3.1 方法。

HttpServletRequest#changeSessionId()

HttpServletRequest.changeSessionId() 是在 Servlet 3.1 及更高版本中防止 Session Fixation 攻击的默认方法。

5. Spring Data 集成

Spring Security 提供 Spring Data 集成,允许在查询中引用当前用户。将用户包含在查询中以支持分页结果不仅有用而且很有必要。

5.1 Spring Data & Spring Security 配置

要使用此支持,请添加 org.springframework.security:spring-security-data 依赖项并提供 SecurityEvaluationContextExtension 类型的 bean:

@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}

5.2 Security 表达式中的@Query

现在可以在您的查询中使用 Spring Security。例如:

@Repository
public interface MessageRepository extends PagingAndSortingRepository<Message,Long> {
@Query("select m from Message m where m.to.id = ?#{ principal?.id }")
Page<Message> findInbox(Pageable pageable);
}

这将检查 Authentication.getPrincipal().getId() 是否等于 Message 的接收者。请注意,此示例假定您已将主体自定义为具有 id 属性的对象。通过公开 SecurityEvaluationContextExtension bean,所有通用安全表达式都可以在查询中使用。

6. Spring MVC 集成

Spring Security 提供了许多与 Spring MVC 的可选集成。本节更详细地介绍了集成。

6.1 @EnableWebMvcSecurity

备注:

  • 从 Spring Security 4.0 开始,@EnableWebMvcSecurity 已被弃用。替代品是 @EnableWebSecurity,它将根据类路径确定添加 Spring MVC 功能。

要启用 Spring Security 与 Spring MVC 的集成,请将 @EnableWebSecurity 注释添加到您的配置中。

备注:

  • Spring Security 使用 Spring MVC 的 WebMvcConfigurer 提供配置。这意味着如果您使用更高级的选项,例如直接与 WebMvcConfigurationSupport 集成,那么您将需要手动提供 Spring Security 配置。

6.2 MvcRequestMatcher

Spring Security 提供了与 Spring MVC 如何使用 MvcRequestMatcher 匹配 URL 的深度集成。这有助于确保您的安全规则与用于处理您的请求的逻辑相匹配。

为了使用 MvcRequestMatcher,您必须将 Spring Security 配置放置在与 DispatcherServlet 相同的 ApplicationContext 中。这是必要的,因为 Spring Security 的 MvcRequestMatcher 期望一个名为 mvcHandlerMappingIntrospector 的 HandlerMappingIntrospector bean 用于执行匹配的 Spring MVC 配置注册。

对于 web.xml,这意味着您应该将配置放在 DispatcherServlet.xml 中。

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- All Spring Configuration (both MVC and Security) are in /WEB-INF/spring/ -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/*.xml</param-value>
</context-param>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!-- Load from the ContextLoaderListener -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

WebSecurityConfiguration 下面放置在 DispatcherServlet 的 ApplicationContext 中。

public class SecurityInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[] { RootConfiguration.class,
WebMvcConfiguration.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}

备注:

  • 始终建议通过匹配 HttpServletRequest 和方法安全性来提供授权规则。

    通过匹配 HttpServletRequest 来提供授权规则很好,因为它发生在代码路径的早期,有助于减少攻击面( attack surface.)。方法安全性确保如果有人绕过了 Web 授权规则,您的应用程序仍然是安全的。这就是所谓的纵深防御( Defence in Depth)

考虑一个映射如下的控制器

@RequestMapping("/admin")
public String admin() {

如果我们想限制 admin 用户对该 controller 方法的访问,开发人员可以通过在 HttpServletRequest 上匹配以下内容来提供授权规则:

protected configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.antMatchers("/admin").hasRole("ADMIN")
);
}

无论使用哪种配置,URL /admin 都将要求经过身份验证的用户是 admin 用户。但是,根据我们的 Spring MVC 配置,URL /admin.html 也将映射到我们的 admin() 方法。此外,根据我们的 Spring MVC 配置,URL /admin/ 也将映射到我们的 admin() 方法。

问题是我们的安全规则只保护 /admin。我们可以为 Spring MVC 的所有排列添加额外的规则,但这会非常冗长乏味。

相反,我们可以利用 Spring Security 的 MvcRequestMatcher。以下配置将通过使用 Spring MVC 匹配 URL 来保护 Spring MVC 将匹配的相同 URL。

protected configure(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/admin").hasRole("ADMIN")
);
}

6.3 @AuthenticationPrincipal

Spring Security 提供了 AuthenticationPrincipalArgumentResolver ,它可以为 Spring MVC 参数自动解析当前的 Authentication.getPrincipal() 。通过使用@EnableWebSecurity,您将自动将其添加到您的 Spring MVC 配置中。一旦正确配置了 AuthenticationPrincipalArgumentResolver,您就可以在 Spring MVC 层中与 Spring Security 完全解耦。

考虑一种情况,其中自定义 UserDetailsService 返回一个实现 UserDetails 的对象和您自己的 CustomUser 对象。可以使用以下代码访问当前经过身份验证的用户的 CustomUser:

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal();
// .. find messages for this user and return them ...
}

从 Spring Security 3.2 开始,我们可以通过添加注释更直接地解析参数。例如:

import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {
// .. find messages for this user and return them ...
}

有时可能需要以某种方式转换 principal。例如,如果 CustomUser 需要是 final 的,它就不能被扩展。在这种情况下,UserDetailsService 可能会返回一个实现 UserDetails 并提供名为 getCustomUser 的方法来访问 CustomUser 的对象。例如,它可能看起来像:

public class CustomUserUserDetails extends User {
// ...
public CustomUser getCustomUser() {
return customUser;
}
}

然后,我们可以使用 SpEL expression 访问 CustomUser,该表达式使用 Authentication.getPrincipal() 作为根对象:

import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) {
// .. find messages for this user and return them ...
}

我们还可以在 SpEL 表达式中引用 Bean。例如,如果我们使用 JPA 来管理我们的用户并且我们想要修改并保存当前用户的属性,则可以使用以下内容。

import org.springframework.security.core.annotation.AuthenticationPrincipal;
// ...
@PutMapping("/users/self")
public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser,
@RequestParam String firstName) {
// change the firstName on an attached instance which will be persisted to the database
attachedCustomUser.setFirstName(firstName);
// ...
}

我们可以通过将 @AuthenticationPrincipal 设置为我们自己的注释的元注释来进一步消除对 Spring Security 的依赖。下面我们将演示如何在名为 @CurrentUser 的注解上执行此操作。

备注:

  • 重要的是要意识到,为了消除对 Spring Security 的依赖,消费应用程序会创建 @CurrentUser。此步骤不是严格要求的,但有助于将您对 Spring Security 的依赖隔离到更中心的位置。
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}

现在已经指定了@CurrentUser,我们可以使用它来发出信号来解析当前经过身份验证的用户的 CustomUser。我们还将对 Spring Security 的依赖隔离到一个文件中。

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {
// .. find messages for this user and return them ...
}

6.4 Spring MVC 异步集成

Spring Web MVC 3.2+ 对异步请求处理有很好的支持。无需额外配置,Spring Security 将自动将 SecurityContext 设置为调用 controllers 返回的 Callable 的线程。例如,以下方法将自动使用创建 Callable 时可用的 SecurityContext 调用其 Callable:

@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public Object call() throws Exception {
// ...
return "someView";
}
};
}

备注:

  • 从技术上讲,Spring Security 与 WebAsyncManager 集成。用于处理 Callable 的 SecurityContext 是在调用 startCallableProcessing 时存在于 SecurityContextHolder 上的 SecurityContext。

controllers 返回的 DeferredResult 没有自动集成。这是因为 DeferredResult 是由用户处理的,因此无法自动与其集成。但是,您仍然可以使用并发支持( Concurrency Support )来提供与 Spring Security 的透明集成。

6.5 Spring MVC 和 CSRF 集成

① Automatic Token Inclusion

Spring Security 将自动在使用 Spring MVC 表单标签的表单中包含 CSRF 令牌。例如,以下 JSP:

jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:c="http://java.sun.com/jsp/jstl/core"
xmlns:form="http://www.springframework.org/tags/form" version="2.0">
<jsp:directive.page language="java" contentType="text/html" />
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<!-- ... -->
<c:url var="logoutUrl" value="/logout"/>
<form:form action="${logoutUrl}"
method="post">
<input type="submit"
value="Log out" />
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form:form>
<!-- ... -->
</html>
</jsp:root>

将输出类似于以下内容的 HTML:

<!-- ... -->
<form action="/context/logout" method="post">
<input type="submit" value="Log out"/>
<input type="hidden" name="_csrf" value="f81d4fae-7dec-11d0-a765-00a0c91e6bf6"/>
</form>
<!-- ... -->

② 解析 CsrfToken

Spring Security 提供了 CsrfTokenArgumentResolver,它可以为 Spring MVC 参数自动解析当前的 CsrfToken。通过使用@EnableWebSecurity,您将自动将其添加到您的 Spring MVC 配置中。

正确配置 CsrfTokenArgumentResolver 后,您可以将 CsrfToken 公开给基于静态 HTML 的应用程序。

@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}

对其他域保密 CsrfToken 很重要。这意味着如果您使用跨域共享 Cross Origin Sharing(CORS),则不应将 CsrfToken 暴露给任何外部域。

7. WebSocket 安全性

Spring Security 4 添加了对保护 Spring 的 WebSocket 支持的支持。本节介绍如何使用 Spring Security 的 WebSocket 支持。

直接 JSR-356 支持

  • Spring Security 不提供直接的 JSR-356 支持,因为这样做几乎没有价值。这是因为格式是未知的,所以 Spring 几乎无法保护未知格式。此外,JSR-356 没有提供截取消息的方法,因此安全性将是相当具有侵略性的。

7.1 WebSocket 配置

Spring Security 4.0 通过 Spring Messaging 抽象引入了对WebSockets的授权支持。要使用 Java Configuration 配置授权,只需扩展 AbstractSecurityWebSocketMessageBrokerConfigurer 并配置 MessageSecurityMetadataSourceRegistry。例如:

@Configuration
public class WebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer { ①②
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.simpDestMatchers("/user/**").authenticated()
}
}

这将确保:

任何入站 CONNECT 消息都需要有效的 CSRF 令牌来执行同源策略(Same Origin Policy)

SecurityContextHolder 由任何入站请求的 simpUser 标头属性中的用户填充。

我们的消息需要适当的授权。具体来说,任何以“/user/”开头的入站消息都需要 ROLE_USER。有关授权的更多详细信息,请参阅 WebSocket Authorization

7.2 WebSocket 身份验证

WebSocket 重用在建立 WebSocket 连接时在 HTTP 请求中找到的相同身份验证信息。这意味着 HttpServletRequest 上的 Principal 将被移交给 WebSockets。如果您使用 Spring Security,则 HttpServletRequest 上的 Principal 会自动被覆盖。

更具体地说,要确保用户已对您的 WebSocket 应用程序进行身份验证,所需要的只是确保您设置 Spring Security 以对基于 HTTP 的 Web 应用程序进行身份验证。

7.3 WebSocket 授权

要使用 Java Configuration 配置授权,只需扩展 AbstractSecurityWebSocketMessageBrokerConfigurer 并配置 MessageSecurityMetadataSourceRegistry。例如:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated()
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
.simpDestMatchers("/app/**").hasRole("USER")
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER")
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll()
.anyMessage().denyAll();
}
}

备注:

  • Subscribe 订阅:simpSubscribeDestMatchers
  • Dest 发送:simpDestMatchers 猜测
  • Type : 从大的分类来讲,MESSAGE, SUBSCRIBE

任何没有目的地的消息(即除 MESSAGE 或 SUBSCRIBE 的消息类型之外的任何消息)都需要对用户进行身份验证

任何人都可以订阅 /user/queue/errors

任何具有以“/app/”开头的目的地的消息都将要求用户具有角色 ROLE_USER。simpDestMatchers:匹配目的地。

任何以“/user/”或“/topic/friends/”开头且属于 SUBSCRIBE 类型的消息都需要 ROLE_USER。simpSubscribeDestMatchers:订阅类型。

MESSAGE 或 SUBSCRIBE 类型的任何其他消息都会被拒绝。由于 6 我们不需要这一步,但它说明了如何匹配特定的消息类型。

任何其他消息都被拒绝。这是确保您不会错过任何消息的好主意。

7.3.1 WebSocket 授权说明

为了正确保护您的应用程序,了解 Spring 的 WebSocket 支持非常重要。

① 针对 Message 的授权

了解 SUBSCRIBE 和 MESSAGE 类型的消息之间的区别以及它在 Spring 中的工作方式非常重要。

考虑一个聊天应用程序。

  • MESSAGE:系统可以通过“/topic/system/notifications”的目的地向所有用户发送通知
  • SUBSCRIBE:客户端可以通过订阅“/topic/system/notifications”来接收通知。

虽然我们希望客户能够订阅“/topic/system/notifications”,我们不想让他们将 MESSAGE 发送到该目的地。如果我们允许向“/topic/system/notifications”发送一条消息,那么客户端可以直接向该端点发送一条消息并模拟系统。

通常,应用程序通常会拒绝发送到以代理前缀( broker prefix )(即“/topic/”或“/queue/”)开头的目的地的任何 MESSAGE。

② 针对 Destinations 的授权

了解目的地是如何转变的也很重要。

考虑一个聊天应用程序。

  • 用户可以通过向“/app/chat”目的地发送消息来向特定用户发送消息。
  • 应用程序看到消息,确保“from”属性被指定为当前用户(我们不能信任客户端)。
  • 然后应用程序使用 SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message) 将消息发送给接收者。
  • 消息变成“/queue/user/messages-[sessionid]”的目的地

使用上面的应用程序,我们希望让我们的客户端监听“/user/queue”,它被转换为“/queue/user/messages-[sessionid]”。但是,我们不希望客户端能够收听“/queue/*”,因为这将允许客户端查看每个用户的消息。

通常,应用程序通常会拒绝发送到以代理前缀(即“/topic/”或“/queue/”)开头的消息的任何 SUBSCRIBE。当然,我们可能会提供例外情况来解释这些。

7.3.2 Outbound Messages

Spring 包含一个名为 Flow of Messages 的部分,描述了消息如何在系统中流动。需要注意的是,Spring Security 仅保护 clientInboundChannelSpring Security 不会尝试保护 clientOutboundChannel

最重要的原因是性能。对于每条进入的消息,通常会有更多的消息出去。我们鼓励保护对端点的订阅,而不是保护出站消息。

7.4 执行同源策略

需要强调的是,浏览器不会对 WebSocket 连接强制执行同源策略。这是一个极其重要的考虑因素。

① 为什么同源?

考虑以下场景。用户访问 bank.com 并对其帐户进行身份验证。同一用户在其浏览器中打开另一个选项卡并访问 evil.com。同源策略确保 evil.com 无法读取或写入 bank.com 的数据。

对于 WebSockets,同源策略不适用。事实上,除非 bank.com 明确禁止,否则 evil.com 可以代表用户读写数据。这意味着用户可以通过 webSocket 执行的任何操作(即转账),evil.com 可以代表该用户执行。

由于 SockJS试图模拟 WebSockets,它也绕过了同源策略。这意味着开发人员在使用 SockJS 时需要明确保护他们的应用程序免受外部域的影响。

② Spring WebSocket 允许的来源

幸运的是,从 Spring 4.1.5 开始,Spring 的 WebSocket 和 SockJS 支持限制了对当前域的访问。Spring Security 添加了额外的保护层以提供深度防御。

③ 将 CSRF 添加到 Stomp Headers

默认情况下,Spring Security 需要任何 CONNECT message 类型中添加 CSRF 令牌。这确保只有有权访问 CSRF 令牌的站点才能连接。由于只有同源可以访问 CSRF 令牌,因此不允许外部域建立连接。

通常我们需要在 HTTP header 或 HTTP 参数中包含 CSRF 令牌。但是,SockJS 不允许这些选项。相反,我们必须在 Stomp header 中包含令牌

应用程序可以通过访问名为 _csrf 的请求属性来获取 CSRF 令牌。例如,以下将允许访问 JSP 中的 CsrfToken:

var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";

If you are using static HTML, you can expose the CsrfToken on a REST endpoint. For example, the following would expose the CsrfToken on the URL /csrf

@RestController
public class CsrfController {
@RequestMapping("/csrf")
public CsrfToken csrf(CsrfToken token) {
return token;
}
}

JavaScript 可以对端点进行 REST 调用并使用响应来填充 headerName 和令牌。

我们现在可以在 Stomp client 中包含令牌。例如:

...
var headers = {};
headers[headerName] = token;
stompClient.connect(headers, function(frame) {
...
}

④ 在 WebSockets 中禁用 CSRF

如果您想允许其他域访问您的站点,您可以禁用 Spring Security 的保护。例如,在 Java 配置中,您可以使用以下内容:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
...
@Override
protected boolean sameOriginDisabled() {
return true;
}
}

7.5 使用 SockJS

SockJS 提供后备传输来支持旧浏览器。当使用回退选项时,我们需要放宽一些安全约束以允许 SockJS 与 Spring Security 一起使用。

① SockJS & frame-options

SockJS 可以使用利用 iframe 的传输。默认情况下,Spring Security 将拒绝站点被框架以防止 Clickjacking 攻击。为了允许基于 SockJS 框架的传输工作,我们需要配置 Spring Security 以允许相同的来源来构建内容。

您可以使用 frame-options 元素自定义 X-Frame-Options。例如,以下将指示 Spring Security 使用“X-Frame-Options: SAMEORIGIN”,它允许同一域内的 iframe:

<http>
<!-- ... -->
<headers>
<frame-options
policy="SAMEORIGIN" />
</headers>
</http>

同样,您可以使用以下方法自定义框架选项以在 Java 配置中使用相同的来源:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
);
}
}

② SockJS &放松 CSRF

SockJS 对任何基于 HTTP 的传输使用 CONNECT 消息上的 POST。通常我们需要在 HTTP 标头或 HTTP 参数中包含 CSRF 令牌。但是,SockJS 不允许这些选项。相反,我们必须在 Stomp headers 中包含令牌,如将 CSRF 添加到 Stomp headers 中所述。

这也意味着我们需要放松对 Web 层的 CSRF 保护。具体来说,我们希望为我们的连接 URL 禁用 CSRF 保护。我们不想为每个 URL 禁用 CSRF 保护。否则我们的网站将容易受到 CSRF 攻击。

我们可以通过提供 CSRF RequestMatcher 轻松实现这一点。我们的 Java 配置使这非常容易。例如,如果我们的 stomp 端点是“/chat”,我们可以使用以下配置仅对以“/chat/”开头的 URL 禁用 CSRF 保护:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
// ignore our stomp endpoints since they are protected using Stomp headers
.ignoringAntMatchers("/chat/**")
)
.headers(headers -> headers
// allow same origin to frame our site to support iframe SockJS
.frameOptions(frameOptions -> frameOptions
.sameOrigin()
)
)
.authorizeHttpRequests(authorize -> authorize
...
)
...

7.6 CORS 支持

什么是 CORS?它是如何工作的 ?

Spring Framework 为 CORS 提供一流的支持。CORS 必须在 Spring Security 之前处理,因为预检请求将不包含任何 cookie(即 JSESSIONID)。如果请求不包含任何 cookie 并且 Spring Security 优先,则请求将确定用户未通过身份验证(因为请求中没有 cookie)并拒绝它。

确保首先处理 CORS 的最简单方法是使用 CorsFilter。用户可以通过使用以下方式提供 CorsConfigurationSource 来将 CorsFilter 与 Spring Security 集成:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// by default uses a Bean by the name of corsConfigurationSource
.cors(withDefaults())
...
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

如果您使用 Spring MVC 的 CORS 支持,则可以省略指定 CorsConfigurationSource 并且 Spring Security 将利用提供给 Spring MVC 的 CORS 配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// if Spring MVC is on classpath and no CorsConfigurationSource is provided,
// Spring Security will use CORS configuration provided to Spring MVC
.cors(withDefaults())
...
}
}

8. JSP 标签库

8.1 声明标签库

要使用任何标签,您必须在 JSP 中声明安全标签库:

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

8.2 授权标签

<sec:authorize access="hasRole('supervisor')">
This content will only be visible to users who have the "supervisor" authority in their list of <tt>GrantedAuthority</tt>s.
</sec:authorize>

该标签还可以用于检查权限

<sec:authorize access="hasPermission(#domain,'read') or hasPermission(#domain,'write')">
This content will only be visible to users who have read or write permission to the Object found as a request attribute named "domain".
</sec:authorize>

一个常见的要求是只显示一个特定的链接,如果用户实际上被允许点击它。我们如何提前确定是否允许某事?此标记还可以在允许您将特定 URL 定义为属性的替代模式下运行。如果允许用户调用该 URL,则将评估标签正文,否则将跳过它。所以你可能有类似的东西:

<sec:authorize url="/admin">
This content will only be visible to users who are authorized to send requests to the "/admin" URL.
</sec:authorize>

要使用此标记,您的应用程序上下文中还必须有一个 WebInvocationPrivilegeEvaluator 实例。

如果您正在使用命名空间,则会自动注册一个。这是 DefaultWebInvocationPrivilegeEvaluator 的一个实例,它为提供的 URL 创建一个虚拟 Web 请求并调用安全拦截器以查看请求是成功还是失败。这允许您委托您使用 命名空间配置中的拦截 url 声明定义的访问控制设置,并且不必在 JSP 中复制信息(例如所需的角色)。这种方法还可以与提供 HTTP 方法的方法属性结合使用,以实现更具体的匹配。

通过将 var 属性设置为变量名,可以将评估标记的布尔结果(无论是授予还是拒绝访问)存储在页面上下文范围变量中,避免重复和重新评估页面中其他点的条件。

① 为测试禁用标签授权

为未经授权的用户隐藏页面中的链接不会阻止他们访问 URL。例如,他们可以直接在浏览器中输入。作为测试过程的一部分,您可能希望显示隐藏区域,以检查链接在后端是否确实受到保护。如果将系统属性 spring.security.disableUISecurity 设置为 true,则授权标签仍将运行但不会隐藏其内容。默认情况下,它还会用 ... 标签包围内容。这允许您显示具有特定 CSS 样式(例如不同的背景颜色)的“隐藏”内容。例如,尝试在启用此属性的情况下运行“教程”示例应用程序。

如果要更改默认跨度标记的周围文本(或使用空字符串将其完全删除),还可以设置属性 spring.security.securedUIPrefixspring.security.securedUISuffix

8.3 认证标签

此标记允许访问存储在安全上下文中的当前身份验证对象。它直接在 JSP 中呈现对象的属性。因此,例如,如果 Authentication 的 principal 属性是 Spring SecurityUserDetails 对象的一个实例,那么使用 <sec:authentication property="principal.username" /> 将呈现当前用户的名称。

当然,这种事情没有必要使用 JSP 标签,有些人更喜欢在视图中保留尽可能少的逻辑。您可以访问 MVC 控制器中的 Authentication 对象(通过调用 SecurityContextHolder.getContext().getAuthentication())并将数据直接添加到模型以供视图呈现。

8.4 访问控制列表标签

此标签仅在与 Spring Security 的 ACL 模块一起使用时有效。它检查指定域对象所需权限的逗号分隔列表。如果当前用户拥有所有这些权限,则将评估标签正文。如果他们不这样做,它将被跳过。一个例子可能是

通常,此标签应被视为已弃用。而是使用授权标签。

<sec:accesscontrollist hasPermission="1,2" domainObject="${someObject}">
This will be shown if the user has all of the permissions represented by the values "1" or "2" on the given object.
</sec:accesscontrollist>

权限被传递给应用程序上下文中定义的 PermissionFactory,将它们转换为 ACL Permission 实例,所以它们可以是工厂支持的任何格式——它们不必是整数,它们可以是 READ 或 WRITE 之类的字符串。如果没有找到 PermissionFactory,将使用 DefaultPermissionFactory 的实例。应用程序上下文中的 AclService 将用于为提供的对象加载 Acl 实例。将使用所需的权限调用 Acl,以检查是否所有这些都被授予。

此标签也支持 var 属性,与授权标签相同。

8.5 csrfInput 标签

如果启用了 CSRF 保护,此标签会插入一个隐藏的表单字段,其中包含 CSRF 保护令牌的正确名称和值。如果未启用 CSRF 保护,则此标签不输出任何内容。

通常 Spring Security 会为您使用的任何 form:form 标签自动插入一个 CSRF 表单字段,但如果由于某种原因您不能使用 form:form,csrfInput 是一个方便的替代品。

您应该将此标记放置在 HTML

块中,通常会放置其他输入字段。不要将此标签放在 Spring form:form</form:form> 块中。Spring Security 自动处理 Spring 表单。
<form method="post" action="/do/something">
<sec:csrfInput />
Name:<br />
<input type="text" name="name" />
...
</form>

8.6 csrfMetaTags 标签

如果启用了 CSRF 保护,则此标签会插入包含 CSRF 保护令牌表单字段和标头名称以及 CSRF 保护令牌值的元标签。这些元标记对于在应用程序的 JavaScript 中使用 CSRF 保护很有用。

您应该将 csrfMetaTags 放置在 HTML 块中,您通常会在其中放置其他元标记。使用此标记后,您可以使用 JavaScript 轻松访问表单字段名称、标题名称和标记值。此示例中使用 JQuery 来简化任务。

<!DOCTYPE html>
<html>
<head>
<title>CSRF Protected JavaScript Page</title>
<meta name="description" content="This is the description for this page" />
<sec:csrfMetaTags />
<script type="text/javascript" language="javascript">
var csrfParameter = $("meta[name='_csrf_parameter']").attr("content");
var csrfHeader = $("meta[name='_csrf_header']").attr("content");
var csrfToken = $("meta[name='_csrf']").attr("content");
// using XMLHttpRequest directly to send an x-www-form-urlencoded request
var ajax = new XMLHttpRequest();
ajax.open("POST", "https://www.example.org/do/something", true);
ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded data");
ajax.send(csrfParameter + "=" + csrfToken + "&name=John&...");
// using XMLHttpRequest directly to send a non-x-www-form-urlencoded request
var ajax = new XMLHttpRequest();
ajax.open("POST", "https://www.example.org/do/something", true);
ajax.setRequestHeader(csrfHeader, csrfToken);
ajax.send("...");
// using JQuery to send an x-www-form-urlencoded request
var data = {};
data[csrfParameter] = csrfToken;
data["name"] = "John";
...
$.ajax({
url: "https://www.example.org/do/something",
type: "POST",
data: data,
...
});
// using JQuery to send a non-x-www-form-urlencoded request
var headers = {};
headers[csrfHeader] = csrfToken;
$.ajax({
url: "https://www.example.org/do/something",
type: "POST",
headers: headers,
...
});
<script>
</head>
<body>
...
</body>
</html>

如果未启用 CSRF 保护,则 csrfMetaTags 不输出任何内容。

8.7 thymeleaf

thymeleaf 使用 springsecurity 标签