认识Spring Security
Spring Security 是为基于 Spring 的应用程序提供声明式安全保护的安全性框架。Spring Security 提供了完整的安全性解决方案,它能够在 Web 请求级别和方法调用级别处理身份认证和授权。因为基于 Spring 框架,所以 Spring Security 充分利用了依赖注入(dependency injection, DI)和面向切面的技术。
核心功能
对于一个权限管理框架而言,无论是 Shiro 还是 Spring Security,最最核心的功能,无非就是两方面:
- 认证
- 授权
通俗点说,认证就是我们常说的登录,授权就是权限鉴别,看看请求是否具备相应的权限。
认证(Authentication)
Spring Security 支持多种不同的认证方式,这些认证方式有的是 Spring Security 自己提供的认证功能,有的是第三方标准组织制订的,主要有如下一些:
一些比较常见的认证方式:
- HTTP BASIC authentication headers:基于IETF RFC 标准。
- HTTP Digest authentication headers:基于IETF RFC 标准。
- HTTP X.509 client certificate exchange:基于IETF RFC 标准。
- LDAP:跨平台身份验证。
- Form-based authentication:基于表单的身份验证。
- Run-as authentication:用户用户临时以某一个身份登录。
- OpenID authentication:去中心化认证。
除了这些常见的认证方式之外,一些比较冷门的认证方式,Spring Security 也提供了支持。
- Jasig Central Authentication Service:单点登录。
- Automatic “remember-me” authentication:记住我登录(允许一些非敏感操作)。
- Anonymous authentication:匿名登录。
- ……
作为一个开放的平台,Spring Security 提供的认证机制不仅仅是上面这些。如果上面这些认证机制依然无法满足你的需求,我们也可以自己定制认证逻辑。当我们需要和一些“老破旧”的系统进行集成时,自定义认证逻辑就显得非常重要了。
授权(Authorization)
无论采用了上面哪种认证方式,都不影响在 Spring Security 中使用授权功能。Spring Security 支持基于 URL 的请求授权、支持方法访问授权、支持 SpEL 访问控制、支持域对象安全(ACL),同时也支持动态权限配置、支持 RBAC 权限模型等,总之,我们常见的权限管理需求,Spring Security 基本上都是支持的。
当完成认证后,接下来就是授权。
在 Spring Security 的授权体系中,有两个关键接口:
- AccessDecisionManager
- AccessDecisionVoter
AccessDecisionVoter 是一个投票器,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票;
AccessDecisionManager 则是一个决策器,来决定此次访问是否被允许。AccessDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AccessDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
过滤器
Spring Security进阶学习 - 掘金 (juejin.cn)
Spring Security 采用的是责任链的设计模式,它有一条很长的过滤器链。如下是常见的过滤器:
Spring Security 的默认 Filter 链:
1 | SecurityContextPersistenceFilter |
这些过滤器按照既定的优先级排列,最终形成一个过滤器链。
介绍几个重要的过滤器:
- SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
- UsernamePasswordAuthenticationFilter过滤器用于处理基于表单方式的登录验证,该过滤器默认只有当请求方法为post、请求页面为/login时过滤器才生效,如果想修改默认拦截url,只需在刚才介绍的Spring Security配置类WebSecurityConfig中配置该过滤器的拦截url:.loginProcessingUrl(“url”)即可;
- BasicAuthenticationFilter用于处理基于HTTP Basic方式的登录验证,当通过HTTP Basic方式登录时,默认会发送post请求/login,并且在请求头携带Authorization:Basic dXNlcjoxOWEyYWIzOC1kMjBiLTQ0MTQtOTNlOC03OThkNjc2ZTZlZDM=信息,该信息是登录用户名、密码加密后的信息,然后由BasicAuthenticationFilter过滤器解析后,构建UsernamePasswordAuthenticationFilter过滤器进行认证;如果请求头没有Authorization信息,BasicAuthenticationFilter过滤器则直接放行;
- FilterSecurityInterceptor的拦截器,用于判断当前请求身份认证是否成功,是否有相应的权限,当身份认证失败或者权限不足的时候便会抛出相应的异常;
- ExceptionTranslateFilter捕获并处理,所以我们在ExceptionTranslateFilter过滤器用于处理了FilterSecurityInterceptor抛出的异常并进行处理,比如需要身份认证时将请求重定向到相应的认证页面,当认证失败或者权限不足时返回相应的提示信息;
认证流程
- 用户提交用户名、密码被
SecurityFilterChain
中的UsernamePasswordAuthenticationFilter
过滤器获取到, 封装为请求Authentication
,通常情况下是UsernamePasswordAuthenticationToken
这个实现类。 - 然后过滤器将 Authentication 提交至认证管理器(
AuthenticationManager
)进行认证 。 - 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
- SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication()方法,设置到其中。 可以看出 AuthenticationManager 接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为 ProviderManager。而 Spring Security 支持多种认证方式,因此 ProviderManager 维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。其中web表单的对应的 AuthenticationProvider 实现类为 DaoAuthenticationProvider,它的内部又维护着一个
UserDetailsService
负责UserDetails
的获取。最终 AuthenticationProvider 将 UserDetails 填充至 Authentication。
(2)AuthenticationManager 本身不包含认证逻辑,其核心是用来管理所有的 AuthenticationProvider
,通过交由合适的 AuthenticationProvider 来实现认证。
AuthenticationProvider
的定义:
1
2
3
4
5 public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}可以看到,AuthenticationProvider 中就两个方法:
- authenticate 方法用来做验证,就是验证用户身份。
- supports 则用来判断当前的
AuthenticationProvider
是否支持对应的 Authentication。
这里又涉及到一个东西,就是 Authentication
。
Authentication 本身是一个接口,从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表。我们来看下 Authentication 的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.springframework.security.core;
public interface Authentication extends Principal, Serializable {
// 获取用户的权限
Collection<? extends GrantedAuthority> getAuthorities();
//获取用户凭证,一般是密码,认证之后会移出,来保证安全性
Object getCredentials();
//获取用户携带的详细信息,Web应用中一般是访问者的ip地址和sessionId
Object getDetails();
// 获取当前用户
Object getPrincipal();
//判断当前用户是否认证成功
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
官方文档里说过,当用户提交登录信息时,会将用户名和密码进行组合成一个实例 UsernamePasswordAuthenticationToken
,而这个类是 Authentication 的一个常用的实现类,用来进行用户名和密码的认证,类似的还有 RememberMeAuthenticationToken
,它用于记住我功能。
Spring Security 支持多种认证逻辑,每一种认证逻辑的认证方式其实就是一种 AuthenticationProvider。通过 getProviders()
方法就能获取所有的 AuthenticationProvider,通过 provider.supports()
来判断 provider 是否支持当前的认证逻辑。
当选择好一个合适的 AuthenticationProvider 后,通过 provider.authenticate(authentication)
来让 AuthenticationProvider 进行认证。
如何选择 AuthenticationProvider
来处理它?
我们在 DaoAuthenticationProvider
的基类 AbstractUserDetailsAuthenticationProvider
发现以下代码:
1 | public boolean supports(Class<?> authentication) { |
也就是说当web表单提交用户名密码时,Spring Security 由 DaoAuthenticationProvider
处理。
DaoAuthenticationProvider
的父类是 AbstractUserDetailsAuthenticationProvider
, 在该类中的 authenticate()方法用于处理认证逻辑,这里就不粘贴代码了,该方法大致逻辑如下:
- 首先实例化
UserDetails
对象,调用了retrieveUser
方法获取到了一个user
对象,retrieveUser
是一个抽象方法。该方法进一步会调用我们自己在登录时候的写的loadUserByUsername
方法,具体在自定义的UserDetailsService
或InMemoryUserDetailsManager
等。 - 如果没拿到信息就会抛出异常,如果查到了就会去调用
preAuthenticationChecks
的check(user)
方法去进行预检查。在预检查中进行了三个检查,因为UserDetail
类中有四个布尔类型,去检查其中的三个,用户是否锁定、用户是否过期,用户是否可用。 - 预检查之后紧接着去调用了
additionalAuthenticationChecks
方法去进行附加检查,这个方法也是一个抽象方法,检查密码是否匹配,在DaoAuthenticationProvider
的additionalAuthenticationChecks
方法中去具体实现,在里面进行了加密解密去校验当前的密码是否匹配。我们想要校验图片验证码,就可以和密码一起校验,即我们重写additionalAuthenticationChecks
方法。 - 最后在 postAuthenticationChecks.check 方法中检查密码是否过期。
- 所有的检查都通过,则认为用户认证是成功的。用户认证成功之后,会将这些认证信息和user传递进去,调用
createSuccessAuthentication
方法。
DaoAuthenticationProvider
中的 additionalAuthenticationChecks
方法用于比对密码,逻辑比较简单,就是将 password 加密后与事先保存好的密码做比对。代码如下:
1 | protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { |
AuthenticationManager
AuthenticationManager 认证管理器是用来处理认证请求的接口.
1 | package org.springframework.security.authentication; |
AuthenticationManager 只有一个 authenticate 方法用来做认证,该方法有三个不同的返回值:
- 返回 Authentication,表示认证成功;
- 抛出 AuthenticationException 异常,表示用户输入了无效的凭证;
- 返回 null,表示不能断定。
AuthenticationManager 是一个接口,它有很多实现类,开发人员可以自定义实现类。它默认的实现是 ProviderManager,但它不处理认证请求,而是将委托给 AuthenticationProvider 列表,然后依次使用 AuthenticationProvider 进行认证。
如果有一个 AuthenticationProvider 认证的结果不为null,则表示成功(否则失败,抛出 ProviderNotFoundException),之后不在进行其它 AuthenticationProvider 认证,并作为结果保存在 ProviderManager。
AuthenticationProvider
AuthenticationProvider 是一个身份认证接口,实现该接口来定制自己的认证方式。
AuthenticationProvider 的源码如下:
1 | public interface AuthenticationProvider { |
Spring Security 支持多种不同的认证方式,不同的认证方式对应不同的身份类型,每个 AuthenticationProvider 需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时 Spring Security 会生成 UsernamePasswordAuthenticationToken,它是一个 Authentication,里面封装着用户提交的用户名、密码信息。而对应的,哪个 AuthenticationProvider 来处理它?
我们在 DaoAuthenticationProvider 的基类 AbstractUserDetailsAuthenticationProvider 发现以下代码:
1 | public boolean supports(Class<?> authentication) { |
也就是说当web表单提交用户名密码时,Spring Security 由 DaoAuthenticationProvider 处理。
如果有一个 AuthenticationProvider 认证的结果不为null,则表示成功(否则失败,抛出 ProviderNotFoundException),之后不在进行其它 AuthenticationProvider 认证,并作为结果保存在 ProviderManager。
ProviderManager 具有一个可选的 parent,如果所有的 AuthenticationProvider 都认证失败,那么就会调用 parent 进行认证。parent 相当于一个备用认证方式,即各个 AuthenticationProvider 都无法处理认证问题的时候,就由 parent 来负责。
Authentication
最后,我们来看一下Authentication(认证信息)的结构,它是一个接口,我们之前提到的 UsernamePasswordAuthenticationToken就是它的实现之一:
1 | package org.springframework.security.core; |
从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表。
官方文档里说过,当用户提交登录信息时,会将用户名和密码进行组合成一个实例 UsernamePasswordAuthenticationToken,而这个类是 Authentication 的一个常用的实现类,用来进行用户名和密码的认证,类似的还有 RememberMeAuthenticationToken,它用于记住我功能。
UserDetailsService
现在咱们现在知道 DaoAuthenticationProvider 处理了web表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken),里面包含了身份信息(Principal)。这个身份信息就是一个 Object ,大多数情况下它可以被强转为UserDetails对象。
DaoAuthenticationProvider 中包含了一个 UserDetailsService 实例,它负责根据用户名提取用户信息 UserDetails(包含密码),而后 DaoAuthenticationProvider 会去对比 UserDetailsService 提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为spring bean来定义自定义身份验证。
1 | public interface UserDetailsService { |
很多人把 DaoAuthenticationProvider 和 UserDetailsService 的职责搞混淆,其实 UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而 DaoAuthenticationProvider 的职责更大,它完成完整的认证流程,同时会把 UserDetails 填充至 Authentication。
UserDatails 是用户信息,源码如下:
1 | public interface UserDetails extends Serializable { |
它和 Authentication 接口很类似,比如它们都拥有 username,authorities。Authentication 的 getCredentials()与 UserDetails 中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication 中的 getAuthorities()实际是由 UserDetails 的 getAuthorities()传递而形成的。还记得 Authentication 接口中的getDetails()方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 认证之后被填充的。
通过实现 UserDetailsService 和 UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
Spring Security 提供的 InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是 UserDetailsService 的实现类,主要区别无非就是从内存还是从数据库加载用户。
自定义 UserDetailsService
1 |
|
PasswordEncoder
DaoAuthenticationProvider 认证处理器通过 UserDetailsService 获取到 UserDetails 后,它是如何与请求 Authentication 中的密码做对比呢?
在这里 Spring Security 为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider 通过 PasswordEncoder 接口的matches 方法进行密码的对比,而具体的密码对比细节取决于实现:
1 | public interface PasswordEncoder { |
而 Spring Security 提供很多内置的 PasswordEncoder,能够开箱即用,使用某种 PasswordEncoder 只需要进行如下声明即可,如下:
1 |
|
NoOpPasswordEncoder 采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
1、用户输入密码(明文 )
2、DaoAuthenticationProvider 获取 UserDetails(其中存储了用户的正确密码)
3、DaoAuthenticationProvider 使用 PasswordEncoder 对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
NoOpPasswordEncoder 的校验规则拿输入的密码和 UserDetails 中的正确密码进行字符串比较,字符串内容一致则校验通过,否则 校验失败。
实际项目中首选 BCryptPasswordEncoder,在安全配置中定义:
1 |
|
测试发现认证失败,提示:Encoded password does not look like BCrypt。
原因:
由于UserDetails 中存储的是原始密码(比如:123),它不是BCrypt格式。
多个请求共享认证信息
Spring Security 通过 Session
来保存用户的认证信息,那么 Spring Security 到底是在什么时候将认证信息放入 Session,又在什么时候将认证信息从 Session 中取出来的呢?
下面将 Spring Security 的认证流程补充完整,如下图:
在上一节认证成功的 successfulAuthentication()
方法中,有一行语句:
1 | SecurityContextHolder.getContext().setAuthentication(authResult); |
其实就是在这里将认证信息放入 Session 中。
查看 SecurityContext
源码,发现内部就是对 Authentication 的封装,提供了 equals、hashcode、toString等方法,而SecurityContextHolder
可以理解为线程中的 ThreadLocal
。
我们知道一个 HTTP 请求和响应都是在一个线程中执行,因此在整个处理的任何一个方法中都可以通过 SecurityContextHolder.getContext()
来取得存放进去的认证信息。
从 Session 中对认证信息的处理由 SecurityContextPersistenceFilter
来处理,它位于 Spring Security 过滤器链的最前面,它的主要作用是:
- 当请求时,检查 Session 中是否存在 SecurityContext,如果有将其放入到线程中。
- 当响应时,检查线程中是否存在 SecurityContext,如果有将其放入到 Session 中。
会话控制
Session 会话管理需要在configure(HttpSecurity http)
方法中通过http.sessionManagement()
开启配置。此处对http.sessionManagement()
返回值的主要方法进行说明,这些方法涉及 Session 会话管理的配置,具体如下:
invalidSessionUrl(String invalidSessionUrl)
:指定会话失效时(请求携带无效的 JSESSIONID 访问系统)重定向的 URL,默认重定向到登录页面。invalidSessionStrategy(InvalidSessionStrategy invalidSessionStrategy)
:指定会话失效时(请求携带无效的 JSESSIONID 访问系统)的处理策略。maximumSessions(int maximumSessions)
:指定每个用户的最大并发会话数量,-1 表示不限数量。maxSessionsPreventsLogin(boolean maxSessionsPreventsLogin)
:如果设置为 true,表示某用户达到最大会话并发数后,新会话请求会被拒绝登录;如果设置为 false,表示某用户达到最大会话并发数后,新会话请求访问时,其最老会话会在下一次请求时失效并根据 expiredUrl() 或者 expiredSessionStrategy() 方法配置的会话失效策略进行处理,默认值为 false。expiredUrl(String expiredUrl)
:如果某用户达到最大会话并发数后,新会话请求访问时,其最老会话会在下一次请求时失效并重定向到 expiredUrl。expiredSessionStrategy(SessionInformationExpiredStrategy expiredSessionStrategy)
:如果某用户达到最大会话并发数后,新会话请求访问时,其最老会话会在下一次请求中失效并按照该策略处理请求。注意如果本方法与 expiredUrl() 同时使用,优先使用 expiredUrl() 的配置。sessionRegistry(SessionRegistry sessionRegistry)
:设置所要使用的 sessionRegistry,默认配置的是 SessionRegistryImpl 实现类。sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
:创建 session 的时机,默认是 ifRequired,Spring Security在需要时才创建session。
获取用户认证信息
通过调用 SecurityContextHolder.getContext().getAuthentication()
就能够取得认证信息:
1 |
|
仅想获取 UserDetails
对象,也是可以的,写法如下:
1 |
|
授权流程
Spring Security 可以通过 http.authorizeRequests() 对web请求进行授权保护。Spring Security 使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。授权流程如下:
分析授权流程:
- 拦截请求,已认证用户访问受保护的web资源将被 SecurityFilterChain 中的 FilterSecurityInterceptor 的子类拦截。
- 获取资源访问策略,FilterSecurityInterceptor 会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection 。
SecurityMetadataSource 其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
1 | http.csrf().disable() //屏蔽CSRF控制,即spring security不再限制CSRF |
- 最后,FilterSecurityInterceptor 会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资 源,否则将禁止访问。
AccessDecisionManager(访问决策管理器)的核心接口如下:
1 | public interface AccessDecisionManager { |
这里着重说明一下decide的参数:
- authentication:要访问资源的访问者的身份
- object:要访问的受保护资源,web请求对应FilterInvocation
- configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。
授权决策
1、AccessDecisionManager,为web和方法安全性提供访问决。AccessDecisionManager
由 AbstractSecurityInterceptor
调用,负责做出最终的访问控制决策。AccessDecisionManager
接口包含三种方法:
1 | package org.springframework.security.access; |
AccessDecisionManager
的 decide
方法传递了它所需的所有相关信息,以便做出授权决定。特别是,传递 secure Object
可以检查实际安全对象调用中包含的那些参数。例如,假设安全对象是 MethodInvocation
。查询 MethodInvocation
任何Customer
参数很容易,然后在AccessDecisionManager
中实现某种安全逻辑,以确保允许委托人对该客户进行操作。如果访问被拒绝,预计实现将抛出AccessDeniedException
。
AbstractSecurityInterceptor
在启动时调用supports(ConfigAttribute)
方法来确定AccessDecisionManager
是否可以处理传递的ConfigAttribute
。安全拦截器实现调用supports(Class)
方法以确保配置的AccessDecisionManager
支持安全拦截器将呈现的安全对象的类型。
2、AccessDecisionVoter
1 | package org.springframework.security.access; |
具体实现返回int
,可能的值反映在AccessDecisionVoter
静态字段ACCESS_ABSTAIN
,ACCESS_DENIED
和ACCESS_GRANTED
中。如果投票实施对授权决定没有意见,则返回ACCESS_ABSTAIN
。如果确实有意见,则必须返回ACCESS_DENIED
或ACCESS_GRANTED
。
AccessDecisionVoter 是一个投票器,投票器会检查用户身份具备应有的角色,进而投出赞成、反对或者弃权票;AccessDecisionManager 则是一个决策器,来决定此次访问是否被允许。AccessDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会挨个遍历 AccessDecisionVoter,进而决定是否允许用户访问,因而 AccessDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。
Spring Security 提供的最常用的 AccessDecisionVoter
是简单的 RoleVoter
,它将配置属性视为简单的角色名称,并在用户被分配了该角色时授予访问权限。
在 Spring Security 中,用户请求一个资源(通常是一个网络接口或者一个 Java 方法)所需要的角色会被封装成一个 ConfigAttribute 对象,在 ConfigAttribute 对象中只有一个 getAttribute 方法,该方法返回一个 String 字符串,就是角色的名称。如果任何 ConfigAttribute
以前缀 ROLE_
开头,它将投票。如果有 GrantedAuthority
返回String
表示(通过getAuthority()
方法)完全等于从前缀ROLE_
开始的一个或多个ConfigAttributes
,它将投票授予访问权限。如果与ROLE_
开头的任何ConfigAttribute
没有完全匹配,则RoleVoter
将投票拒绝访问。如果没有ConfigAttribute
以ROLE_
开头,选民将弃权。