Spring Security的配置
Spring Security 5.7
开始实现WebSecurityConfigurerAdapter
来配置的方式已经被废弃了。可直接在配置类上打@Configuration注解,并直接注入SecurityFilterChain及其他相关的方法。
1 |
|
关于CSRF
csrf
全称是Cross—Site Request Forgery 跨站点请求伪造,这是一种安全攻击手段。利用存在客户端的信息伪造成正常用户来进行攻击。
例如你访问网站A登录后,未退出又打开一个tab页访问网站B,这时候网站B就可以利用保存在浏览器中的sessionId伪造成你的身份访问网站 A。
Spring Security对CSRF的检查机制。
他的思想就是在后台的session中加入一个csrf的token值,向后端发送请求时,对于GET、HEAD、TRACE、OPTIONS以外的请求,例如POST、PUT、DELETE等,会要求带上这个token值进行比对。
我们访问默认的登录页时,可在登录form看到有一个name为csrf的隐藏字段,这个就是csrf的token。
Spring Security后台有一个CsrfFilter专门负责对Csrf参数进行检查。他会调用 HttpSessionCsrfTokenRepository生成一个CsrfToken,并将值保存到Session中。
方法级别的注解支持
可直接在Controller中进行权限控制。在Configuration配置类中打@EnableGlobalMethodSecurity()
注解,参数有prePostEnabled、securedEnabled、jsr250Enabled
,分别对应@PreAuthorize、@Secured、@RolesAllowed(等价于@Secured)
,使用时在Controller的方法上打对应注解并配置权限就行。
常用的Handler
在Spring Security中,常用的处理程序(handlers)包括以下几个:
AuthenticationSuccessHandler
:用于处理成功的身份验证请求。可以在身份验证成功后执行自定义的操作,例如重定向到特定页面或生成访问令牌。AuthenticationFailureHandler
:用于处理身份验证失败的请求。可以根据失败的原因执行自定义的操作,例如显示错误消息或重定向到特定页面。AccessDeniedHandler
:用于处理访问被拒绝的请求。当用户尝试访问他们没有权限的资源时,可以执行自定义的操作,例如显示错误消息或重定向到特定页面。LogoutSuccessHandler
:用于处理成功的注销请求。在用户注销成功后执行自定义的操作,例如显示注销成功消息或重定向到登录页面。AuthenticationEntryPoint
:用于处理需要身份验证的资源的请求。当用户尝试访问需要身份验证的资源时,但未提供有效的凭据时,该处理程序将处理请求,例如返回身份验证错误的响应或重定向到登录页面。
这些处理程序可以通过实现相应的接口或使用现有的实现类来自定义。此外,还可以通过配置适当的bean将它们与Spring Security集成,并将其应用于适当的请求。
认证流程
用户提交用户名、密码被
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。
调试代码从
UsernamePasswordAuthenticationFilter
开始跟踪。 最后的认证流程在AbstractUserDetailsAuthenticationProvider
的authenticate
方法中。获取用户在retrieveUser
方法。密码比较在additionalAuthenticationChecks
方法
下面是Spring Security的基本认证流程:
- 用户提交身份验证请求。
- 请求被拦截,由
AuthenticationFilter
处理。 AuthenticationFilter
调用AuthenticationManager
进行身份验证。AuthenticationManager
委托给AuthenticationProvider
进行身份验证。AuthenticationProvider
验证用户的凭据,并返回一个已验证的Authentication
对象。AuthenticationManager
将验证的Authentication
对象返回给AuthenticationFilter
。AuthenticationFilter
将验证的Authentication
对象存储在SecurityContextHolder
中。- 用户被认为是已验证的,并且可以继续访问受保护的资源。
1 | 用户提交身份验证请求 |
AuthenticationProvider接口:认证处理器
1 | public interface AuthenticationProvider { |
1 | //AbstractUserDetailsAuthenticationProvider |
这里对于AbstractUserDetailsAuthenticationProvider
,他的support
方法就表明他可以处理用户名密码这样的认证。
Authentication接口:认证信息
1 | public interface Authentication extends Principal, Serializable { |
继承自Principal
类,代表一个抽象主体身份。继承了一个getName()方法来表示主 体的名称。
UserDetailsService接口: 获取用户信息
1 | public interface UserDetailsService { |
获取用户信息的接口,只有根据用户名获取用户信息的方法。
在
DaoAuthenticationProvider
的retrieveUser
方法中,会获取spring容器中的UserDetailsService
。如果我们没有自己注入UserDetailsService对象,那么在UserDetailsServiceAutoConfiguration
类中,会在启动时默认注入一个带user用户 的UserDetailsService
。 我们可以通过注入自己的UserDetailsService来实现加载自己的数据。
UserDetails: 用户信息实体
代表了一个用户实体,包括用户、密码、权限列表、账号过期、认证过期、是否启用、是否锁定。
1 | public interface UserDetails extends Serializable { |
PasswordEncoder 密码解析器
1 | public interface PasswordEncoder { |
对密码进行加密解密
DaoAuthenticationProvider
在additionalAuthenticationChecks
方法中会获取 Spring容器中的PasswordEncoder来对用户输入的密码进行比较。
BCryptPasswordEncoder
SpringSecurity
中最常用的密码解析器。他使用BCrypt算法。他的特点是加密可以加盐sault,但是解密不需要盐。因为盐就在密文当中。这样可以通过每次添加不同的盐,而给同样的字符串加密出不同的密文。 密文形如:$2a$10$vTUDYhjnVb52iM3qQgi2Du31sq6PRea6xZbIsKIsmOVDnEuGb/.7K
$是分割符,无意义;2a是bcrypt加密版本号;10是cost的值;而后的前22 位是salt值;再然后的字符串就是密码的密文了
授权流程
在用户认证通过后,对访问资源的权限进行检查的过程。
Spring Security通过http.authorizeRequests()对web请求进行授权保护,使用 标准Filter建立对web请求的拦截,实现对资源的授权访问。
拦截请求,已认证用户访问受保护的web资源将被
SecurityFilterChain
中(实现类为DefaultSecurityFilterChain
)的FilterSecurityInterceptor
的子类拦截。获取资源访问策略,
FilterSecurityInterceptor
会从SecurityMetadataSource
的子类DefaultFilterInvocationSecurityMetadataSource
获取要访问当前资源所需要的权限Collection
。SecurityMetadataSource
其实就是读取访问策略的抽象,读取的内容是在配置类中对SecurityFilterChain filterChain(HttpSecurity http)
的配置最后,
FilterSecurityInterceptor
调用AccessDecisionManager
进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。关于AccessDecisionManager接口,最核心的就是其中的decide
方法。这个方法用来鉴定当前用户是否有访问对应受保护资源的权限。1
2
3
4
5
6
7
8public interface AccessDecisionManager {
//通过传递的参数来决定用户是否有访问对应受保护资源的权限
void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}authentication:要访问资源的访问者的身份
object:要访问的受保护资源,web请求对应
FilterInvocation
configAttributes:是受保护资源的访问策略,通过
SecurityMetadataSource
获取。
下面是Spring Security的基本授权流程:
- 用户请求访问受保护的资源。
- 请求被拦截,由
FilterSecurityInterceptor
处理。 FilterSecurityInterceptor
从SecurityContextHolder
获取当前用户的Authentication
对象。FilterSecurityInterceptor
获取访问所需的权限信息。AccessDecisionManager
被调用来进行决策,判断用户是否有足够的权限访问资源。AccessDecisionManager
委托给AccessDecisionVoter
进行投票,根据策略决定用户是否有访问权限。- 所有
AccessDecisionVoter
完成投票后,AccessDecisionManager
根据策略计算最终的投票结果。 AccessDecisionManager
将决策结果返回给FilterSecurityInterceptor
。FilterSecurityInterceptor
根据决策结果决定是否允许用户访问资源。- 如果允许访问,用户将获得对受保护资源的访问权限。
1 | 用户请求访问受保护的资源 |
决策流程
在AccessDecisionManager
的实现类ConsensusBased
中,使用投票的方式来确定是否能访问受保护的资源。
AccessDecisionManager
中包含了一系列的AccessDecisionVoter
讲会被用来对 Authentication
是否有权访问受保护对象进行投票,AccessDecisionManager
根据投票结果,做出最终角色。
投票是因为权限可以从多个方面来进行配置,有角色但是没有资源,这就需要有不同的处理策略
1 | public interface AccessDecisionVoter<S> { |
vote()
是进行投票的方法。投票可以表示赞成、拒绝、弃权。
Spring Security内置了三个基于投票的实现类,分别是 AffirmativeBased
,ConsensusBasesd
,UnanimaousBased
AffirmativeBased的逻辑:只要有一 个投票通过,就表示通过。
1、只要有一个投票通过了,就表示通过。
2、如果全部弃权也表示通过。
3、如果没有人投赞成票,但是有人投反对票,则抛出
AccessDeniedException
。ConsensusBased的逻辑:多数赞成就通过。
1、赞成票多于反对票则表示通过。
2、反对票多于赞成票则抛出
AccessDeniedException
。3、赞成票与反对票相同且不等于0,并且属性
allowIfEqualGrantedDeniedDecisions
为true
,则表示通过,否则抛出AccessDeniedException
。参数allowIfEqualGrantedDeniedDecisions
的值默认true
。4、如果所有的
AccessDecisionVoter
都弃权,则视参数allowIfAllAbstainDecisions
的值而定,如果该值为true
则通过,否则抛出异常AccessDeniedException
。参数allowIfAllAbstainDecisions
的值默认false
UnanimousBased相当于一票否决。
1、受保护对象配置的某一个
ConfigAttribute
被任意的AccessDecisionVoter
反对了,则将抛出AccessDeniedException
。
2、没有反对票,但是有赞成票,则表示通过。
3、如果全部弃权了,则将视参数allowIfAllAbstainDecisions
的值而定,true
通过,false
抛出AccessDeniedException
。
Spring Security默认使用
AffirmativeBased
投票器,我们同样可以通过往 Spring容器里注入的方式来选择投票决定器
1 | AccessDecisionManager被调用 |
会话控制
用户认证通过后,为避免用户的每次操作都认证,可将用户的信息保存在会话中。Spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
SecurityContextHolder.getContext().getAuthentication()
获取当前登录用户信息。1
2
3
4
5
6
public String getLoginUser(){
Principal principal =
(Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return principal.getName();
}可通过配置
sessonCreationPolicy
参数来了控制如何管理Session。1
2
3protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) }
机制 | 描述 |
---|---|
always | 如果没有Session就创建一个 |
ifRequired | 如果需要就在登录时创建一个 |
never | SpringSecurity将不会创建Session。但是如果应用中其他地方创建了 Session,那么Spring Security就会使用。 |
stateless | SpringSecurity将绝对不创建Session,也不使用。适合于一些REST API 的无状态场景。 |
会话超时时间可以通过spring boot的配置读取。
1
2
3
4server:
servlet:
session:
timeout:session超时后,可以通过SpringSecurity的http配置跳转地址
1
2
3
4
5
6.sessionManagement(session -> session
.invalidSessionUrl("/common/invalidSession")//session失效后跳转到这个页面
.maximumSessions(1)//最大session数
.maxSessionsPreventsLogin(true)//当达到最大值时,是否阻止新的登录
.expiredUrl("/common/invalidSession")//session失效后跳转到这个页面
);expired是指session过期,invalidSession指传入的sessionId失效。
我们可以使用httpOnly和secure标签来保护我们的会话cookie
1
2
3
4
5
6server:
servlet:
session:
cookie:
http-only: true
secure: truehttpOnly:如果为true,那么浏览器脚本将无法访问cookie secure:如果为true,则cookie将仅通过HTTPS连接发送
Spring Security默认实现了logout退出,直接访问/logout就会跳转到登出页面,而ajax访问/logout就可以直接退出。
1
2
3
4
5.logout(logout->logout
.logoutUrl("/logout")
.logoutSuccessUrl("/index.html")//退出登录后跳转到这个页面
.invalidateHttpSession(true)//清除session 默认为true
);//配置退出登录)在退出操作时,会做以下几件事情:
- 使HTTP Session失效。
- 清除SecurityContextHolder
- 跳转到定义的地址。
logoutHandler
一般LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。
下面是Spring Security提供的一些实现
- PersistentTokenBasedRememberMeServices 基于持久化token的 RememberMe功能的相关清理
- TokenBasedRememberMeService 基于token的RememberMe功能的相关清理
- CookieClearingLogoutHandler 退出时Cookie的相关清理
- CsrfLogoutHandler 负责在退出时移除csrfToken
- CecurityContextLogoutHandler 退出时SecurityContext的相关清理
链式API提供了调用相应的 LogoutHandler 实现的快捷方式,比如 deleteCookies()。