乱讲SpringSecurity
最近又想起了SpringSecurity,记着头两年这东西把我难坏了,当然现在我对他也玩的不那么好看,先记一下最近学习的结果,如果以后还有机会学习,再撸一次
SpringSecurity的工作流程
先说明几个名词
- 身份认证,指的是确认你的身份或者说就是登陆信息
- 权限认证,指的是判断你有没有访问这个资源的权限
- 身份认证流程,指的是获得身份认证的过程,这可能需要几个Filter和浏览器的跳转完成的
以他默认的基于Session的方式说明
绿色部分是用于加载已经存在于本次会话的 SecurityContex
蓝色部分是用于身份认证流程的,也就是没有认证的情况下会进入这部分在
橙色部分是权限认定的部分,会最终决定是权限足够还是未登陆。
以上的功能都是Security的过滤器链来完成的,特别说明一下,分析Security流程是不要只抓住一次请求去分析,要结合来回几次分析,同时一定要分析好,这次请求是资源访问还是身份认证请求
SpringSecurity的主要Filter
Filter | 本文重点 |
---|---|
ChannelProcessingFilter | |
ConcurrentSessionFilter | |
WebAsyncManagerIntegrationFilter | |
SecurityContextPersistenceFilter | yes |
HeaderWriterFilter | |
CorsFilter | |
CsrfFilter | |
LogoutFilter | |
OAuth2AuthorizationRequestRedirectFilter | |
Saml2WebSsoAuthenticationRequestFilter | |
X509AuthenticationFilter | |
AbstractPreAuthenticatedProcessingFilter | |
CasAuthenticationFilter | |
OAuth2LoginAuthenticationFilter | |
Saml2WebSsoAuthenticationFilter | |
UsernamePasswordAuthenticationFilter | yes |
ConcurrentSessionFilter | |
OpenIDAuthenticationFilter | |
DefaultLoginPageGeneratingFilter | yes |
DefaultLogoutPageGeneratingFilter | |
DigestAuthenticationFilter | |
BearerTokenAuthenticationFilter | |
BasicAuthenticationFilter | |
RequestCacheAwareFilter | |
SecurityContextHolderAwareRequestFilter | |
JaasApiIntegrationFilter | |
RememberMeAuthenticationFilter | |
AnonymousAuthenticationFilter | yes |
OAuth2AuthorizationCodeGrantFilter | |
SessionManagementFilter | |
ExceptionTranslationFilter | yes |
FilterSecurityInterceptor | yes |
Security的所有逻辑都在上面的这个链中实现的,本文中主要说明的几个Filter,这些个Filter可以完成一次完整的,基于Session的password认证与鉴权的流程。其他的Filter,有些与这些功能上相似,但都是结合不同的认证逻辑的不同实现,有些我也我也不知道干什么的(T_T),这些有的不在默认的依赖中,需要使用的话还需要引入特定的依赖,比如OAuth2的相关Filter。所以我们先关心下面这几个Filter就可以了
Filter |
---|
SecurityContextPersistenceFilter |
UsernamePasswordAuthenticationFilter |
DefaultLoginPageGeneratingFilter |
AnonymousAuthenticationFilter |
ExceptionTranslationFilter |
FilterSecurityInterceptor |
SpringSecurity主要的对象
用于保存权限与身份数据的对象
- GrantedAuthority 保存当前请求持有的权限,比如 ROLE_ADMIN,ROLE_MANAGER,最终进行权限认证的时候会比对这个请求的权限和请求想要访问资源所需要的权限
- Authentication 用于保存认证信息,以最常见的登陆用户举例,这个对象会持有用户的用户名(Principal),密码(Credentials),用户拥有的权限(Authorities/ GrantedAuthority),其他详细信息和是否认证过的标识
- SecurityContext 保存Authentication ,他在代码上的意思不光是用于保存Authentication数据,他可以让你更改保存Authentication的方式
- SecurityContextHolder 保存 SecurityContext,这个类提供的功能是可以提供不同的SecurityContext的共享策略,对于普通的WEB工程,他会将SecurityContext保存在一个ThreadLocal中
图怎么这么大
用于认证身份信息的对象
- AuthenticationManager 用于通过一个不完整的Authentication 计算出一个数据完整的身份认证过的 Authentication,就是说,入参可能只有username,但是出参会包含username, GrantedAuthority ,details,isAuthenticated等等更多的信息,一个默认的,使用最多的例子是ProviderManager
- AuthenticationProvider 这个类用于具体计算Authentication,通常来看,他是AuthentiationManager持有的对象,AuthenticationManager通过这个对象来得到具体的Authentication,但根据你的认证方式不同,也可以没有这个对象,比如使用OAuth2时 OAuth2AuthenticationManager。
认证失败的处理类
- AuthenticationFailureHandler 如果你的身份认证流程是在SecurityFilterChain中的,比如你使用UsernamePasswordAuthenticationFilter,那么在这个过程中,获得认证信息时(比如去数据库检索用户)失败了,会使用这个类去处理,一般会重定向到一个登陆错误页面(比如默认的 /login?error)或者响应 401 (参考SimpleUrlAuthenticationFailureHandler)
- AuthenticationEntryPoint 这个是在最终认证权限时如果发现请求没有身份认证 ,那么会使用这个类进行处理,处理的方式可能和AuthenticationFailureHandler类似,但是响应给前台的信息可能会不同,因为这个类代表着没有登陆,AuthenticationFailureHandler代表登陆过程出错,比如密码错误,这时前台的良好的表现是会提示用户这个错误
- AccessDeniedHandler 这个是在最终认证权限时发现请求的权限不对,比如希望有ROLE_ADMIN,而你是ROLE_NORMAL,那么会响应一个 403
保存、获得SecurityContext的类
SecurityContextRepository 这个类一般没有人关注,网上一些例子也没有使用这个类,而是编写其他Filter类来实现其功能,他的功能是加载SecurityContext, 他工作在SecurityContextPersistenceFilter 中,默认的,基于Session的工程,这个类的功能就是在Session中获得SecurityContext。网上有一些教程使用jwt写成鉴权的,不希望在Session中保存SecurityContext ,而是通过token 获得,那么他们会写一个Filter中Request header中获得token ,然后加载一个SecurityContext,这个Filter的位置放在 SecurityContextPersistenceFilter后面。其实spring security 是提供了这种扩展的,就是重新实现一个SecurityContextRepository,在配置Security时 这样配置
http.securityContext().securityContextRepository(new BarnSecurityContextRepository()).and()
SpringSecurity具体流程
我们只以最普通的以Session为基础的,数据库鉴权的方式,相关的Filter可以有下面这几个
其大概流程是这样子的
这UML画的不标准,可能只有我能看明白吧(T_T),看下面的吧。
一个请求进入可能分为3种情况
- POST /login
- GET /login,GET /login?error
- 其他
第一种情况,他会他会走到 UsernamePasswordAuthenticationFilter
,去加载认证信息和对应的权限,如果成功,跳转到到登陆成功页面,如果失败会将请求重定向到 /login?error,后面的Filter就不会走了,所以这个请求过程中起作用的Filter只有一个 UsernamePasswordAuthenticationFilter
,SecurityContextPersistenceFilter
打了个酱油就完了,可以简单相像成走了下面几个Filter(实际这个Filter之前的Filter也会进入的,但作用不是鉴权,有可能是防止攻击一类的,如果cors,或者什么都没做)
第三种情况,对了,我想先说第三种情况,就是未登陆访问任意的受限资源,在进入 AnonymousAuthenticationFilter
之前的Filter都没有什么用,在这个Filter中会给这次请求生成一个 匿名访问用户的 Authentication
。 然后到 ExceptionTranslationFilter
,暂时不做什么,等着后面的Filter出错,如果抛Exception他也没有什么用。最后是 FilterSecurityInterceptor
, 他会根据 Authentication
和你的Security配置请求判断这次请求是权限不足,或是因为没有登陆不能访问,这两种不能访问的情况会分别抛出 AccessDeniedException
和 AuthenticationException
,然后Filter链回到 ExceptionTranslationFilter
,对这两种 Exception
进行不同的处理, 对于 AuthenticationException
会被重定向到 /login,对于 AccessDeniedException
会响应一个 403消息
第二种情况,对了最后说第二种情况,我觉得第二种情况是为了使Security的鉴权流程完整补充上去的,实际不会有人用到他,或者一定会替换他,第一种情况和第二种情况在重定向到login的时候,DefaultLoginPageGeneratingFilter
会生成一个login静态页面,好主用户有一个login的入口
所以就我浅浅的看法来看,真正生产我会只要保证第二种情况就可以了第一和第三种情况一般会通过接口和更美丽的页面实现,而不会在Filter中实现。
简单的扩展
基于前面的说明,那我们针对前面一节说的三种情况逐个进行对Security的扩展 。这次我们结合jwt的鉴权方式进行扩展 第一和第三种情况简单,将登陆页面和登陆接口两个 请求路径放开就可以了,这里有一个特别说明,对于放开的配置最好用下面的配置,这会使放开的url不进入SecurityFilter
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.mvcMatchers("/login/ps")
.mvcMatchers("/login/ps/refresh")
.mvcMatchers("/login");
}
}
然后自己实现登陆画面和接口
对于第二种情况需要实现的可能多一点
对应前面说的这个流程针对每一步的Filter进行改造就可以了
SecurityContextPersistenceFilter扩展
前面说了,这个类用于加载SecurityContext,默认他是保存在Session中的,而这个类持有一个SecurityContextRepository对象,这个对象负责具体的保存和加载SecurityContext的逻辑。如果我们使用token 保存身份信息,那么需要实现一个自己的SecurityContextRepository就可以了。
AnonymousAuthenticationFilter
这个不用动,它挺好的
ExceptionTranslationFilter
需要对这个类持有的两个对象进行改自定义
- AuthenticationEntryPoint
当没有登陆时会进入这个类,你需要实现如果没有登陆的情况下如何处理请求就可以 ,302 or 401
- AccessDeniedHandler
当没有足够权限的情况下会进入这个类,你需要实现如果没有登陆的情况下如何处理请求就可以 ,302 or 403
- FilterSecurityInterceptor
这个类我认为不太好改造,它需要注入的范围很多,但我们可以消减他的权力,正常我们会这样配置Security
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/system/api1").hasAnyRole("ROLE_ADMIN")
.mvcMatchers("/system/api2").hasAnyRole("ROLE_CUSTOM")
.anyRequest().authenticated()
}
}
那么 FilterSecurityInterceptor
会对每个请求进行权限(hasAnyRole)和登陆两个认证(authenticated),我们去掉 hasAnyRole 那两个配置,让他只进行登陆认证就可以了,另外的功能自定义一个Filter实现
其次,这个类需要一个 AuthenticationProvider
前面写过这个类是用于具体获取用户身份的对象,比如查数据库,我们需要实现这个类,通过token 来获得用户的 Authenticaton
对象(你可能也会自定义一个 Authentication
),然后注入到 FilterSecurityInterceptor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(
new YourAuthenticationProvider()
);
}}
自定义FilterSecurityInterceptor
前面说对于默认的FilterSecurityInterceptor,将它配置成只负责登陆认证,不负责权限认证,那么我们就需要再添加一个认证权限的Filter就可以了,位置是在 FilterSecurityInterceptor的后面,你只要保证自定义的Filter在访问者没有权限的情况下抛出 AccessDeniedException就可以了,具体怎么鉴权,请发挥想你