详细带你彻底搞懂 Spring Security 6.0 的实现原理

 ​

 博客主页:     南来_北往

系列专栏:Spring Boot实战


前言

Spring Security 6.0是一个功能强大且可扩展的身份验证和访问控制框架,它用于保护基于Java的应用程序。其主要目标是提供一个全面的安全解决方案,包括身份验证、授权、防止跨站请求伪造(CSRF)等功能。

  1. 身份验证(Authentication)

身份验证是确认用户身份的过程。Spring Security提供了多种身份验证机制,如表单登录、HTTP基本身份验证、OAuth2等。在Spring Security中,AuthenticationManager负责处理身份验证逻辑。当用户提供凭据(如用户名和密码)时,AuthenticationManager将创建一个Authentication对象,其中包含有关用户的信息。

  1. 授权(Authorization)

授权是确定用户可以访问哪些资源的过程。在Spring Security中,AccessDecisionManager负责处理授权逻辑。它根据用户的角色和权限来确定是否允许用户访问特定资源。AccessDecisionManager使用投票策略来决定是否允许访问。每个投票者可以根据其配置投赞成票、反对票或弃权票。如果赞成票多于反对票,则允许访问。

  1. 过滤器链(Filter Chain)

Spring Security使用一系列过滤器来处理请求。这些过滤器按照特定的顺序组成一个过滤器链。每个过滤器都负责处理特定的任务,如处理CSS和JavaScript资源、处理CORS、处理会话管理等。当请求进入应用程序时,过滤器链中的过滤器将按顺序处理请求。如果某个过滤器决定终止请求(例如,因为用户未经身份验证),则后续过滤器将不会执行。

  1. 安全上下文(Security Context)

安全上下文是一个包含有关当前用户和其权限的对象。在Spring Security中,SecurityContextHolder负责存储和管理安全上下文。当用户通过身份验证时,Authentication对象将被存储在SecurityContextHolder中。这使得应用程序可以在任何地方访问用户的凭据和权限信息。

  1. CSRF保护

跨站请求伪造(CSRF)是一种攻击,攻击者试图利用已登录用户的凭据来执行恶意操作。为了防止CSRF攻击,Spring Security提供了一个CsrfFilter,它会自动为每个表单添加一个隐藏的CSRF令牌。当表单提交时,CsrfFilter将验证令牌是否有效。如果令牌无效或不存在,请求将被拒绝。

这只是Spring Security 6.0实现原理的一个简要概述。要深入了解Spring Security的各个方面,建议您查阅官方文档和相关教程。

Java Web应用的Security实现基本思路

Java Web应用的Security实现基本思路主要包括以下几个方面:

  1. 身份验证(Authentication):确保用户的身份合法,通常使用用户名和密码进行验证。
  2. 授权(Authorization):确定用户具有哪些权限,以便控制用户可以访问的资源和执行的操作。
  3. 防止跨站请求伪造(CSRF):确保用户提交的请求是合法的,避免恶意网站利用用户在其他网站的认证状态发起攻击。
  4. 输入验证(Input Validation):对用户输入的数据进行验证,防止恶意数据注入攻击。
  5. 错误处理:正确处理异常情况,避免泄露敏感信息。
  6. 敏感数据保护:对敏感数据进行加密存储和传输,防止数据泄露。

以下是一个简单的Java Web应用Security实现代码示例:

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}

在这个示例中,我们使用了Spring Security框架来实现Web应用的安全功能。首先,我们通过@EnableWebSecurity注解启用了Web安全配置。然后,我们继承了WebSecurityConfigurerAdapter类并重写了configure方法来配置安全规则。

configure方法中,我们使用authorizeRequests方法定义了不同URL路径的访问权限。例如,只有具有"ADMIN"角色的用户才能访问/admin/**路径,而具有"USER"或"ADMIN"角色的用户都可以访问/user/**路径。其他所有请求都需要用户进行身份验证。

我们还配置了表单登录和注销功能,分别对应于/login页面和允许所有用户访问的注销操作。

Spring Security框架的基本架构和原理 

Spring Security是一个功能强大且可扩展的身份验证和访问控制框架,它提供了一种简单的方式来保护基于Java的应用程序。以下是Spring Security的基本架构和原理:

  1. 身份验证(Authentication):Spring Security通过AuthenticationManager接口来处理身份验证。这个接口负责从用户提交的凭据中获取认证信息,并将其封装成一个Authentication对象。常见的认证方式包括用户名密码认证、OAuth2认证等。

  2. 授权(Authorization):一旦用户被认证,Spring Security会使用AccessDecisionManager来决定用户是否有权访问特定的资源。AccessDecisionManager会根据用户的权限和请求的资源来判断是否允许访问。

  3. 过滤器链(Filter Chain):Spring Security使用一系列过滤器来处理HTTP请求。这些过滤器按照顺序执行,每个过滤器都负责一个特定的安全功能,如身份验证、授权、防止跨站请求伪造(CSRF)等。

  4. 安全上下文(Security Context):Spring Security使用SecurityContextHolder来存储当前用户的安全上下文信息。这个上下文包含了用户的认证信息、权限等信息,可以在应用程序的任何位置访问。

  5. 配置(Configuration):Spring Security的配置通常通过继承WebSecurityConfigurerAdapter类并重写其方法来实现。例如,可以配置登录页面、注销行为、URL访问规则等。

下面是一个简单的Spring Security配置示例:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 配置内存中的用户存储
        auth.inMemoryAuthentication()
            .withUser("user").password("{noop}password").roles("USER")
            .and()
            .withUser("admin").password("{noop}password").roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}

在这个示例中,我们配置了两个内存中的用户(user和admin),分别具有不同的角色。我们还定义了URL访问规则,要求访问/admin/**路径的用户必须具有"ADMIN"角色,而访问/user/**路径的用户必须具有"USER"或"ADMIN"角色。最后,我们配置了表单登录和注销功能。

Authentication身份认证

身份认证有很多种方式,大致可以分为以下4类:

  1. 标准的账号密码认证:这是很多网站都支持的方式,也是大家最熟悉的认证模式;

  2. 调用第三方服务或内部其它API进行认证:当服务自身无法直接获取用户的密码时,需要借助第三方服务或者内部API进行认证;

  3. 基于Token的认证:这是API服务一般使用的认知方式,通过令牌来进行身份验证;

  4. OAuth2或其它OpenID认证:这种方式广泛用于允许用户使用其它平台的身份信息进行登录,例如微信登录,Google登录等。

Spring Security支持大部分的认证方式,但不同的认证方式需要配置不同的Bean及其依赖Bean,否则很容易遇到各种异常和空指针。

本文重点讨论标准的账号密码认证方式。

如果你使用的是Spring Boot,那么Spring Boot Starter Security默认就配置了Form表单和Basic认证方式,其配置代码如下所示:

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnDefaultWebSecurity
    static class SecurityFilterChainConfiguration {
        @Bean
        @Order(SecurityProperties.BASIC_AUTH_ORDER)
        SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
            http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // 所有URL都需要认证用户
            http.formLogin(withDefaults()); // 支持form表单认证,默认配置提供了自动生成的登录和注销页面
            http.httpBasic(withDefaults()); // 支持HTTP Basic Authentication
            return http.build();
        }

    }
    // ...其它配置...
}

为了讨论方便,我们用下面的配置覆盖Spring Boot默认的配置,只支持Form表单认证方式,讨论它具体是如何实现的。 

@Configuration()  
public class MySecurityConfig {
    @Bean
    SecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // (1)
        http.formLogin(withDefaults()); // (2)
        return http.build();
    }
}
  1. authorizeHttpRequests方法用于配置每个请求的权限控制,这里要求所有请求都要通过认证后才能访问。实际上,这个方法配置的更多是鉴权相关的内容,跟身份认证的关联较小,它本质上是增加了一个AuthorizationFilter用于鉴权,具体细节在鉴权部分会详细说明。

  2. http.formLogin方法提供了Form表单认证的方式,withDefaults方法是Form表单认证的默认配置。这段配置的作用就是增加了用于账号密码认证的UsernamePasswordAuthenticationFilter,以及自动生成登录页面和注销页面的DefaultLogoutPageGeneratingFilterDefaultLogoutPageGeneratingFilter共3个Security Filter。值得注意的是,登录页面和注销页面这两个Filter是配合DefaultLoginPageConfigurer配置一起注册的。如果你通过formLogin.loginPage提供了自定义的登录页面,那么这两个Filter就不会被注册。

在本节中,我们主要讨论身份认证的实现,因此,接下来将详细探究Form表单认证方式中UsernamePasswordAuthenticationFilter的实现。

AbstractAuthenticationProcessingFilter

对于Filter,我们重点分析它的doFilter方法的源码。实际上,它继承了抽象类AbstractAuthenticationProcessingFilter,而这个抽象类的doFilter是一个模板方法,定义了整个认证流程。其核心流程非常简单,伪代码如下:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    // 首先判断该请求是否是认证请求或者登录请求
    if (!requiresAuthentication(request, response)) { // (1)
        chain.doFilter(request, response);
        return;
    }
    try {
        Authentication authenticationResult = attemptAuthentication(request, response); // (2) 实际认证逻辑
        // 认证成功
        successfulAuthentication(request, response, chain, authenticationResult); // (3)
    }
    catch (AuthenticationException ex) {
        // 认证失败
        unsuccessfulAuthentication(request, response, ex); // (4)
    }
}
  1. 首先requiresAuthentication方法用于判断当前请求是否为认证请求或者登录请求,例如通常是POST /login。只有在登录认证的情况下,才需要通过这个Filter;

  2. attempAuthentication方法是实际的认证逻辑,这是一个抽象方法,具体的逻辑由子类重写实现。它的规范行为是,如果认证成功,应该返回认证结果Authentication,否则以抛出异常AuthenticationException的方式表示认证失败;

  3. successfulAuthentication认证成功后,该方法会将Authentication对象放到Security Context中,这是非常关键的一步,后续需要认证结果的时候都是从Security Context获取的,比如鉴权Filter。此外,该方法还会处理其它一些相关功能,比如RememberMe,事件发布,最后再调用AuthenticationSuccessHandler

  4. unsuccessfulAuthentication :在认证失败后,它会清空Security Context,调用RememberMe相关服务和AuthenticationFailureHandler来处理认证失败后的回调逻辑,比如跳转到错误页面。

 

Authentication模型

在这里,我们涉及到了一个非常重要的数据模型——Authentication,它是一个接口类型,它既是对认证结果的一个抽象表示,同时也是对认证请求的一个抽象,通常也被称为认证Token。它的方法都比较抽象,定义如下:

public interface Authentication extends Principal, Serializable {
    // 当前认证用户拥有的权限列表
    Collection<? extends GrantedAuthority> getAuthorities();
    // 用户的一个身份标识,通常就是用户名
    Object getPrincipal();
    // 可用于证明用户身份的一个凭证,通常就是用户密码
    Object getCredentials();
    // 当前用户是否认证通过
    boolean isAuthenticated();
    // 更新用户的认证状态
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
    // 获取附加的详情信息,比如原始的Http请求体等。
    Object getDetails();
}

具体的Authentication实现一般都命名为XXXToken,大部分都继承自抽象类AbstractAuthenticationToken,比如表示标准的用户名密码认证结果的UsernamePasswordAuthenticationToken,表示匿名登录用户认证结果的AnonymousAuthenticationToken等等,你也可以完全实现自己的Authentication

attempAuthentication方法

接下来,我们看下UsernamePasswordAuthenticationFilter的认证具体实现方法attempAuthentication,它的源码如下:

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
        throws AuthenticationException {
    // 默认只支持POST请求
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    // 从form表单获取用户名和密码
    String username = obtainUsername(request);
    username = (username != null) ? username.trim() : "";
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    // 构建一个用于认证的请求
    UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
            password);
    // 附加详细信息,比如请求体,有些认证方式需要除了用户名密码外更多的信息
    setDetails(request, authRequest);
    // 委托给AuthenticationManager做具体的认证
    return this.getAuthenticationManager().authenticate(authRequest);
}

 这个方法非常简单,它主要进行一些前置校验工作,从请求体中获取用户名和密码,并构建认证请求对象。然后,剩余的认证工作都是委托给AuthenticationManager接口来完成的,该接口的定义如下:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AuthenticationManager和AuthenticationProvider

AuthenticationManager接口只有一个方法,它的入参和出参都是Authentication对象。通常情况下,入参提供了必要的认证信息,例如用户名和密码。而在认证成功后,该方法会返回认证结果,并附加认证状态,用户拥有的权限列表等信息。如果认证失败,它会抛出AuthenticationException异常类的子类,其中包括DisabledExceptionLockedExceptionBadCredentialsException等账号相关的异常。

AuthenticationManager接口定义了Spring Security的认证行为。你可以提供自定义的实现,Spring Security也提供了一个通用的实现类ProviderManagerProviderManager将具体的认证工作委托给一系列的AuthenticationProvider

每个AuthenticationProvider对应不同的认证方式。比如最常见的用户名密码的认证实现是DaoAuthenticationProvider,而JwtAuthenticationProvider提供了JWT Token的认证。你可以通过添加不同的AuthenticationProvider的方式,在同一个服务内支持多种类型的认证方式,比如需要调用其它API检验密码的情况,就需要自定义AuthenticationProvider

此外,ProviderManager还可以配置父级AuthenticationManager,当这个ProviderManager的所有AuthenticationProvider都不支持所需的认证方式时,它会继续委托给父级的AuthenticationManager,而该父级通常也是一个ProviderManager类型。

UserDetailsService和PasswordEncoder

DaoAuthenticationProvider是最常用的认证实现之一,它通过UserDetailsServicePasswordEncoder来验证用户名和密码。

UserDetailsService的作用是查找用户信息UserDetails,这些信息包括用户密码,状态,权限列表等。用户信息可以存储在内存,数据库或者其它任何地方。Spring Security默认的配置是内存存储,对应的UserDetailsService实现是InMemoryUserDetailsManager,而数据库存储则对应JdbcUserDetailsManager

UserDetailsService获取到用户密码后,需要通过PasswordEncoder来验证密码的正确性。因为密码一般都不应该以明文形式存储,实际存储的是按一定规则编码后的文本,Spring Security支持多种编码方式,例如bcryptargon2scryptpbkdf2等。你可以配置PasswordEncoder Bean来选择不同的编码方式。都是请注意,内置的编码方式默认对编码后的文本有一个格式要求,就是必须有类似{bcrypt}的前缀来表示编码方式。

总结

本文重点分析了Spring Security的源码和架构,帮助读者理解其实现原理。由于篇幅有限,本文只覆盖了身份认证和鉴权模块的核心逻辑,很多特性没有涉及,包括Session管理,Remember Me服务,异常分支和错误处理等等,不过有了上述的基础知识,读者完全可以自己分析源码并深入理解这些特性。 

  • 16
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值