博客主页: 南来_北往
系列专栏:Spring Boot实战
前言
Spring Security 6.0是一个功能强大且可扩展的身份验证和访问控制框架,它用于保护基于Java的应用程序。其主要目标是提供一个全面的安全解决方案,包括身份验证、授权、防止跨站请求伪造(CSRF)等功能。
- 身份验证(Authentication)
身份验证是确认用户身份的过程。Spring Security提供了多种身份验证机制,如表单登录、HTTP基本身份验证、OAuth2等。在Spring Security中,AuthenticationManager
负责处理身份验证逻辑。当用户提供凭据(如用户名和密码)时,AuthenticationManager
将创建一个Authentication
对象,其中包含有关用户的信息。
- 授权(Authorization)
授权是确定用户可以访问哪些资源的过程。在Spring Security中,AccessDecisionManager
负责处理授权逻辑。它根据用户的角色和权限来确定是否允许用户访问特定资源。AccessDecisionManager
使用投票策略来决定是否允许访问。每个投票者可以根据其配置投赞成票、反对票或弃权票。如果赞成票多于反对票,则允许访问。
- 过滤器链(Filter Chain)
Spring Security使用一系列过滤器来处理请求。这些过滤器按照特定的顺序组成一个过滤器链。每个过滤器都负责处理特定的任务,如处理CSS和JavaScript资源、处理CORS、处理会话管理等。当请求进入应用程序时,过滤器链中的过滤器将按顺序处理请求。如果某个过滤器决定终止请求(例如,因为用户未经身份验证),则后续过滤器将不会执行。
- 安全上下文(Security Context)
安全上下文是一个包含有关当前用户和其权限的对象。在Spring Security中,SecurityContextHolder
负责存储和管理安全上下文。当用户通过身份验证时,Authentication
对象将被存储在SecurityContextHolder
中。这使得应用程序可以在任何地方访问用户的凭据和权限信息。
- CSRF保护
跨站请求伪造(CSRF)是一种攻击,攻击者试图利用已登录用户的凭据来执行恶意操作。为了防止CSRF攻击,Spring Security提供了一个CsrfFilter
,它会自动为每个表单添加一个隐藏的CSRF令牌。当表单提交时,CsrfFilter
将验证令牌是否有效。如果令牌无效或不存在,请求将被拒绝。
这只是Spring Security 6.0实现原理的一个简要概述。要深入了解Spring Security的各个方面,建议您查阅官方文档和相关教程。
Java Web应用的Security实现基本思路
Java Web应用的Security实现基本思路主要包括以下几个方面:
- 身份验证(Authentication):确保用户的身份合法,通常使用用户名和密码进行验证。
- 授权(Authorization):确定用户具有哪些权限,以便控制用户可以访问的资源和执行的操作。
- 防止跨站请求伪造(CSRF):确保用户提交的请求是合法的,避免恶意网站利用用户在其他网站的认证状态发起攻击。
- 输入验证(Input Validation):对用户输入的数据进行验证,防止恶意数据注入攻击。
- 错误处理:正确处理异常情况,避免泄露敏感信息。
- 敏感数据保护:对敏感数据进行加密存储和传输,防止数据泄露。
以下是一个简单的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 {@Overrideprotected 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的基本架构和原理:
-
身份验证(Authentication):Spring Security通过
AuthenticationManager
接口来处理身份验证。这个接口负责从用户提交的凭据中获取认证信息,并将其封装成一个Authentication
对象。常见的认证方式包括用户名密码认证、OAuth2认证等。 -
授权(Authorization):一旦用户被认证,Spring Security会使用
AccessDecisionManager
来决定用户是否有权访问特定的资源。AccessDecisionManager
会根据用户的权限和请求的资源来判断是否允许访问。 -
过滤器链(Filter Chain):Spring Security使用一系列过滤器来处理HTTP请求。这些过滤器按照顺序执行,每个过滤器都负责一个特定的安全功能,如身份验证、授权、防止跨站请求伪造(CSRF)等。
-
安全上下文(Security Context):Spring Security使用
SecurityContextHolder
来存储当前用户的安全上下文信息。这个上下文包含了用户的认证信息、权限等信息,可以在应用程序的任何位置访问。 -
配置(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 {@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 配置内存中的用户存储auth.inMemoryAuthentication().withUser("user").password("{noop}password").roles("USER").and().withUser("admin").password("{noop}password").roles("ADMIN");}@Overrideprotected 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类:
-
标准的账号密码认证:这是很多网站都支持的方式,也是大家最熟悉的认证模式;
-
调用第三方服务或内部其它API进行认证:当服务自身无法直接获取用户的密码时,需要借助第三方服务或者内部API进行认证;
-
基于Token的认证:这是API服务一般使用的认知方式,通过令牌来进行身份验证;
-
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)@ConditionalOnDefaultWebSecuritystatic 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 Authenticationreturn http.build();}}// ...其它配置...
}
为了讨论方便,我们用下面的配置覆盖Spring Boot默认的配置,只支持Form表单认证方式,讨论它具体是如何实现的。
@Configuration()
public class MySecurityConfig {@BeanSecurityFilterChain mySecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated()); // (1)http.formLogin(withDefaults()); // (2)return http.build();}
}
-
authorizeHttpRequests
方法用于配置每个请求的权限控制,这里要求所有请求都要通过认证后才能访问。实际上,这个方法配置的更多是鉴权相关的内容,跟身份认证的关联较小,它本质上是增加了一个AuthorizationFilter
用于鉴权,具体细节在鉴权部分会详细说明。 -
http.formLogin
方法提供了Form表单认证的方式,withDefaults
方法是Form表单认证的默认配置。这段配置的作用就是增加了用于账号密码认证的UsernamePasswordAuthenticationFilter
,以及自动生成登录页面和注销页面的DefaultLogoutPageGeneratingFilter
和DefaultLogoutPageGeneratingFilter
共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)}
}
-
首先
requiresAuthentication
方法用于判断当前请求是否为认证请求或者登录请求,例如通常是POST /login
。只有在登录认证的情况下,才需要通过这个Filter; -
attempAuthentication
方法是实际的认证逻辑,这是一个抽象方法,具体的逻辑由子类重写实现。它的规范行为是,如果认证成功,应该返回认证结果Authentication
,否则以抛出异常AuthenticationException
的方式表示认证失败; -
successfulAuthentication
:认证成功后,该方法会将Authentication
对象放到Security Context中,这是非常关键的一步,后续需要认证结果的时候都是从Security Context获取的,比如鉴权Filter。此外,该方法还会处理其它一些相关功能,比如RememberMe,事件发布,最后再调用AuthenticationSuccessHandler
; -
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
异常类的子类,其中包括DisabledException
,LockedException
和BadCredentialsException
等账号相关的异常。
AuthenticationManager
接口定义了Spring Security的认证行为。你可以提供自定义的实现,Spring Security也提供了一个通用的实现类ProviderManager
。ProviderManager
将具体的认证工作委托给一系列的AuthenticationProvider
。
每个AuthenticationProvider
对应不同的认证方式。比如最常见的用户名密码的认证实现是DaoAuthenticationProvider
,而JwtAuthenticationProvider
提供了JWT Token的认证。你可以通过添加不同的AuthenticationProvider
的方式,在同一个服务内支持多种类型的认证方式,比如需要调用其它API检验密码的情况,就需要自定义AuthenticationProvider
。
此外,ProviderManager
还可以配置父级AuthenticationManager
,当这个ProviderManager
的所有AuthenticationProvider
都不支持所需的认证方式时,它会继续委托给父级的AuthenticationManager
,而该父级通常也是一个ProviderManager
类型。
UserDetailsService和PasswordEncoder
DaoAuthenticationProvider
是最常用的认证实现之一,它通过UserDetailsService
和PasswordEncoder
来验证用户名和密码。
UserDetailsService
的作用是查找用户信息UserDetails
,这些信息包括用户密码,状态,权限列表等。用户信息可以存储在内存,数据库或者其它任何地方。Spring Security默认的配置是内存存储,对应的UserDetailsService
实现是InMemoryUserDetailsManager
,而数据库存储则对应JdbcUserDetailsManager
。
从UserDetailsService
获取到用户密码后,需要通过PasswordEncoder
来验证密码的正确性。因为密码一般都不应该以明文形式存储,实际存储的是按一定规则编码后的文本,Spring Security支持多种编码方式,例如bcrypt
,argon2
,scrypt
,pbkdf2
等。你可以配置PasswordEncoder
Bean来选择不同的编码方式。都是请注意,内置的编码方式默认对编码后的文本有一个格式要求,就是必须有类似{bcrypt}
的前缀来表示编码方式。
总结
本文重点分析了Spring Security的源码和架构,帮助读者理解其实现原理。由于篇幅有限,本文只覆盖了身份认证和鉴权模块的核心逻辑,很多特性没有涉及,包括Session管理,Remember Me服务,异常分支和错误处理等等,不过有了上述的基础知识,读者完全可以自己分析源码并深入理解这些特性。