type
status
date
Jul 24, 2024 01:58 PM
slug
summary
category
tags
password
icon
总体框架架构概览图SecurityContextHolder获取当前用户的信息AuthenticationSpring Security是如何完成身份认证的?AuthenticationManagerDaoAuthenticationProviderUserDetails与UserDetailsService核心配置@EnableWebSecurityWebSecurityConfigurationAuthenticationConfigurationWebSecurityConfigurerAdapterHttpSecurity常用配置WebSecurityBuilderAuthenticationManagerBuilder
总体框架
架构概览图

SpringSecurity的本质其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

- UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
- FilterSecurityInterceptor:负责权限校验的过滤器。
我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。

SecurityContextHolder
SecurityContextHolder
用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。SecurityContextHolder
默认使用ThreadLocal
策略来存储认证信息。看到ThreadLocal
也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持,SecurityContextHolder
的策略则需要被替换。获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:
getAuthentication()
返回了认证信息,再次getPrincipal()
返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。Authentication
Authentication
是spring security
包中的接口,直接继承自Principal
类,而Principal
是位于java.security
包中的。可以见得,Authentication
在spring security
中是最高级别的身份/认证的抽象。
- 由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。在获取当前用户的信息中,
authentication.getPrincipal()
返回了一个Object
,我们将Principal
强转成了Spring Security
中最常用的UserDetails
,这在Spring Security中非常常见,接口返回Object,使用instanceof判断类型,强转成对应的具体实现类。接口详细解读如下: getAuthorities()
,权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。getCredentials()
,密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。getDetails()
,细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值。getPrincipal()
,最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。
Spring Security是如何完成身份认证的?

- 用户名和密码被过滤器获取到,封装成
Authentication
,通常情况下是UsernamePasswordAuthenticationToken
这个实现类。
AuthenticationManager
身份管理器负责验证这个Authentication
- 认证成功后,
AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication
实例。
SecurityContextHolder
安全上下文容器将第3步填充了信息的Authentication
,通过SecurityContextHolder.getContext().setAuthentication(…)
方法,设置到其中。
这是一个抽象的认证流程,而整个过程中,如果不纠结于细节,下面是一个简单的流程,在实际使用中,整个流程会变得更加的复杂:
AuthenticationManager
初次接触Spring Security的朋友相信会被
AuthenticationManager
,ProviderManager
,AuthenticationProvider
……这么多相似的Spring认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager
内部会维护一个List<AuthenticationProvider>
列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。只保留了关键认证部分的ProviderManager源码:
ProviderManager
中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager
会抛出一个ProviderNotFoundException异常。到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。
DaoAuthenticationProvider
AuthenticationProvider
最常用的一个实现便是DaoAuthenticationProvider
。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。DaoAuthenticationProvider UML


按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了
UsernamePasswordAuthenticationToken
,而根据用户名加载用户的任务则是交给了UserDetailsService
,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。如果你已经被这些概念搞得晕头转向了,不妨这么理解DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
UserDetails与UserDetailsService
它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的
getCredentials()
与UserDetails中的getPassword()
需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。UserDetailsService
和AuthenticationProvider
两者的职责常常被人们搞混,关于他们的问题在文档的FAQ和issues中屡见不鲜。记住一点即可,UserDetailsService
只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService
,通常这更加灵活。核心配置
当配置了上述的javaconfig之后,我们的应用便具备了如下的功能:
@EnableWebSecurity
注解使得SpringMVC集成了Spring Security的web安全支持。另外,WebSecurityConfig配置类同时集成了WebSecurityConfigurerAdapter
,重写了其中的特定方法,用于自定义Spring Security配置。
configure(HttpSecurity)
定义了哪些URL路径应该被拦截,如字面意思所描述:”/“, “/home”允许所有人访问,”/login”作为登录入口,也被允许访问,而剩下的”/hello”则需要登录后才可以访问。也就是说,除了“/”,”/home”(首页),”/login”(登录),”/logout”(注销),之外,其他路径都需要认证。
configureGlobal(AuthenticationManagerBuilder)
在内存中配置一个用户,admin/admin分别是用户名和密码,这个用户拥有USER角色。
- 防止CSRF攻击
- Session Fixation protection(Spring Session,防止别人篡改sessionId)
- Security Header(添加一系列和Header相关的控制)
- HTTP Strict Transport Security for secure requests
- 集成X-Content-Type-Options
- 缓存控制
- X-Frame-Options integration to help prevent Clickjacking(iframe被默认禁止使用)
- 为Servlet API集成了如下的几个方法
@EnableWebSecurity
我们自己定义的配置类WebSecurityConfig加上了
@EnableWebSecurity
注解,同时继承了WebSecurityConfigurerAdapter。你可能会在想谁的作用大一点,毫无疑问@EnableWebSecurity
起到决定性的配置作用,它其实是个组合注解。@Import
是springboot提供的用于引入外部的配置的注解,可以理解为:@EnableWebSecurity
注解激活了@Import
注解中包含的配置类。SpringWebMvcImportSelector
的作用是判断当前的环境是否包含springmvc,因为spring security可以在非spring环境下使用,为了避免DispatcherServlet的重复配置,所以使用了这个注解来区分。
WebSecurityConfiguration
顾名思义,是用来配置web安全的。
@EnableGlobalAuthentication
注解的源码如下:
注意点同样在
@Import
之中,它实际上激活了AuthenticationConfiguration
这样的一个配置类,用来配置认证相关的核心类。也就是说:
@EnableWebSecurity
完成的工作便是加载了WebSecurityConfiguration
,AuthenticationConfiguration
这两个核心配置类,也就此将spring security的职责划分为了配置安全信息,配置认证信息两部分。WebSecurityConfiguration
在这个配置类中,有一个非常重要的Bean被注册了。
在未使用springboot之前,大多数人都应该对“
springSecurityFilterChain
”这个名词不会陌生,它是spring security的核心过滤器,是整个认证的入口。在曾经的XML配置中,想要启用spring security,需要在web.xml中进行如下配置:而在springboot集成之后,这样的XML被java配置取代。WebSecurityConfiguration中完成了声明springSecurityFilterChain的作用,并且最终交给DelegatingFilterProxy这个代理类,负责拦截请求(注意DelegatingFilterProxy这个类不是spring security包中的,而是存在于web包中,spring使用了代理模式来实现安全过滤的解耦)。
AuthenticationConfiguration
AuthenticationConfiguration
的主要任务,便是负责生成全局的身份认证管理者AuthenticationManager
。在Spring Security的认证体系,AuthenticationManager
便是最核心的身份认证管理器。WebSecurityConfigurerAdapter
适配器模式在spring中被广泛的使用,在配置中使用
Adapter
的好处便是,我们可以选择性地配置想要修改的那一部分配置,而不用覆盖其他不相关的配置。WebSecurityConfigurerAdapter中我们可以选择自己想要修改的内容,来进行重写,而其提供了三个configure重载方法,是我们主要关心的:
由参数就可以知道,分别是对
AuthenticationManagerBuilder
,WebSecurity
,HttpSecurity
进行个性化的配置。HttpSecurity常用配置
上述是一个使用Java Configuration配置
HttpSecurity
的典型配置,其中http
作为根开始配置,每一个and()
对应了一个模块的配置(等同于xml
配置中的结束标签),并且and()
返回了HttpSecurity
本身,于是可以连续进行配置。他们配置的含义也非常容易通过变量本身来推测,authorizeRequests()
配置路径拦截,表明路径访问所对应的权限,角色,认证信息。
formLogin()
对应表单认证相关的配置
logout()
对应了注销相关的配置
httpBasic()
可以配置basic登录
- ……
他们分别代表了http请求相关的安全配置,这些配置项无一例外的返回了
Configurer
类,而所有的http
相关配置可以通过查看HttpSecurity
的主要方法得知:
需要对http协议有一定的了解才能完全掌握所有的配置,不过,springboot和spring security的自动配置已经足够使用了。其中每一项Configurer(e.g.FormLoginConfigurer,CsrfConfigurer)都是HttpConfigurer的细化配置项。
WebSecurityBuilder
这个配置中并不会出现太多的配置信息。
AuthenticationManagerBuilder
想要在
WebSecurityConfigurerAdapter
中进行认证相关的配置,可以使用configure(AuthenticationManagerBuilder auth)
暴露一个AuthenticationManager
的建造器:AuthenticationManagerBuilder
。如上所示,我们便完成了内存中用户的配置。细心的朋友会发现,在前面的文章中我们配置内存中的用户时,似乎不是这么配置的,而是:
如果你的应用只有唯一一个WebSecurityConfigurerAdapter,那么他们之间的差距可以被忽略,从方法名可以看出两者的区别:使用
@Autowired
注入的AuthenticationManagerBuilder是全局的身份认证器,作用域可以跨越多个WebSecurityConfigurerAdapter
,以及影响到基于Method的安全控制;而 protected configure()
的方式则类似于一个匿名内部类,它的作用域局限于一个WebSecurityConfigurerAdapter
内部。
- 作者:Frank
- 链接:https://blog.franksteven.me//article/ss_overview_config
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。