Spring Security是如何工作的?

嗨,你好呀,我是猿java

Spring Security 是一个帮助保护企业应用程序的框架,通过与 Spring MVC、Spring Webflux 或 Spring Boot 集成,创建一个功能强大且高度可定制的身份验证和访问控制框架。在本文中,我们将解释核心概念,并仔细研究 Spring Security 提供的默认配置以及它们的工作原理。

什么是Spring Security?

Spring Security 是一个用于保护Spring应用程序的框架,它提供了全面的安全服务,包括身份验证、授权、加密、以及防止常见攻击(如CSRF和会话固定攻击)。Spring Security 的设计使其易于扩展,可以根据特定需求进行高度定制。

Spring Security的核心组件

要了解 Spring Security 默认配置的工作原理,我们首先需要了解其三大核心组件:

  • Servlet Filters
  • Authentication
  • Authorization

Servlet Filters

当 DefaultSecurityFilterChain 请求到达 DispatcherServlet 之前触发了一连串过滤器。DispatcherServlet 是 Web 框架中的一个关键组件,用于处理传入的 Web 请求并将其分派给适当的处理程序进行处理。

为了理解 FilterChain 是如何工作的,让我们看一下 Spring Security 文档中的流程图:

img.png

接下来,让我们看一下参与 filter chain 的核心组件:

  • DelegatingFilterProxy:它是 Spring 提供的一个 servlet 过滤器,充当 Servlet 容器和 Spring 应用程序上下文之间的桥梁。DelegatingFilterProxy 类负责将实现 javax.servlet.Filter 的任何类连接到过滤器链中。
  • FilterChainProxy Spring security 在内部创建一个名为 springSecurityFilterChain 的 FilterChainProxy bean,该 bean 包装在 DelegatingFilterProxy 中。FilterChainProxy 是一个过滤器,它根据安全配置链接多个过滤器。因此,DelegatingFilterProxy 将请求委托给 FilterChainProxy,后者确定要调用的过滤器。
  • SecurityFilterChain:SecurityFilterChain 中的安全过滤器是在 FilterChainProxy 中注册的 bean。一个应用程序可以有多个 SecurityFilterChain。FilterChainProxy 在 HttpServletRequest 上使用 RequestMatcher 接口来确定需要调用哪个 SecurityFilterChain。

通过上述分析可以看出 Spring Security 会提供了一个默认的过滤器链,该过滤器链调用一组预定义的和有序的过滤器,因此,我们接下来要分析几个重要的过滤器角色:

  • org.springframework.security.web.csrf.CsrfFilter :默认情况下,此过滤器将CSRF保护应用于所有REST端点。要了解有关 Spring Boot 和 Spring Security 中的 CSRF 功能的更多信息,请参阅此文章。
  • org.springframework.security.web.authentication.logout.LogoutFilter :当用户注销应用程序时,将调用此过滤器。将调用 LogoutHandler 的默认注册实例,这些实例负责使会话无效并清除 SecurityContext。接下来,LogoutSuccessHandler 的默认实现将用户重定向到新页面 (/login?logout)。
  • org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter :使用启动时提供的默认凭据验证URL(/login)的用户名和密码。
  • org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter :在/login处生成默认登录页面html
  • org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter:在/login?logout处生成默认注销页面html
  • org.springframework.security.web.authentication.www.BasicAuthenticationFilter :此过滤器负责处理具有Authorization,Basic Authentication scheme,Base64编码用户名密码的HTTP请求头的任何请求。身份验证成功后,Authentication 对象将放置在 SecurityContextHolder 中。
  • org.springframework.security.web.authentication.AnonymousAuthenticationFilter :如果在SecurityContext中找不到Authentication对象,它将创建一个具有主体anonymousUser和role ROLE_ANONYMOUS的对象。
  • org.springframework.security.web.access.ExceptionTranslationFilter :处理过滤器链中抛出的AccessDeniedException和AuthenticationException。对于 AuthenticationException,需要 AuthenticationEntryPoint 的实例来处理响应。对于 AccessDeniedException,此筛选器将委托给 AccessDeniedHandler,其默认实现为 AccessDeniedHandlerImpl。
  • org.springframework.security.web.access.intercept.FilterSecurityInterceptor :此过滤器负责授权在请求到达控制器之前通过过滤器链的每个请求。

Authentication

Authentication(身份验证)是验证用户凭据并确保其有效性的过程,接下来看看 spring 框架是如何验证创建的默认 credentials凭据,其具体步骤如下:

  • 步骤.1:当启用 FormLogin 时,即当向 URL /login发出请求时,UsernamePasswordAuthenticationFilter将作为安全过滤器链的一部分被调用。此类是基 AbstractAuthenticationProcessingFilter 的特定实现。尝试进行身份验证时,筛选器会将请求转发到 AuthenticationManager。
  • 步骤2:UsernamePasswordAuthenticationToken是Authentication接口的实现。此类指定身份验证机制必须通过用户名-密码进行。
  • 步骤.3:获得身份验证详细信息后,AuthenticationManager 尝试借助 AuthenticationProvider 的适当实现对请求进行身份验证,并返回完全经过身份验证的 Authentication 对象。默认实现是 DaoAuthenticationProvider,它从 UserDetailsService 中检索用户详细信息。如果身份验证失败,则会引发 AuthenticationException。
  • 步骤.4:UserDetailsService 的 loadUserByUsername(username) 方法返回包含用户数据的 UserDetails 对象。如果未找到具有给定用户名的用户,则会引发 UsernameNotFoundException。
  • 步骤 5:身份验证成功后,SecurityContext 将使用当前经过身份验证的用户进行更新。

为了理解上述概述的步骤,让我们看一下 Spring Security 文档中定义的身份验证架构图:

img.png

ProviderManager 是 AuthenticationManager 最常见的实现,如图所示,ProviderManager 将请求委托给已配置的 AuthenticationProvider 列表,查询每个 AuthenticationProvider 以查看它是否可以执行身份验证。如果身份验证失败并显示 ProviderNotFoundException(一种特殊类型的 AuthenticationException),则表明 ProviderManager 不支持传递的身份验证类型。此体系结构允许我们在同一应用程序中配置多种身份验证类型。

AuthenticationEntryPoint 是一个接口,它充当身份验证的入口点,用于确定客户端在请求资源时是否包含了有效的凭据,否则,将使用接口的适当实现从客户端请求凭据。

那么,Authentication 对象是如何绑定整个身份验证过程?身份验证接口用于以下用途:

  • 向 AuthenticationManager 提供用户凭据。
  • 表示 SecurityContext 中当前经过身份验证的用户。每个身份验证实例都必须包含
    • principal - 这是标识用户的 UserDetails 实例。
    • credentials
    • authorities - GrantedAuthority GrantedAuthority 的实例在授权过程中起着重要作用。

Authorization

Authorization(授权)是确保访问资源的用户或系统具有有效权限的过程。
在 Spring 安全过滤器链中,FilterSecurityInterceptor 触发授权检查。从过滤器执行的顺序可以看出,身份验证在授权之前运行。在用户成功通过身份验证后,此筛选器将检查有效权限。如果授权失败,将引发 AccessDeniedException。

授予权限

如上一部分所示,每个用户实例都包含一个 GrantedAuthority(授予权限)对象的列表。GrantedAuthority 是一个具有单一方法的接口:

1
2
3
public interface GrantedAuthority extends Serializable {
String getAuthority();
}

默认情况下,Spring 安全性调用具体的 GrantedAuthority 实现 SimpleGrantedAuthority。SimpleGrantedAuthority 允许我们将角色指定为 String,并自动将它们映射到 GrantedAuthority 实例中。AuthenticationManager 负责将 GrantedAuthority 对象列表插入到 Authentication 对象中。然后,AccessDecisionManager 使用 getAuthority() 来确定授权是否成功。

授予的权限与角色

Spring Security 分别使用 hasAuthority() 和 hasRole() 方法通过授予的权限和角色提供授权支持。这些方法用于基于表达式的安全性,并且是接口 SecurityExpressionOperations 的一部分。

在大多数情况下,这两种方法可以互换使用,最显着的区别是 hasRole() 不需要指定 ROLE 前缀,而 hasAuthority() 需要显式指定完整的字符串。例如,hasAuthority(“ROLE_ADMIN”) 和 hasRole(“ADMIN”) 执行相同的任务。

附加说明

Spring 允许我们使用 @PreAuthorize 和 @PostAuthorize 注解来配置方法级证券。顾名思义,它们允许我们在方法执行之前和之后对用户进行授权。可以在 Spring 表达式语言 (SpEL) 中指定授权检查的条件。我们将在后面的部分中查看一些示例。
我们可以通过公开 GrantedAuthorityDefaults bean 来配置授权规则以使用不同的前缀(而不是 ROLE_)。

如何使用?

这里以一个简单的登录功能为例进行说明。

首先,我们需要添加 Spring Security 的依赖,这里以 Maven 为例:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

接着,编写一个测试的 Controller,如下:

1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping
public class TestController {

@GetMapping("/login")
public String login(){
// 业务逻辑
return "success";
}
}

最后,使用浏览器访问地址:127.0.0.1:8080/login,会弹出一个登录页面,如下图:

img.png

通过上面 3步,我们在没有写一行前端代码的前提下实现了一个登录功能,默认情况下 Spring Security 会生成默认的密钥,我们也可以在 application.yml 中配置用户名和秘密,配置如下:

1
2
3
4
5
spring:
security:
user:
name: test
password: test

上述示例,我们展示了 Spring Security 最简单的一个使用,另外它还支持自定义配置,基于 Java 的配置是 Spring Security推荐的配置方式。通过扩展 WebSecurityConfigurerAdapter,可以覆盖 configure方法来配置认证和授权规则,如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER")
.and()
.withUser("admin").password("{noop}admin").roles("ADMIN");
}
}

Spring Security使用场景

Spring Security 的使用场景非常多,下面列举一些常见的使用场景:

认证和授权

  • 用户认证:确保用户是系统中合法的用户,常用的认证方式包括表单登录、HTTP Basic 认证、OAuth2 等。
  • 用户授权:根据用户角色或权限,控制用户对特定资源或操作的访问。

保护 REST API

  • 保护 RESTful 服务,确保只有经过认证和授权的用户才能访问 API 端点。
  • 使用 OAuth2 或 JWT(JSON Web Token)进行令牌认证。

方法级别安全

  • 使用注解(如 @PreAuthorize、@Secured 等)在服务层或控制层的方法上进行安全控制。
  • 根据用户角色或权限动态控制方法的访问。

会话管理

  • 处理用户会话,包括会话超时、并发会话控制等。
  • 防止会话固定攻击(Session Fixation Attack)。

防止跨站请求伪造(CSRF)

  • 动生成和验证 CSRF 令牌,防止恶意网站伪造请求。

安全事件监控

  • 记录和监控安全事件,如登录尝试、登录失败、访问被拒绝等。
  • 集成日志系统,帮助分析和审计安全事件。

单点登录(SSO)

  • 实现跨多个应用程序的单点登录,使用 OAuth2、OpenID Connect 等协议。

加密和解密

  • 处理敏感数据的加密和解密,如用户密码的存储和校验。

总结

在本文中,我们介绍了 Spring Security 的基本概念以及其三大核心组件,此外,我们还解释了 Spring 提供的默认配置以及如何覆盖它们。

Spring Security 的功能很强大也很灵活性,但是入门有难度,初学者一定要抓大放小,抓住主要流程,其核心原理是通过一些列的 Servlet Filters来实现。

参考资料

Getting started with Spring Security and Spring Boot

spring-security官方文档

学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。

drawing