SpringSecurity 学习指南大全
基于 Servlet 的应用程序
Spring Security 使用标准的 Servlet Filter(过滤器) 来与 Servlet 容器进行集成。也就是说它可以与运行在 Servlet 容器中的任何应用程序一起工作。更具体地说,您可以在基于 Servlet 的应用程序中不使用 Spring 来整合 Spring Security。
新手入门
本节将介绍如何在 SpringBoot 中快速使用 SpringSecurity。
环境准备
引入依赖
- 创建一个基础 SpringBoot 的 Web 项目,添加基础依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<groupId>org.example</groupId>
<artifactId>securityDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
除了 Web 其它可以按需引入即可。
- 添加 SpringSecurity 的启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 编写 Hello 接口和 SpringBoot 启动类
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "Hello Security!";
}
}
到此 SpringBoot 整合 SpringSecurity 的基本完成。当然我们后续还需要自定义配置。
启动项目
输出日志:
2022-06-12 12:02:38.499 WARN 6952 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: aa45a28b-214b-4bdb-80e2-35183dfa2e49
This generated password is for development use only. Your security configuration must be updated before running your application in production.
默认 SpringSecurity 会提供一个 user 用户,并随即生成密码输出到控制台。并且默认会提供一个登录页面。
我们访问 http://localhost:8080/hello 接口,可以发现已经被 Security 拦截了,被重定向到 http://localhost:8080/login SpringSecurity 默认提供的登录页面。
使用 user 和随机密码进行登录,我们就可以访问我们的 hello 接口了。我们的 SpringBoot 应用已经被 Security 监管起来了。
基础入门就是这么简单,那么这其中到底做了什么呢,一切都可以从查看 SpringBoot 的自动配置开始。
SpringBoot 的自动配置
接下来我们需要了解 SpringBoot 都干了哪些事情。
- 启用了 Spring Security 的默认配置,此配置将 servlet Filter 创建为名为
springSecurityFilterChain 的 bean。 此 bean 负责应用程序中的所有安全(如保护应用程序 URL、验证提交的用户名和密码、重定向到登录表单等)。 - 使用用户名为 user 和随机生成的密码创建
UserDetailsService bean,并将其记录到控制台。 - 使用 Servlet 容器为每个请求注册一个 bean 名为
springSecurityFilterChain 的过滤器。
SpringBoot 的自动配置不多,但实现了许多功能,功能如下:
- 经过身份验证的用户才能与应用程序进行任何交互
- 生成默认登录表单
- 提供名为 user 的默认用户,来进行基于表单的身份验证
- 使用 BCrypt 来进行密码加密
- 提供用户退出接口
- CSRF 攻击预防
- Session 固定保护
- 提供 Security Header 集成
- 与 Servlet API 方法集成
架构
本节讨论 Spring Security 在基于 Servlet 的应用程序中的架构理论。并且在后续的身份验证、授权、防止漏洞利用文章中实现了此架构理论。
过滤器回顾
Spring Security 实现 Servlet 支持是基于 Servlet 的 Filters,所以我们先回顾一下 Filters 的作用。 下图展示了单个 HTTP 请求到达处理程序典型的执行流程。
客户端向应用程序发送一个请求,容器创建一个 FilterChain,其中包含 Filter 和 Servlet,它们根据请求的 URI 路径处理HttpServletRequest。在 Spring MVC 应用程序中,Servlet 是 DispatcherServlet 的一个实例。一个 Servlet 最多可以处理一个HttpServletRequest 和 HttpServletResponse。但可以使用多个 Filter 来:
- 防止下游 Filter 或 Servlet 被调用。在这种情况下,Filter 通常会写入 HttpServletResponse。
- 修改下游 Filter 和 Servlet 使用的 HttpServletRequest 或 HttpServletResponse。
Filter 的能力来自传递给它的 FilterChain(过滤器链)。
过滤器链使用示例:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
chain.doFilter(request, response);
}
因为 Filter 只影响下游的 Filter 和 Servlet,所以调用每个 Filter 的顺序非常重要。
DelegatingFilterProxy:委派过滤器代理
Spring 提供了一个名为 DelegatingFilterProxy 的过滤器实现,允许在 Servlet 容器的生命周期和 Spring 的 ApplicationContext 之间建立桥梁进行沟通。Servlet 容器允许使用自己的标准注册过滤器,但是它不知道 Spring 定义的 Beans。
DelegatingFilterProxy 可以通过标准的 Servlet 容器机制注册,但是将所有工作委托给实现 Filter 的 Spring Filter Bean。
下面这张图片展示了 DelegatingFilterProxy 如何加入Spring Filter Bean 和 FilterChain。
DelegatingFilterProxy 可以从 ApplicationContext 中查找 Bean Filter0,然后调用 Bean Filter0。DelegatingFilterProxy 的伪代码如下所示。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName);
delegate.doFilter(request, response);
}
DelegatingFilterProxy 的另一个好处是它允许延迟查找 Filter bean 实例。这很重要,因为容器需要在容器启动之前注册 Filter 实例。但是,Spring 通常使用 ContextLoaderListener 来加载 Spring Bean,直到需要注册 Filter 实例之后才会完成。
FilterChainProxy:过滤器链代理
Spring Security 的 Servlet 支持包含在 FilterChainProxy 中。FilterChainProxy 是 Spring Security 提供的一个特殊 Filter,它允许通过SecurityFilterChain 委托给多个 Filter 实例。因为 FilterChainProxy 是一个 Bean,所以它通常被包装在 DelegatingFilterProxy 中。
SecurityFilterChain:Security 过滤器链
FilterChainProxy 通过使用 SecurityFilterChain 来确定应该为此请求调用哪些 Spring Security Filters。
SecurityFilterChain 中的 Security Filters 通常是Beans,但它们是用 FilterChainProxy 而不是 DelegatingFilterProxy 注册的。FilterChainProxy 提供了许多直接向 Servlet 容器或 DelegatingFilterProxy 注册的优点。首先,它为 Spring Security 的所有 Servlet 支持提供了一个起点。因此,如果您试图对 Spring Security 的 Servlet 支持进行故障排除,在 FilterChainProxy 中添加一个调试点是一个很好的起点。
其次,由于 FilterChainProxy 是 Spring Security 使用的核心,它可以执行不被视为可选的任务。例如,它清除 SecurityContext 以避免内存泄漏。它还应用 Spring Security 的 HttpFirewall 来保护应用程序免受某些类型的攻击。
此外,在确定何时应该调用 SecurityFilterChain 时,它提供了更多的灵活性。在 Servlet 容器中,Filter 仅基于 URL 被调用。但是,FilterChainProxy 可以通过利用 RequestMatcher 接口,基于 HttpServletRequest 中的任何内容来确定调用。
事实上,FilterChainProxy 可以用于确定应该使用哪个 SecurityFilterChain。这允许为应用程序的不同部分提供完全独立的配置。
在多个 SecurityFilterChain 中,FilterChainProxy 决定应该使用哪个 SecurityFilterChain。但只会调用第一个匹配的 SecurityFilterChain。如果请求 /api/messages/ 的 URL,它将首先匹配 SecurityFilterChain 0 的 /api/** ,因此即使它也匹配 SecurityFilterChain n ,但也只会调用 SecurityFilterChain 0 。
如果请求 /messages/ 的URL,它将与 SecurityFilterChain 0 的 /api/** 模式不匹配,因此 FilterChainProxy 将继续尝试每个 SecurityFilterChain。假设没有其它的与其匹配,则将调用匹配 SecurityFilterChain n 的 SecurityFilterChain 实例。
请注意,SecurityFilterChain0 只配置了三个 security Filter 实例。但是,SecurityFilterChain n 配置了四个安全筛选器。请务必注意,每个 SecurityFilterChain 都可以是唯一的,并且可以单独配置。事实上,如果应用程序希望 Spring Security 忽略某些请求,SecurityFilterChain 可能没有安全过滤器。
Security Filters:安全过滤器
Security Filters 通过 SecurityFilterChain API 插入到 FilterChainProxy 中。Filter 的顺序很重要。
以下是 Spring Security Filter 排序的完整列表:
ForceEagerSessionCreationFilter - ChannelProcessingFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- CsrfFilter
- LogoutFilter
- OAuth2AuthorizationRequestRedirectFilter
- Saml2WebSsoAuthenticationRequestFilter
- X509AuthenticationFilter
- AbstractPreAuthenticatedProcessingFilter
- CasAuthenticationFilter
- OAuth2LoginAuthenticationFilter
- Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter - OpenIDAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- ConcurrentSessionFilter
DigestAuthenticationFilter - BearerTokenAuthenticationFilter
BasicAuthenticationFilter - RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- JaasApiIntegrationFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- OAuth2AuthorizationCodeGrantFilter
- SessionManagementFilter
ExceptionTranslationFilter FilterSecurityInterceptor - SwitchUserFilter
处理 Security 异常
ExceptionTranslationFilter 允许将 AccessDeniedException 和 AuthenticationException 转换为 HTTP 响应。
ExceptionTranslationFilter 作为 Security Filters 之一插入到 FilterChainProxy 中。
-
首先,ExceptionTranslationFilter 调用 FilterChain.doFilter(request,response) 来调用应用程序的其余部分。 -
如果用户未通过身份验证或者是 AuthenticationException,则开始身份验证。
- SecurityContextHolder 被清除
- HttpServletRequest 保存在 RequestCache 中。当用户成功通过身份验证时,RequestCache 用于重放原始请求
- AuthenticationEntryPoint 用于从客户端请求凭据。 例如,它可能重定向到登录页面或发送 WWW-Authenticate header。
-
否则,如果是 AccessDeniedException,则拒绝访问。调用 AccessDeniedHandler 来处理拒绝访问。
如果应用程序不抛出 AccessDeniedException 或 AuthenticationException,则 ExceptionTranslationFilter 不做任何事情。
ExceptionTranslationFilter 的伪代码如下所示:
try {
filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication();
} else {
accessDenied();
}
}
- 回顾一下过滤器,您会记得调用 FilterChain.doFilter(request,response) 相当于调用应用程序的其余部分。这意味着如果应用程序的另一部分(即 FilterSecurityInterceptor 或 method security)抛出 AuthenticationException 或 AccessDeniedException,它将在此被捕获并处理。
- 如果用户未通过身份验证或者是 AuthenticationException,则开启身份验证。
- 否则,拒绝访问
认证
Spring Security 为认证提供了全面的支持。我们从讨论整体 Servlet 认证架构开始。正如您所预料的,这一部分更抽象地描述了架构,而没有过多地讨论它如何应用于具体的流程。
Servlet 认证架构
本节讨论扩展了 Servlet Security 架构,描述在 Servlet 认证中使用的 Spring Security 的主要架构组件。如果您需要具体的流程来解释这些部分是如何组合在一起的,请查看具体身份验证机制。
SecurityContextHolder :SecurityContextHolder 用于存储 Spring Security 经过身份验证的人的详细信息。SecurityContext :从 SecurityContextHolder 获取,包含当前已通过身份验证的用户的身份验证。Authentication :可以是 AuthenticationManager 的输入,以提供用户提供的凭据以进行身份验证,也可以是 SecurityContext 中的当前用户。GrantedAuthority :在身份验证上授予主体的权限(即角色、范围等)AuthenticationManager :定义 Spring Security 的 Filter 如何执行身份验证的 API。ProviderManager :AuthenticationManager 最常见的实现。AuthenticationProvider :ProviderManager 使用它来执行特定类型的身份验证。- 使用
AuthenticationEntryPoint 请求凭据:用于从客户端请求凭据(即重定向到登录页面、发送 WWW-Authenticate 响应等) AbstractAuthenticationProcessingFilter :用于身份验证的基本 Filter。此类很好地说明了身份验证的高级流程以及各个部分如何协同工作。
SecurityContextHolder:安全上下文持有者
Spring Security 认证模型的核心是 SecurityContextHolder。它包含 SecurityContext。
SecurityContextHolder 是 Spring Security 存储经过身份验证的人的详细信息的地方。Spring Security 不关心如何填充SecurityContextHolder。如果它包含一个值,那么它将被用作当前经过身份验证的用户。
指示用户已通过身份验证的最简单方法是直接设置 SecurityContextHolder。
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
- 我们首先创建一个空的 SecurityContext。 重要的是创建一个新的 SecurityContext 实例而不是使用 SecurityContextHolder.getContext().setAuthentication(authentication) 以避免跨多个线程的竞争条件。
- 接下来我们创建一个新的 Authentication 对象。 Spring Security 不关心 SecurityContext 上设置了哪种类型的身份验证实现。 这里我们使用 TestingAuthenticationToken 因为它非常简单。 更常见的生产场景是 UsernamePasswordAuthenticationToken(userDetails, password, authority)。
- 最后,我们在 SecurityContextHolder 上设置 SecurityContext。 Spring Security 将使用此信息进行授权。
如果您希望获取有关经过身份验证的主体的信息,可以通过访问 SecurityContextHolder 来实现。
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
默认情况下,SecurityContextHolder 使用 ThreadLocal 来存储这些详细信息,这意味着 SecurityContext 始终可用于同一线程中的方法,即使 SecurityContext 没有明确地作为参数传递给这些方法。 如果在处理当前主体的请求后注意清除线程,那么以这种方式使用 ThreadLocal 是非常安全的。 Spring Security 的 FilterChainProxy 确保 SecurityContext 总是被清除。
有些应用程序并不完全适合使用 ThreadLocal,因为它们使用线程的特定方式。 例如,Swing 客户端可能希望 Java 虚拟机中的所有线程使用相同的安全上下文。 SecurityContextHolder 可以在启动时配置一个策略,以指定您希望如何存储上下文。对于独立应用程序,您将使用 SecurityContextHolder.MODE_GLOBAL 策略。 其他应用程序可能希望安全线程产生的线程也采用相同的安全身份。这是通过使用 SecurityContextHolder.MODE_INHERITABLETHREADLOCAL 来实现的。 您可以通过两种方式从默认的 SecurityContextHolder.MODE_THREADLOCAL 更改模式。 第一个是设置系统属性,第二个是调用 SecurityContextHolder 的静态方法。 大多数应用程序不需要更改默认设置,但如果您这样做,请查看 Javadoc for SecurityContextHolder 以了解更多信息。
SecurityContext:安全上下文
SecurityContext 是从 SecurityContextHolder 获取的。SecurityContext 包含身份验证对象。
Authentication:认证对象
在 Spring Security 中,身份验证有两个主要目的:
- AuthenticationManager 的输入,用于提供用户提供的身份验证凭据。 在这种情况下使用时,isAuthenticated() 返回 false。
- 表示当前经过身份验证的用户。 当前的 Authentication 可以从 SecurityContext 中获取。
Authentication 包含以下属性:
principal :标识用户。 当使用用户名/密码进行身份验证时,这通常是 UserDetails 的一个实例。credentials :通常是密码。 在许多情况下,这将在用户通过身份验证后被清除,以确保它不被泄露。authorities :GrantedAuthoritys 是授予用户的高级权限。 示例角色或范围。
GrantedAuthority:授予权限
GrantedAuthoritys 是授予用户的高级权限。 示例角色或范围。
GrantedAuthoritys 可以从 Authentication.getAuthorities() 方法中获得。此方法提供 GrantedAuthority 对象的集合。毫不奇怪,GrantedAuthority 是授予委托人的权限。此类权限通常是“角色”,例如 ROLE_ADMINISTRATOR 或 ROLE_HR_SUPERVISOR。这些角色稍后将被配置用于 web 授权、方法授权和 domain(域)对象授权。Spring Security 的其他部分能够解释这些权限,并期望它们存在。 当使用基于用户名/密码的身份验证时,GrantedAuthoritys 通常由 UserDetailsService 加载。
通常,GrantedAuthority 对象是应用程序范围的权限。它们并不特定于给定的 domain(域)对象。因此,您不太可能有一个GrantedAuthority 来表示对编号为 54 的 Employee 对象的权限,因为如果有数千个这样的权限,您会很快耗尽内存(或者,至少会导致应用程序花很长时间来验证一个用户)。当然,Spring Security 是专门为处理这种常见需求而设计的,但您应该为此目的使用项目的 domain(域) 对象安全功能。
AuthenticationManager:认证管理器
AuthenticationManager 是定义 Spring Security 的 Filter 如何执行身份验证的 API。然后由调用 AuthenticationManager 的控制器(即 Spring Security 的 Filterss)在 SecurityContextHolder 上设置返回的 Authentication。如果你没有与 Spring Security 的 Filters 集成,你可以直接设置 SecurityContextHolder 并且不需要使用 AuthenticationManager。
虽然 AuthenticationManager 的实现可以是任何东西,但最常见的实现是 ProviderManager。
ProviderManager:提供者管理器
ProviderManager 是 AuthenticationManager 最常用的实现。ProviderManager 委托给一个 AuthenticationProviders 列表。每个 AuthenticationProvider 都有机会指示身份验证应该成功、失败或指示它不能做出决定并允许下游 AuthenticationProvider 做出决定。如果配置的 AuthenticationProviders 都不能进行身份验证,则身份验证将失败并出现 ProviderNotFoundException,这是一个特殊的 AuthenticationException,表明 ProviderManager 未配置为支持传递给它的身份验证类型。
实际上,每个 AuthenticationProvider 都知道如何执行特定类型的身份验证。例如,一个 AuthenticationProvider 可能能够验证用户名/密码,而另一个可能能够验证 SAML assertion。这允许每个 AuthenticationProvider 执行非常特定类型的身份验证,同时支持多种类型的身份验证并且只公开单个 AuthenticationManager bean。
ProviderManager 还允许配置一个可选的父 AuthenticationManager,在没有 AuthenticationProvider 可以执行身份验证的情况下进行咨询。父级可以是任何类型的 AuthenticationManager,但它通常是 ProviderManager 的一个实例。
事实上,多个 ProviderManager 实例可能共享同一个父 AuthenticationManager。这在有多个 SecurityFilterChain 实例具有一些共同的身份验证(共享父 AuthenticationManager)但也有不同的身份验证机制(不同的 ProviderManager 实例)的情况下有些常见。
默认情况下,ProviderManager 将尝试从成功的身份验证请求返回的 Authentication 对象中清除任何敏感的凭据信息。这可以防止诸如密码之类的信息在 HttpSession 中保留的时间超过必要的时间。
当您使用用户对象的缓存时,这可能会导致问题,例如,为了提高无状态应用程序的性能。如果 Authentication 包含对缓存中的对象(例如 UserDetails 实例)的引用,并且已删除其凭据,则将不再可能针对缓存的值进行身份验证。如果您使用缓存,则需要考虑到这一点。一个明显的解决方案是首先在缓存实现中或在创建返回的 Authentication 对象的 AuthenticationProvider 中制作对象的副本。或者,您可以禁用 ProviderManager 上的 eraseCredentialsAfterAuthentication 属性。有关更多信息,请参阅 Javadoc 。
AuthenticationProvider:认证提供者
ProviderManager 中可以注入多个 AuthenticationProvider。每个 AuthenticationProvider 执行特定类型的身份验证。例如,DaoAuthenticationProvider 支持基于用户名/密码的身份验证,而 JwtAuthenticationProvider 支持对 JWT 令牌进行身份验证。
使用 AuthenticationEntryPoint 请求凭据
AuthenticationEntryPoint 用于发送从客户端请求凭据的 HTTP 响应。
有时,客户端会主动包含凭据(例如用户名/密码)来请求资源。在这些情况下,Spring Security 不需要提供从客户端请求凭据的 HTTP 响应,因为它们已经包含在内。
也就是说如果客户端的请求没有经过认证,会响应客户端需要认证信息,常使用在未认证处理。
在有些情况下,客户端将对他们无权访问的资源发出未经身份验证的请求。在这种情况下,AuthenticationEntryPoint 的实现用于响应客户端需要请求凭据。AuthenticationEntryPoint 实现可能会重定向到登录页面,使用 WWW-Authenticate 标头等进行响应。
AbstractAuthenticationProcessingFilter:抽象身份验证处理过滤器
凭据可以是用户登录信息或者token等等,代表用户有效证明的数据
AbstractAuthenticationProcessingFilter 是验证用户凭据的基本过滤器。在可以对凭据进行身份验证之前,Spring Security 通常使用 AuthenticationEntryPoint 来响应用户访问请求需要凭据。
接下来,AbstractAuthenticationProcessingFilter 可以验证提交给它的任何身份验证请求。
- 当用户提交他们的凭据时, AbstractAuthenticationProcessingFilter 从 HttpServletRequest 创建一个 Authentication 以进行身份验证。创建的 Authentication 类型取决于 AbstractAuthenticationProcessingFilter 的子类。例如,UsernamePasswordAuthenticationFilter 根据在 HttpServletRequest 中提交的用户名和密码创建 UsernamePasswordAuthenticationToken。
- 接下来,将 Authentication 传递给 AuthenticationManager 进行身份验证。
- 如果认证失败,则失败
- SecurityContextHolder 已被清除。
- 调用 RememberMeServices.loginFail。 如果记住我没有配置,这是一个空操作。
- AuthenticationFailureHandler 被调用。
- 如果认证成功,则成功。
- SessionAuthenticationStrategy 收到新登录通知。
- Authentication 在 SecurityContextHolder 上设置。 稍后 SecurityContextPersistenceFilter 将 SecurityContext 保存到 HttpSession。
- 调用 RememberMeServices.loginSuccess。 如果记住我没有配置,这是一个空操作。
- ApplicationEventPublisher 发布一个 InteractiveAuthenticationSuccessEvent。
- AuthenticationSuccessHandler 被调用。
用户名 / 密码验证
验证用户身份的最常见方法之一是验证用户名和密码。 因此,Spring Security 为使用用户名和密码进行身份验证提供了全面的支持。
读取用户名和密码
Spring Security 提供了以下内置机制来从 HttpServletRequest 读取用户名和密码:
表单登录
Spring Security 支持通过 html 表单提供用户名和密码。本节详细介绍了基于表单的身份验证在 Spring Security 中是如何工作的。
让我们看看基于表单的登录在 Spring Security 中是如何工作的。首先,我们看到用户如何被重定向到登录表单。
该图基于我们的 SecurityFilterChain 过滤器链。
- 首先,用户向未授权的资源 /private 资源发出未经身份验证的请求。
- Spring Security 的 FilterSecurityInterceptor 通过抛出 AccessDeniedException 来表示未经身份验证的请求被拒绝。
- 由于用户未经过身份验证,ExceptionTranslationFilter 会启动开始身份验证,并使用配置的 AuthenticationEntryPoint 发送重定向到登录页面。在大多数情况下,AuthenticationEntryPoint 是 LoginUrlAuthenticationEntryPoint 的一个实例。
- 然后,浏览器将请求它被重定向到的登录页面。
- 用户可以通过登录页面来进行认证
提交用户名和密码后,UsernamePasswordAuthenticationFilter 将对用户名和密码进行身份验证。UsernamePasswordAuthenticationFilter 继承至 AbstractAuthenticationProcessingFilter,执行流程请看下面这张图:
该图基于 SecurityFilterChain 过滤器链:
- 当我们进行登录时,UsernamePasswordAuthenticationFilter 会创建一个 UsernamePasswordAuthenticationToken,通过从 HttpServletRequest 中提取用户名和密码。
UsernamePasswordAuthenticationToken 是 Authentication 认证对象的一个具体实现类
-
接下来 UsernamePasswordAuthenticationToken 会被传入 AuthenticationManager 认证管理器中进行认证处理,如何进行认证取决于用户对象的存储方法。 -
如果认证失败
- SecurityContextHolder 被清除。
- 调用记住我 RememberMeServices.loginFail,如果没有配置则不会调用
- AuthenticationFailureHandler 认证失败处理器会被调用。
-
如果认证成功
- SessionAuthenticationStrategy 认证会话策略收到新登录的通知。
- Authentication 认证对象会被保存到 SecurityContextHolder 安全上下文持有者中。
- RememberMeServices.loginSuccess 记住我被调用,如果没配置则不会
- ApplicationEventPublisher 发布登录成功事件 InteractiveAuthenticationSuccessEvent
- 接着调用 AuthenticationSuccessHandler 登录成功处理器,常用实现是 SimpleUrlAuthenticationSuccessHandler。当我们登录成功后,它会帮我们重定向到 ExceptionTranslationFilter 保存的登录请求之前的页面。
默认情况下启用表单登录,但是如果一但提供了基于 Servlet 的配置,就必须明确提供基于表单的登录,下面是一个示例配置:
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(withDefaults());
}
默认 Security 会提供一个登录页面,基本项目开发都会有自己的登录页面。
下面配置演示了如何提供自定义登录页面:
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
}
默认是通过 Thymeleaf 来生成登录页面,路径:src/main/resources/templates/login.html :
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
</head>
<body>
<h1>Please Log In</h1>
<div th:if="${param.error}">
Invalid username and password.</div>
<div th:if="${param.logout}">
You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
</body>
</html>
默认登录表单说明:
- 该表单会发送 post 请求到 /login 来进行登录
- 该表单包含一个 CSRF 令牌,这是 Thymeleaf 默认提供的
- 默认使用 username 和 password 来进行登录
以上是默认内容我们都可以通过自定义配置来进行配置。
如果你正在使用 SpringMVC 通过通过 Controller 控制器返回 login 路径来访问默认的登录页面,示例:
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}
基本认证
本节介绍了 Spring Security 为基于 Servlet 的应用程序提供了基本的 HTTP 认证支持。
查看下图,我们可以清楚的看到 SpringSecurity 对 HTTP 基础认证的工作原理,我们可以清楚的看到 WWW-Authenticate 的头会被发送回未经身份验证的客户端。
认证也是基于 SecurityFilterChain 过滤器链:
- 首先,未经过身份验证的用户向未授权的 /private 接口发起请求
- Spring Security 的 FilterSecurityInterceptor 通过抛出 AccessDeniedException 异常来拒绝未经过身份认证的请求
- 由于用户未认证,ExceptionTranslationFilter 会开启身份认证,配置的 AuthenticationEntryPoint 认证入口点实现是 BasicAuthenticationEntryPoint 的一个实例,他会发送 WWW-Authenticate 头返回回客户端,并且 RequestCache 请求为空。请求重定向由客户端去实现。
当客户端收到 WWW-Authenticate 头时,表示需要身份认证后才能访问请求接口,并且使用用户名和密码来进行身份认证。
认证流程,基于 SecurityFilterChain 过滤器链:
-
当用户进行身份验证请求时,BasicAuthenticationFilter 会创建一个 UsernamePasswordAuthenticationToken 认证对象,此对象的创建也是通过从 HttpServletRequest 中提取用户名和密码来实现的。 -
接下来 UsernamePasswordAuthenticationToken 会被传入 AuthenticationManager 中进行身份验证,AuthenticationManager 如何进行验证取决于用户如何存储的。 -
如果认证失败
- SecurityContextHolder 被清除。
- 调用记住我 RememberMeServices.loginFail,如果没有配置则不会调用
- 调用 AuthenticationEntryPoint 再次发送 WWW-Authenticate 头到客户端
-
如果认证成功
- Authentication 被保存在 SecurityContextHolder 中。
- RememberMeServices.loginSuccess 记住我被调用,如果没配置则不会
- BasicAuthenticationFilter 调用 FilterChain.doFilter(request,response) 来继续其余的应用程序逻辑。
默认情况下,Spring Security 的 HTTP 基本身份验证支持是启用的。然而,一旦提供了任何基于 servlet 的配置,就必须显式地提供HTTP Basic。
示例:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.httpBasic(withDefaults());
return http.build();
}
摘要式身份验证
密码存储
持续身份验证
会话管理
记住我认证
OpenID 支持
匿名认证
预认证场景
Java 身份验证和授权服务 (JAAS) 提供程序
CAS认证
X.509 身份验证
运行身份验证替换
处理注销
认证事件
授权
Spring Security 中的高级授权功能是其受欢迎的最令人信服的原因之一。 无论您选择如何进行身份验证 - 无论是使用 Spring Security 提供的机制和提供者,还是与容器或其他非 Spring Security 身份验证权限集成 - 您都会发现授权服务可以在您的应用程序中以一致且简单的方式使用。
在这一部分中,我们将探索不同的 AbstractSecurityInterceptor 抽象安全拦截器 实现,这些实现在第一部分中已经介绍过了。
授权架构
Authentication 身份认证对象,身份认证对象存储了 GrantedAuthority 授权对象列表,AuthenticationManager 会把 GrantedAuthority 授权对象插入到 Authentication 认证对象中。然后 AuthorizationManager 在做出权限认证时,会取出认证对象里存储的权限对象。
GrantedAuthority 是一个只有一个方法的接口:
String getAuthority();
此方法允许 AuthorizationManagers 获得 GrantedAuthority 权限对象的精确字符串。大多数 AuthorizationManagers 和AccessDecisionManagers 可以轻松地 “读取” GrantedAuthority。如果 GrantedAuthority 不能精确地表示为字符串,则 GrantedAuthority 被认为是 “复杂的”, getAuthority() 必须返回 null。
复杂的 GrantedAuthority 如不同的用户账户的操作和权限列表。如果很难把权限表示为字符串,GrantedAuthority 会返回 null。我们需要专门实现 GrantedAuthority 对象,并且向所有的 AuthorizationManager 授权管理器表明我们有自定义权限对象,以便权限管理器使用。
Spring Security 有一个具体的实现 SimpleGrantedAuthority,它允许将用户给定的字符串转换为 GrantedAuthority 权限对象。在 Security 中 AuthenticationProviders 身份验证提供者都使用默认的 SimpleGrantedAuthority 权限对象来填充身份验证对象。
调用前处理
Spring Security 提供了拦截器来控制对安全对象的访问,比如方法调用或 web 请求。由 AccessDecisionManager 来处理是否允许继续调用。
AuthorizationManager:授权管理器
AuthorizationManager 取代了 AccessDecisionManager 和 AccessDecisionVoter。
鼓励自定义 AccessDecisionManager 或 AccessDecisionVoter 的应用程序更改为使用 AuthorizationManager。
在做权限认证时 AuthorizationFilter,会调用 AuthorizationManagers 来处理权限认证,AuthorizationManager 接口包含两种方法。
AuthorizationDecision check(Supplier<Authentication> authentication, Object secureObject);
default AuthorizationDecision verify(Supplier<Authentication> authentication, Object secureObject)
throws AccessDeniedException {
AuthorizationDecision decision = check(authentication, object);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}
check 方法用于授权处理,我们可以通过传入安全对象,在做权限处理时,可以获取到安全对象中的属性。例如:我们传入的是一个 MethodInvocation 方法安全对象。我们可以在 AuthorizationManager 中获取到传入的 MethodInvocation 方法安全对象中的参数,并且在 AuthorizationManager 授权管理器中实现我们的安全逻辑。如果允许访问,其实例将返回一个通过的 AuthorizationDecision 授权决策,如果访问拒绝,则返回拒绝的 AuthorizationDecision 授权决策,如果放弃做出决策,则返回一个 NULL 的 AuthorizationDecision。
verify 会调用 check 方法,并且在 AuthorizationDecision 为拒绝访问的情况下,抛出 AccessDeniedException 异常。
基于委托的 AuthorizationManager 授权管理器实现
虽然用户可以自定义 AuthorizationManager 授权管理器,来实现自己的授权认证,但 SpringSecurity 附带了一个委托 AuthorizationManager 授权管理器,并且它可以和各个 AuthorizationManager 授权管理器合作。
RequestMatcherDelegatingAuthorizationManager 请求匹配委托授权管理器会将请求与合适的委托 AuthorizationManager 授权管理器进行匹配。对于方法安全,可以使用 AuthorizationManagerBeforeMethodInterceptor 和 AuthorizationManagerAfterMethodInterceptor。
Spring Security 提供了 7 个 AuthorizationManager 授权管理器的实现类:
我们可以使用 AuthorizationManager 不同实现类来组合授权。
AuthorityAuthorizationManager
AuthorityAuthorizationManager 是 SpringSecurity 提供的最常见的 AuthorizationManager 实现类,它配置了一组给定的权限集合来进行权限验证,如果用户的 Authentication 对象的权限和给定的权限集合有值相匹配,则会返回 true 的 AuthorizationDecision 授权决定,否则返回 false 的 AuthorizationDecision 授权决定。
AuthenticatedAuthorizationManager
AuthenticatedAuthorizationManager 是认证授权管理器。它可以用来区分是匿名用户还是记住我用户,许多网站在记住我身份验证下只有有限的权限,只有当用户登录后才能获取完整的权限。
自定义授权管理器
我们可以实现 AuthorizationManager 接口,来自定义其权限验证的实现。例如:我们可以根据自己的授权数据库来进行授权管理逻辑。
当然我们也可以使用老版的 AccessDecisionVoter 类来实现自定义授权逻辑
调整 AccessDecisionManager 和 AccessDecisionVoters
在 AuthorizationManager 之前,Spring Security 发布了 AccessDecisionManager 和 AccessDecisionVoter。
在一些升级旧的项目中,我们可以需要一个 AuthorizationManager 来调用 AccessDecisionManager 和 AccessDecisionVoter。
如果要调用现有的 AccessDecisionManager 你可以这么做:
@Component
public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager {
private final AccessDecisionManager accessDecisionManager;
private final SecurityMetadataSource securityMetadataSource;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
try {
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
return new AuthorizationDecision(true);
} catch (AccessDeniedException ex) {
return new AuthorizationDecision(false);
}
}
@Override
public void verify(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
this.accessDecisionManager.decide(authentication.get(), object, attributes);
}
}
然后把它加入你的 SecurityFilterChain 安全过滤器链上。
如果只调用 AccessDecisionVoter 你可以这样:
@Component
public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager {
private final AccessDecisionVoter accessDecisionVoter;
private final SecurityMetadataSource securityMetadataSource;
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) {
Collection<ConfigAttributes> attributes = this.securityMetadataSource.getAttributes(object);
int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes);
switch (decision) {
case ACCESS_GRANTED:
return new AuthorizationDecision(true);
case ACCESS_DENIED:
return new AuthorizationDecision(false);
}
return null;
}
}
记得把它加入 SpringSecurity 的安全过滤器链中。
分层角色
在程序开发中,可能会出现角色包含的情况,比如一个父角色包含其它子角色。比如管理员和普通用户,你可能会要求管理员也有普通用户的权限。因此你可能会分配给此用户,管理员和普通用户的权限。有可能是用户角色包含管理员角色,或者管理员角色包含用户角色,当系统角色特别多时,在处理上会变得十分麻烦。
使用分层角色我们可以轻松的去包含其它子角色。Spring Security 的 RoleVoter 的扩展版本 RoleHierarchyVoter 配置了 RoleHierarchy,从中获取分配给用户的所有“可访问权限”。 典型的配置可能如下所示:
@Bean
AccessDecisionVoter hierarchyVoter() {
RoleHierarchy hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
"ROLE_STAFF > ROLE_USER\n" +
"ROLE_USER > ROLE_GUEST");
return new RoleHierarchyVoter(hierarchy);
}
上面实例有四个角色权限 ROLE_ADMIN、ROLE_STAFF、ROLE_USER、ROLE_GUEST,其中 ROLE_ADMIN 是顶层角色,当用户被授予了 ROLE_ADMIN,则此用户还可以拥有剩下的三个角色的权限。> 符号是包含的意思。
Spring Security 提供角色分层赋予来简化分层角色的配置,当然我们也可以根据自己的业务情况来使用。
旧版授权组件
Spring Security 包含一些遗留的旧版组件,并且并没有在项目中删除,推荐使用上面的新版授权组件
AccessDecisionManager
AccessDecisionManager 由 AbstractSecurityInterceptor 调用,负责做出最终的访问控制决策。 AccessDecisionManager 接口包含三个方法:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
AccessDecisionManager 的 decide 方法传入了授权需要的全部信息,并通过这些信息来进行授权操作。传递了 secureObject 安全对象,以便在安全对象被调用时可以检查其中的参数,例如:我们假设安全对象为 MethodInvocation 方法对象。我们可以通过 MethodInvocation 对象很容易的获得任何用户参数,然后在 AccessDecisionManager 中实现某种安全逻辑,以确定是否允许操作,如果访问被拒绝,我们应该在其实现类中抛出 AccessDeniedException 异常。
AbstractSecurityInterceptor 在启动时会调用 supports(ConfigAttribute) 方法,以确定 AccessDecisionManager 是否可以处理传递的ConfigAttribute。supports(Class) 方法由 security interceptor 安全拦截器实现调用,以确保配置的 AccessDecisionManager 支持 security interceptor 安全拦截器将呈现的安全对象的类型。
基于投票的 AccessDecisionManager 实现
虽然用户可以实现自己的 AccessDecisionManager 来控制授权,但是 Spring Security 提供了几个基于投票的 AccessDecisionManager 实现。 下图 Voting Decision Manager 投票决策管理器演示了相关的类。
RoleVoter:角色投票者
AuthenticatedVoter:认证投票人
Custom Voters:自定义选民
使用 AuthorizationFilter 授权 http servlet 请求
本节深入讨论基于 Servlet 的应用程序的授权是如何工作的。以Servlet架构和实现为基础。
AuthorizationFilter 取代了 FilterSecurityInterceptor。为了保持向后兼容,FilterSecurityInterceptor 仍然是默认的。本节讨论AuthorizationFilter 如何工作以及如何覆盖默认配置。
AuthorizationFilter 为 HttpServletRequests 提供授权。它作为 Security Filters 安全过滤器之一插入到 FilterChainProxy 中。
当声明 SecurityFilterChain 时,可以重写默认值。不使用 authorizeRequests(授权请求),而是使用 authorizeHttpRequests(授权Http请求),如下所示:
@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated();
)
return http.build();
}
它在许多方面改进了 authorizeRequests(授权请求):
- 使用简化的 AuthorizationManager API,而不是元数据源、配置属性、决策管理器和投票器。这简化了重用和定制
- 延迟 Authentication 身份验证查找。不是每个请求都需要查找身份验证,而是只在授权决策需要身份验证的请求中查找。
- 配置Bean的支持。
当使用 authorizeHttpRequests 而不是 authorizeRequests 时,则使用 AuthorizationFilter 而不是 FilterSecurityInterceptor。
- 首先,AuthorizationFilter 认证过滤器从 SecurityContextHolder 中获得一个 Authentication,它将其包装在 Supplier 供应者中以延迟查找。
- 其次,AuthorizationFilter 从 HttpServletRequest、HttpServletResponse 和 FilterChain 创建一个 FilterInvocation。
- 接下来,它将
Supplier<Authentication> 和 FilterInvocation 传递给 AuthorizationManager。
- 如果授权被拒绝,将抛出 AccessDeniedException。在这种情况下,ExceptionTranslationFilter 会处理 AccessDeniedException。
- 如果授予通过,AuthorizationFilter 将继续使用过滤器链,从而允许应用程序正常处理。
我们可以通过优先级添加更多的规则来配置 Spring Security,使其具有不同的规则。
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.mvcMatchers("/resources/**", "/signup", "/about").permitAll()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
.anyRequest().denyAll()
);
return http.build();
}
- 指定了多个授权规则。每个规则都是按照它们被声明的顺序来考虑的。
- 我们指定了多个 URL 配置,指定 URL 以
/resources 开头,或等于 /signup 、/about 的路径,任何用户都可以访问 - 任何以
/admin/” 开头的 URL 都将限于角色为 ROLE_ADMIN` 的用户。您会注意到,由于我们正在调用 hasRole 方法,所以不需要指定 “ROLE_” 前缀。 - 任何以 “/db/” 开头的 URL 都要求用户同时拥有 “ROLE_ADMIN” 和 “ROLE_DBA” 。 您会注意到,由于我们使用了 hasRole 表达式,我们不需要指定 “ROLE_” 前缀。
- 任何未配置的路径都将被拒绝
您可以采用基于 bean 的方法,来构造自己的 RequestMatcherDelegatingAuthorizationManager,如下所示:
@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
throws AuthenticationException {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().access(access)
)
return http.build();
}
@Bean
AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
RequestMatcher permitAll =
new AndRequestMatcher(
new MvcRequestMatcher(introspector, "/resources/**"),
new MvcRequestMatcher(introspector, "/signup"),
new MvcRequestMatcher(introspector, "/about"));
RequestMatcher admin = new MvcRequestMatcher(introspector, "/admin/**");
RequestMatcher db = new MvcRequestMatcher(introspector, "/db/**");
RequestMatcher any = AnyRequestMatcher.INSTANCE;
AuthorizationManager<HttpRequestServlet> manager = RequestMatcherDelegatingAuthorizationManager.builder()
.add(permitAll, (context) -> new AuthorizationDecision(true))
.add(admin, AuthorityAuthorizationManager.hasRole("ADMIN"))
.add(db, AuthorityAuthorizationManager.hasRole("DBA"))
.add(any, new AuthenticatedAuthorizationManager())
.build();
return (context) -> manager.check(context.getRequest());
}
您还可以为任何请求匹配器连接您自己的自定义授权管理器。
以下是将自定义授权管理器映射到 my/authorized/endpoint 的示例:
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.mvcMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager());
)
return http.build();
}
CustomAuthorizationManager 自定义授权管理器
或者你可以匹配所有请求:
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest.access(new CustomAuthorizationManager());
)
return http.build();
}
默认情况下,AuthorizationFilter 不适用于 DispatcherType.ERROR 和 DispatcherType.ASYNC。 我们可以使用 shouldFilterAllDispatcherTypes 方法配置 Spring Security 以将授权规则应用于所有调度程序类型:
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.shouldFilterAllDispatcherTypes(true)
.anyRequest.authenticated()
)
return http.build();
}
使用 FilterSecurityInterceptor 授权 HttpServletRequest
基于表达式的访问控制
安全对象实现
Method 安全
Domain 对象安全 (ACL)
授权事件
OAuth2
SAML2
防止漏洞利用
集成
配置
测试
附录
|