Spring Security
整体架构 在Spring Security
的架构设计中,认证Authentication
和Authorization
是分开的,无论使用什么样的认证方式,都不会影响授权,这时两个独立的存在。这种独立的存在带来的好处之一,就是Spring Security
可以非常方便地整合一些外部的认证方案。
在Spring Security
中,用户的认证信息主要由Authentication
的实现类来保存,其中Authentication
接口定义如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Authentication
接口中定义的方法如下:
getAuthorities()
:获取用户的权限。getCredentials()
:获取用户凭证,也就是密码。getDetails()
:获取用户携带的详细信息。getPrincipal()
:获取当前用户,例如一个用户名或者一个用户对象。isAuthenticated()
:当前用户是否认证成功。
当用户使用用户名/
密码登录或使用RememberMe
登录时,都会对应一个不同的Authentication
实例。
Spring Security
中的认证工作主要由AuthenticationManager
接口负责,AuthenticationManager
接口定义如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager
接口中只有一个authenticate
方法可以用来做认证,该方法有三个不同的返回值:
- 返回
Authentication
,表示认证成功。- 抛出
AuthenticationException
异常,表示用户输入了无效的凭证。- 返回
null
,表示不能断定。
AuthenticationManager
接口最主要的实现类是ProviderManager
,ProviderManager
类管理了众多的AuthenticationProvider
实例。AuthenticationProvider
类有点类似于AuthenticationManager
,但是它多了一个supports
方法用来判断是否支持给定的Authentication
类型。AuthenticationProvider
接口定义如下:
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> clazz);
}
由于Authentication
接口拥有众多不同的实现类,这些不同的实现类又由不同的AuthenticationProvider
来处理,所以AuthenticationProvider
会有一个supports
方法,用来判断当前的AuthenticationProvider
是否支持对应的Authentication
。
在一次完整的认证流程中,可能会同时存在多个AuthenticationProvider
,例如项目同时支持form
表单登录和短信验证码登录。多个AuthenticationProvider
统一由ProviderManager
来管理。同时,ProviderManager
具有一个可选的parent
,如果所有的AuthenticationProvider
都认证失败,那就会调用parent
进行认证。parent
相当于一个具备认证方式,但当各个AuthenticationProvider
都无法处理认证问题的时候,就由parent
出面收拾残局。
当完成认证后,接下来就是授权了。在Spring Security
的授权体系中,有两个关键的接口:
AccessDecisionManager
AccessDecisionVoter
AccessDecisionVoter
是一个投票器,投票器会检查用户是否具备应有的校色,进而投出赞成、反对或者弃权票。AccessDecisionManager
则是一个决策器,来决定此次访问是否被允许。AccessDecisionManager
和AccessDecisionVoter
都有众多的实现类。在AccessDecisionManager
中会挨个遍历AccessDecisionVoter
,进而决定是否允许用户访问,因此AccessDecisionVoter
和AccessDecisionManager
两者的关系类似于AuthenticationProvider
和ProviderManager
的关系。
Spring Security
中,用户请求一个资源,这个资源通常是一个网络接口或者一个Java
方法所需要的角色会被封装成一个ConfigAttribute
对象,在ConfigAttribute
中只有一个getAttribute
方法,该方法返回一个String
字符串,就是角色的名称。一般来说,一个角色名称都带有一个ROLE_
前缀,投票器AccessDecisionVoter
所作的事情,其实就是比较用户所具备的角色和请求某个资源所需的ConfigAttribute
之间的关系。ConfigAttribute
接口定义如下:
public interface ConfigAttribute extends Serializable {
String getAttribute();
}
Web
安全 在Spring Security
中,认证、授权等功能都是基于过滤器来完成的。下列为Spring Security
中常见的过滤器。
过滤器 | 过滤器作用 | 是否默认加载 |
---|---|---|
ChannelProcessingFilter |
过滤请求协议,如HTTP 和HTTPS |
NO |
WebAsyncManagerIntegrationFilter |
将WebAsyncManager 与Spring Security 上下文进行集成 |
YES |
SecurityContextPersistenceFilter |
在处理请求之前,将安全信息加载到SecurityContextHolder 中以便后续使用。请求结束后再擦除SecurityContextHolder 中的信息 |
YES |
HeaderWriterFilter |
头信息加入到响应中 | YES |
CorsFilter |
处理跨域请求问题 | NO |
CsrfFilter |
处理CSRF 攻击 |
YES |
LogoutFilter |
处理注销登录 | YES |
OAuth2AuthorizationRequestRedirectFilter |
处理OAuth2 认证重定向 |
NO |
Saml2WebSsoAuthenticationRequestFilter |
处理SAML 认证 |
NO |
X509AuthenticationFilter |
处理X509 认证 |
NO |
AbstractPreAuthenticatedProcessingFilter |
处理预认证问题 | NO |
CasAuthenticationFilter |
处理CAS 单点登录 |
NO |
OAuth2LoginAuthenticationFilter |
处理OAuth2 认证 |
NO |
Saml2WebSsoAuthenticationFilter |
处理SAML 认证 |
NO |
UsernamePasswordAuthenticationFilter |
处理表单登录 | YES |
OpenIDAuthenticationFilter |
处理OpenID 认证 |
NO |
DefaultLoginPageGeneratingFilter |
配置默认登录页面 | YES |
DefaultLogoutPageGeneratingFilter |
配置默认注销页面 | YES |
ConcurrentSessionFilter |
处理session 有效期 |
NO |
DigestAuthenticationFilter |
处理HTTP 摘要认证 |
NO |
BearerTokenAuthenticationFilter |
处理OAuth2 认证时的Access Token |
NO |
BasicAuthenticationFilter |
处理HttpBasic 登录 |
YES |
RequestCacheAwareFilter |
处理请求缓存 | YES |
SecurityContextHolderAwareRequestFilter |
包装原始请求 | YES |
JaasApiIntegrationFilter |
处理JAAS 认证 |
NO |
RememberMeAuthenticationFilter |
处理RememberMe 登录 |
NO |
AnonymousAuthenticationFilter |
配置匿名认证 | YES |
OAuth2AuthorizationCodeGrantFilter |
处理OAuth2 认证中的授权码 |
NO |
SessionManagementFilter |
处理Session 并发问题 |
YES |
ExceptionTranslationFilter |
处理异常/ 授权中的情况 |
YES |
FilterSecurityInterceptor |
处理授权 | YES |
SwitchUserFilter |
处理账号切换 | NO |
以上过滤器是否默认加载是指引入
Spring Security
依赖之后,开发者不做任何配置时,会自动加载的过滤器。
开发者所见到的Spring Security
提供的功能,都是通过这些过滤器实现的,这些过滤器按照既定的优先级排列,最终形成一个过滤器链。开发者也可以自定义过滤器,并通过@Order
注解去调整自定义过滤器在过滤器链中的位置。
需要注意的是,默认过滤器并不是直接放在Web
项目的原生过滤器中,而是通过一个FilterChainProxy
来统一管理。Spring Security
中的过滤链通过FilterChainProxy
嵌入到Web
项目的原生过滤器链中。
在Spring Security
中,这样的过滤器链不仅仅只有一个,可能会有多个。当存在多个过滤器链时,多个过滤器链之间要指定优先级,当请求到达后,会从FilterChainProxy
进行分发,先和哪个过滤器链匹配上,就用哪个过滤器链进行处理。当系统中存在多个不同认证体系时,使用多个过滤器链就非常有效。
FilterChainProxy
作为一个顶层管理者,将统一管理Security Filter
。FilterChainProxy
本身将通过Spring
框架提供的DelegatingFilterProxy
整合到原生过滤器链中,所以上图还可以做进一步优化:
如果不使用Spring Security
这一类安全管理框架,大部分的开发者可能会将登录用户数据保存在session
中。事实上,Spring Security
也是这么做的。但是,为了使用方便,Spring Security
在此基础上还做了一些改进,其中最重要的一个变化就是线程绑定。
当用户登陆成功后,Spring Security
会将登陆成功的用户信息保存到SecurityContextHolder
中。SecurityContextHolder
中的数据保存默认是通过ThreadLocal
来实现的。使用ThreadLocal
创建的变量只能被当前线程访问,不能被其它线程访问和修改,也就是将用户数据与请求线程绑定在一起。当请求处理完毕后,Spring Security
会将SecurityContextHolder
中的数据拿出来保存到Session
中,同时将SecurityContextHolder
中的数据清空。每当有请求到来时,Spring Security
会先从Session
中取出用户登录数据,保存到SecurityContextHolder
中,方便在该请求的后续处理过程中使用。同时在请求结束时将SecurityContextHolder
中的数据拿出来保存到Session
中,然后将SecurityContextHolder
中的用户数据清空。
这一策略非常方便用户在Controller
或者Service
层获取当前登录用户数据,但是带来的另外一个问题就是,在字线程中想要获取用户登录数据就比较麻烦。Spring Security
对此也提供了相应的解决方案,如果开发者使用@Async
注解来开启异步任务的话,那么只需要添加如下配置,使用Spring Security
提供的异步任务代理,就可以在异步任务中从SecurityContextHolder
中获取当前登录用户的信息:
@Configuration
public class ApplicationAsyncConfiguration extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}
}