SpringSecurity原理
自我整理,部分图片转载
1. DelegatingFilterProxy
1.1 DelegatingFilterProxy是如何代理Security的过滤链的
首先,springboot的自动配置
会在上图红框的SecurityFilterAutoConfiguration
中,注册一个名为springSecurityFilterChain
的DelegatingFilterProxy
DelegatingFilterProxyRegistrationBean是Spring Boot提供的针对Servlet 3.0+ Web的一个注册器Bean(RegistrationBean)
它的作用是向Servlet容器注册一个Servlet Filter,实现类为DelegatingFilterProxy
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(SecurityProperties.class)
@ConditionalOnClass({ AbstractSecurityWebApplicationInitializer.class, SessionCreationPolicy.class })
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class SecurityFilterAutoConfiguration {
//springSecurityFilterChain
private static final String DEFAULT_FILTER_NAME = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME;
@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
SecurityProperties securityProperties) {
//注册一个名为 springSecurityFilterChain 的 DelegatingFilterProxy
DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
DEFAULT_FILTER_NAME);
registration.setOrder(securityProperties.getFilter().getOrder());
registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
return registration;
}
private EnumSet<DispatcherType> getDispatcherTypes(SecurityProperties securityProperties) {
if (securityProperties.getFilter().getDispatcherTypes() == null) {
return null;
}
return securityProperties.getFilter().getDispatcherTypes().stream()
.map((type) -> DispatcherType.valueOf(type.name()))
.collect(Collectors.toCollection(() -> EnumSet.noneOf(DispatcherType.class)));
}
}
但是此处有@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
,意思是在springSecurityFilterChain
注册为Bean之后之后再进行DelegatingFilterProxyRegistrationBean
的注册
那么springSecurityFilterChain
是在哪里先进行注册的呢?如下:
粘贴最后WebSecurityConfiguration
的关键代码
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
相当于
@Bean(name = “springSecurityFilterChain”)
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)//括号里相当于name=springSecurityFilterChain
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
boolean hasFilterChain = !this.securityFilterChains.isEmpty();
Assert.state(!(hasConfigurers && hasFilterChain),
"Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.");
if (!hasConfigurers && !hasFilterChain) {
WebSecurityConfigurerAdapter adapter = this.objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
for (Filter filter : securityFilterChain.getFilters()) {
if (filter instanceof FilterSecurityInterceptor) {
this.webSecurity.securityInterceptor((FilterSecurityInterceptor) filter);
break;
}
}
}
for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
customizer.customize(this.webSecurity);
}
return this.webSecurity.build();
}
总结
springSecurity的自动配置类SecurityFilterAutoConfiguration 会先注册一个springSecurityFilterChain
过滤器链,这个过滤器链是代理类FilterChainProxy
,再用DelegatingFilterProxyRegistrationBean(springSecurityFilterChain)来构建DelegatingFilterProxy代理它
1.2 DelegatingFilterProxy是如何执行过滤器的
GenericFilterBean
GenericFilterBean 实现了Filter,Servlet容器在启动时,首先会调用Filter的init方法
,所以GenericFilterBean 中重写的 init方法也会执行
DelegatingFilterProxy
而DelegatingFilterProxy
继承了GenericFilterBean,所以 GenericFilterBean 的 init方法中, 会执行DelegatingFilterProxy
的initFilterBean
方法
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware,
EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {
//省略中间代码
..........
@Override
public final void init(FilterConfig filterConfig) throws ServletException {
Assert.notNull(filterConfig, "FilterConfig must not be null");
this.filterConfig = filterConfig;
PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
Environment env = this.environment;
if (env == null) {
env = new StandardServletEnvironment();
}
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, env));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
String msg = "Failed to set bean properties on filter '" +
filterConfig.getFilterName() + "': " + ex.getMessage();
logger.error(msg, ex);
throw new NestedServletException(msg, ex);
}
}
//执行子类DelegatingFilterProxy的initFilterBean()方法
initFilterBean();
if (logger.isDebugEnabled()) {
logger.debug("Filter '" + filterConfig.getFilterName() + "' configured for use");
}
}
}
DelegatingFilterProxy的initFilterBean()
将springSecurityFilterChain``FilterChainProxy
对象赋值给
this.delegate`
@Override
protected void initFilterBean() throws ServletException {
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
if (this.targetBeanName == null) {
this.targetBeanName = getFilterName();
}
WebApplicationContext wac = findWebApplicationContext();
if (wac != null) {
this.delegate = initDelegate(wac);
}
}
}
}
执行doFilter方法,相当于执行过滤器,此处的delegate就是上面的
FilterChainProxy
//让代理执行实际的dofilter操作
invokeDelegate(delegateToUse, request, response, filterChain);
invokeDelegate方法执行的代理
FilterChainProxy
实际就是springSecurityFilterChain
过滤器链
总结
DelegatingFilterProxy
间接实现了Filter,Servlet容器在启动时,首先会调用Filter的init方法
,所以DelegatingFilterProxy
中的 initFilterBean方法也会被执行,进而初始化出来一个FilterChainProxy
代理对象,FilterChainProxy
代理的就是springSecurityFilterChain
过滤器链。- 当请求要进入容器时,会经过过滤器,这时
DelegatingFilterProxy
中的doFilter方法会执行,实际执行的就是springSecurityFilterChain
过滤器链。
2.SpringSecurityFilterChain
SpringSecurityFilterChain是如何创建的,以及我们自定义的Security配置类是如何生效的?
SpringSecurity的一系列过滤器链又是如何加入到springSecurityFilterChain,进而被DelegatingFilterProxy代理的呢?
第一步,还是SpringSecurity的入口Filter自动配置类
第二步,进行springSecurityFilterChain的bean注册时进行build构建
通过图片路径一直进入到WebSecurityConfiguration中
进入bulid方法
来到doBuild进行构建
doBuild()
最后调用了WebSecurity
对象的perfomBuild()
,来创建了FilterChainProxy
对象
performBuild()
里遍历securityFilterChainBuilders
建造者列表
把每个SecurityBuilder
建造者对象构建成SecurityFilterChain
实例
最后创建并返回FilterChainProxy
WebSecurity
的performBuild方法,又调用了HttpSecurity
的performBuild方法
HttpSecurity
的performBuild方法,创造了12个过滤器组成的过滤链DefaultSecurityFilterChain
将
HttpSecurity
创建SecurityFilterChain对象,加入到securityFilterChains集合
最后将过滤器构建出来并存入
FilterChainProxy
中
所以,过滤链是通过HttpSecurity
构建出来的,并在WebSecurity
中存入代理的,而我们自定义配置类时,会继承WebSecurityConfigurerAdapter并重写其中三个方法
protected void configure(AuthenticationManagerBuilder auth) throws Exception;
protected void configure(HttpSecurity http) throws Exception;
public void configure(WebSecurity http) throws Exception;
通过重写的方法进行各种访问配置,比如
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置可以直接访问不需要认证的路径
http.authorizeRequests().antMatchers("/","/test/hello","/user/login").permitAll();
//设置拥有admins,role两者任一权限的人可以访问"/test/in",在userDetailsService实现类中可以设置用户权限
http.authorizeRequests().antMatchers("/test/in").hasAnyAuthority("admins","role")
//使用记住我服务
http.rememberMe();
}
WebSecurity
和HttpSecurity
是SecurityBuilder构建起的实现类,所以我们编写自定义配置类重写其方法时,相当于对过滤链的构建器进行了自定义配置,所以自定义配置会生效在过滤链中这就是我们自定义的Security配置类生效的原理
总结
springboot启动后,在SecurityFilterAutoConfiguration自动配置中注册springSecurityFilterChain过滤器链Bean,将一系列过滤器构建出来并存入**FilterChainProxy
**中。
具体构建过程调用了WebSecurity
的performBuild方法,performBuild方法里面做了如下事情:
- 调用
HttpSecurity
的performBuild方法,创建了DefaultSecurityFilterChain,也就是springSecurityFilterChain的实现类,DefaultSecurityFilterChain包含了12个具体过滤器- 将DefaultSecurityFilterChain存入 securityFilterChains 集合(List)
- 使用securityFilterChains 集合构建出**
FilterChainProxy
**并返回
FilterChainProxy
就是security创建的过滤器链
3.Security配置原理
继承WebSecurityConfigurerAdapter
并重写其configure
方法,有三个
protected void configure(AuthenticationManagerBuilder auth) throws Exception;
protected void configure(HttpSecurity http) throws Exception;
public void configure(WebSecurity http) throws Exception;
认证管理的配置AuthenticationManagerBuilder
主要是配置认证相关的,比如
- userDetailsService自定义认证逻辑
- 以编码的形式配置用户名密码、角色和权限
- 基于内存或JDBC的认证方式(将用户信息等保存到内存或数据库)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//从前台请求拿到用户名密码,去userDetailsService中进行验证
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
auth
// 使用基于内存的 InMemoryUserDetailsManager
//.inMemoryAuthentication()
//基于JDBC的认证方式
.jdbcAuthentication()
// 配置用户
.withUser("fox").password("password").roles("admin").authorities("a1")
// 配置其他用户
.and()
.withUser("fox2").password("password").roles("user").authorities("a2");
}
全局安全性的配置WebSecurity
用于影响全局安全性(配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求)的配置设置。一般用于配置全局的某些通用事物,比如,放行静态资源,放行所有免认证就可以访问的路径,比如注册业务,登录业务,退出业务等等。
列举两个WebSecurity专有的配置:
1. ignoring():
用于指定哪些路径不需要被安全框架保护。
这个方法可以接收一个字符串参数,表示需要忽略的路径,也可以接收一个字符串数组参数,表示需要忽略的一组路径。在忽略的路径上的请求将不会经过安全框架的过滤器链,也就是说,这些路径上的请求可以绕过安全控制,直接访问资源。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**",
"/login",
"/logout",
"/css/**",
"/js/**",
"/index.html",
"/favicon.ico",
"/doc.html",
"/webjars/**",
"/swagger-resources/**",
"/v2/api-docs/**",
"/captcha",
"/ws/**"
);
}
}
2. httpFirewall():
HttpFirewall
是spring security提供的HTTP防火墙,它可以用于拒绝潜在的危险请求或者包装这些请求进而控制其行为。HttpFirewall
被注入到FilterChainProxy
中,并在spring security过滤器链执行之前被触发。
Spring security中通过HttpFirewall
来检查请求路径以及参数是否合法,如果合法,才会进入到过滤器链中进行处理。
HttpFirewall
有两个实现类StrictHttpFirewall
严格模式以及DefaultHttpFirewall
普通模式,默认为StrictHttpFirewall
。
下面是一个示例代码,演示如何使用WebSecurity的httpFirewall()方法来配置自定义的HttpFirewall实现:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public HttpFirewall StrictHttpFirewall() {
StrictHttpFirewall shf = new StrictHttpFirewall();
/**
//通过设置allowedHttpMethods进行修改,只允许POST请求。其他请求进不来
Set<String> allowedHttpMethods = new HashSet<>();
allowedHttpMethods.add(HttpMethod.POST.name());
shf.setAllowedHttpMethods(allowedHttpMethods);
*/
//允许所有方式的请求通过
shf.setUnsafeAllowAnyHttpMethod(true);
/**
* 用来校验请求URL是否规范:
*/
//如果请求URL地址中在编码之前或者之后,包含了分号, 即;、%3b、%3B,则该请求会被拒绝,可以通过setAllowSemicolon方法开启或者关闭这一规则。
shf.setAllowSemicolon(false);
//如果请求URL地址中在编码之前或者之后,包含了斜杠, 即%2f、%2F, 则该请求会被拒绝,可以通过setAllowUrlEncodedSlash方法开启或者关闭这一规则。
shf.setAllowUrlEncodedSlash(false);
//如果请求URL地址中在编码之前或者之后,包含了反斜杠, 即\\、%5c、%5C,则该请求会被拒绝,可以通过setAllowBackSlash方法开启或者关闭这一规则。
shf.setAllowBackSlash(false);
//如果请求URL地址中在编码之后包含了%25或者在编码之前包含了%,则该请求会被拒绝,可以通过setAllowUrlEncodedPercent方法开启或者关闭这一规则。
shf.setAllowUrlEncodedPercent(false);
//如果请求URL在编码后包含了英文句号%2e或者%2E,则该请求会被拒绝,可以通过setAllowUrlEncodedPeriod方法开启或者关闭这一规则。
shf.setAllowUrlEncodedPeriod(false);
/**
* 添加受信任的host
* */
shf.setAllowedHostnames((hostname) -> hostname.equalsIgnoreCase("local.javaboy.org"));
return shf;
}
@Override
public void configure(WebSecurity web) throws Exception {
web
.httpFirewall(StrictHttpFirewall());
}
}
针对http请求的配置
用于配置HTTP请求级别安全性
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//设置可以直接访问不需要认证的路径
.antMatchers("/","/test/hello","/user/login").permitAll()
//设置拥有admins,role两者任一权限的人可以访问"/test/in",在userDetailsService实现类中可以设置用户权限
.antMatchers("/test/in").hasAnyAuthority("admins","role")
//设置满足参数任一角色的人可以访问"/test/rt",在userDetailsService实现类中可以设置用户角色,必须加前缀ROLE_
.antMatchers("/test/rt").hasAnyRole("testrole","trole")
.and().rememberMe() //使用记住我服务
.tokenRepository(persistentTokenRepository()) //使用security的token数据库操作
.tokenValiditySeconds(60*60) //设置过期时间
.userDetailsService(userDetailsService) //使用自定义的认证逻辑
.and().csrf().disable(); //关闭csrf防护
}
HttpSecurity和WebSecurity区别
WebSecurity是Spring Security的一个配置类,用于配置如何保护Web应用程序。
它定义了哪些请求需要进行身份验证、哪些请求需要特定的权限等。通过继承WebSecurityConfigurerAdapter类并覆盖configure方法,我们可以自定义WebSecurity的配置。
HttpSecurity是WebSecurity的一部分,它是用于配置HTTP请求级别安全性的对象。
可以认为HttpSecurity是对WebSecurity的进一步细化和扩展。HttpSecurity提供了一系列方法,用于配置如何处理HTTP请求,包括身份验证、授权、跨站请求伪造(CSRF)防护等。我们可以通过调用HttpSecurity的方法来自定义HTTP请求的安全性。
**简而言之,WebSecurity是全局性的安全配置,而HttpSecurity是针对HTTP请求的细粒度安全配置。
**我们可以在WebSecurity中配置一些适用于整个应用程序的安全规则,然后在HttpSecurity中根据具体的HTTP请求进行更详细的安全配置。
4.SecurityContextHolder
SecurityContext的存储策略
SecurityContextHolder可以设置指定JVM策略(SecurityContext的存储策略)
这个策略有三种:
MODE_THREADLOCAL
:SecurityContext 存储在线程中。MODE_INHERITABLETHREADLOCAL
:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。MODE_GLOBAL
:SecurityContext 在所有线程中都相同。
SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。
SecurityContextHolder 使用 ThreadLocal 实现
使用SpringSecurity获取用户
1.继承security的自定义用户类
import org.springframework.security.core.userdetails.UserDetails;
public class LoginUser implements UserDetails
{
/***/
}
2.从SecurityContextHolder中获取对象
LoginUser loginUser = (LoginUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
或者获取权限对象
SecurityContextHolder.getContext().getAuthentication();
5.Security中的过滤器
项目启动默认加载的过滤器,也是默认顺序
Security中的过滤器是以一定的规则排序注册的,在**FilterOrderRegistration
**中,HttpSecurity调用了它
一创建就利用构造方法,决定了过滤器顺序,粘贴部分代码,以下是源码里的过滤器排列顺序
final class FilterOrderRegistration {
FilterOrderRegistration() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter",
order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
order.next());
this.filterToOrder.put(
"org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter",
order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next(); // gh-8105
this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter.class, order.next());
this.filterToOrder.put(
"org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter",
order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(AuthorizationFilter.class, order.next());
put(SwitchUserFilter.class, order.next());
}
}
这些就是所有内置的过滤器。 他们是通过下面的方法获取自己的序号:
Integer getOrder(Class<?> clazz) {
while (clazz != null) {
Integer result = this.filterToOrder.get(clazz.getName());
if (result != null) {
return result;
}
clazz = clazz.getSuperclass();
}
return null;
}
通过过滤器的类全限定名从注册表 filterToOrder 中获取自己的序号,如果没有直接获取到序号通过递归获取父类在注册表中的序号作为自己的序号,序号越小优先级越高。上面的过滤器并非全部会被初始化。有的需要额外引入一些功能包,有的看 HttpSecurity 的配置情况。
比如我们禁用了 CSRF 功能,就意味着 CsrfFilter 不会被注册。
官网过滤器链执行顺序
Spring Security的过滤器链是由多个过滤器组成的,每个过滤器都有不同的功能。当一个请求到达应用程序时,会先经过Spring Security的过滤器链,其中每个过滤器都会对请求进行处理或转发。可以根据需要添加或删除过滤器,以满足不同的需求。需要注意的是,每个过滤器都可以在执行过程中终止请求链,因此过滤器的执行顺序非常重要,需要根据具体的需求进行调整。
Spring Security的过滤器链执行顺序如下:
ChannelProcessingFilter
:用于检查请求的协议是否为HTTPS,如果是则允许请求继续,否则返回错误信息。SecurityContextPersistenceFilter
:用于从Session中获取SecurityContext,如果没有则创建一个新的SecurityContext,保存到Session中。ConcurrentSessionFilter
:用于检测用户的Session是否已经过期或者被其他用户占用,如果是则返回错误信息。LogoutFilter
:用于处理注销请求。X509AuthenticationFilter
:用于处理X.509证书认证请求。AbstractPreAuthenticatedProcessingFilter
:用于处理预认证请求,例如Siteminder或KERBEROS。CasAuthenticationFilter
:用于处理CAS认证请求。UsernamePasswordAuthenticationFilter
:用于处理基于用户名和密码的认证请求。RequestCacheAwareFilter
:用于从缓存中获取之前保存的请求,如果存在则将请求重新发送到目标URL。SecurityContextHolderAwareRequestFilter
:用于包装HttpServletRequest,以确保它在请求链中的任何地方都可以访问SecurityContextHolder。AnonymousAuthenticationFilter
:用于创建匿名用户的认证信息。SessionManagementFilter
:用于处理Session的并发控制和超时。ExceptionTranslationFilter
:用于处理访问被拒绝的请求,例如未经身份验证的请求。FilterSecurityInterceptor
:用于处理基于访问控制的请求,例如验证用户是否有足够的权限来访问请求的资源。
5.1 ChannelProcessingFilter
Spring Security的ChannelProcessingFilter过滤器是用于检查请求的协议是否为HTTPS的过滤器。
如果请求不是HTTPS协议,则会根据配置进行转发或返回错误信息。通常情况下,ChannelProcessingFilter过滤器是在Spring Security的默认过滤器链中自动添加的,不需要额外的配置。
如果没有配置任何requiresChannel()规则,ChannelProcessingFilter过滤器将不会对请求进行任何处理。
ChannelProcessingFilter过滤器可以通过以下方式进行配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requiresChannel()
.antMatchers("/secure/**")//指定需要检查协议的URL地址
.requiresSecure() //指定只允许HTTPS协议的请求
//指定除/secure/**外的其他请求使用非HTTPS协议
.anyRequest().requiresInsecure()//指定只允许非HTTPS协议的请求
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.csrf().disable();
}
}
上述代码中,首先在configure方法中使用requiresChannel()方法配置ChannelProcessingFilter过滤器。其中:
antMatchers()方法用于指定需要检查协议的URL地址,
requiresSecure()方法用于指定只允许HTTPS协议的请求,
requiresInsecure()方法用于指定只允许非HTTPS协议的请求。
需要注意的是,如果没有配置任何requiresChannel()规则,ChannelProcessingFilter过滤器将不会对请求进行任何处理。
5.2 WebAsyncManagerIntegrationFilter
用来处理异步请求的安全上下文传输
WebAsyncManagerIntegrationFilter 是 Spring Security 中的一个过滤器,用于在异步请求中保持安全上下文(SecurityContext)的一致性。
在 Spring MVC 中,当发生异步请求时,WebAsyncManagerIntegrationFilter 会在异步请求处理开始之前,将当前线程的安全上下文(SecurityContext)复制一份,并将其传递给异步请求的处理线程,以确保异步请求能够获得正确的安全上下文。
在异步请求处理结束时,WebAsyncManagerIntegrationFilter 会将异步请求处理线程中的安全上下文合并回主请求线程中的安全上下文,以确保安全上下文的一致性。
如果没有 WebAsyncManagerIntegrationFilter 的支持,异步请求可能会导致安全上下文丢失,从而可能导致安全漏洞或其他问题。因此,WebAsyncManagerIntegrationFilter 在 Spring Security 中是非常重要的一个过滤器。
什么是异步,异步请求会切换线程吗?
是的,异步请求通常会切换线程。spring处理异步请求是如何进行线程切换的
在传统的同步请求中,客户端发送请求,服务端接收请求并处理响应,整个过程都是在一个线程中完成的。但在异步请求中,客户端发送请求后,服务端会立即返回一个响应,而不是等待处理完成后再返回。这时,客户端可以继续做其他的事情,而服务端则会在后台线程中处理请求,并在处理完成后再将响应返回客户端。
因此,异步请求涉及到线程的切换和切换后的上下文管理。为了保证异步请求的正确性,需要在异步请求处理开始前保存请求上下文,然后在异步请求处理结束后恢复请求上下文。
这个过程需要借助一些工具,比如 Spring MVC 中的 DeferredResult 和 Callable,以及 Spring Security 中的 WebAsyncManagerIntegrationFilter。
异步请求时,tomcat与spring中的线程
当客户端发送异步请求到Tomcat时,Tomcat会将其分配给一个工作线程进行处理,该工作线程会在Tomcat的线程池中运行。在处理异步请求时,Tomcat会先将请求从工作线程中解绑,使其可以处理其他请求。然后,Tomcat将异步请求转交给执行代码的spring线程来处理
如果spring也开启了异步执行,tomcat的异步请求会交给spring的主线程,由主线程调用执行具体代码的线程去执行业务逻辑,然后主线程不会等待,转而做其他事情,业务逻辑执行结束后,通过回调(Callable)或者Future等技术手段把结果返回给主线程
主线程会将结果返回给tomcat,tomcat将结果响应给客户端
WebAsyncManagerIntegrationFilter的具体逻辑为:
- 从请求属性上获取所绑定的WebAsyncManager,如果尚未绑定,先做绑定。
- 从asyncManager 中获取 key 为 CALLABLE_INTERCEPTOR_KEY 的安全上下文多线程处理器
SecurityContextCallableProcessingInterceptor
, 如果获取到的为 null,新建一个SecurityContextCallableProcessingInterceptor
并绑定 CALLABLE_INTERCEPTOR_KEY 注册到 asyncManager 中。
这里简单说一下 SecurityContextCallableProcessingInterceptor :
它实现了接口 CallableProcessingInterceptor,当它被应用于一次异步执行时,beforeConcurrentHandling() 方法会在**
调用者线程
执行,该方法会相应地从调用者线程
线程获取SecurityContext,然后被调用者线程
线程中执行逻辑时,会使用这个 SecurityContext,从而实现安全上下文从调用者线程到被调用者线程**的传输。
WebAsyncManagerIntegrationFilter
通过 WebSecurityConfigurerAdapter的**getHttp()**方法添加到 HttpSecurity 中成为 DefaultSecurityFilterChain 的一个链节。
5.3 SecurityContextPersistenceFilter
SecurityContextPersistenceFilter过滤器的作用是在请求进入servlet容器之前,检查当前用户是否已经通过身份验证,并已经存在一个SecurityContext对象,如果存在,则将其存储在安全上下文持久化存储中,以便在请求处理过程中可以随时访问。如果不存在,则创建一个新的SecurityContext对象并存储在持久化存储中。这样可以保证在整个请求处理过程中,安全上下文对象始终可用,以便进行授权和身份验证。还可以确保同一会话中的所有请求都可以共享同一个安全上下文
- 在请求处理过程中,SecurityContextPersistenceFilter 会先从 HttpSession 中获取之前保存的安全上下文,然后将其绑定到当前线程的 ThreadLocal 中,以便在后续的请求处理中使用。
- 如果当前 HttpSession 中没有保存安全上下文,SecurityContextPersistenceFilter 会创建一个新的安全上下文,并将其绑定到当前线程的 ThreadLocal 中,并将其保存到 HttpSession 中。
- 在请求处理结束后,SecurityContextPersistenceFilter 会将当前线程中的安全上下文解绑,并将其保存到 HttpSession 中。这样,下一次请求到来时,SecurityContextPersistenceFilter 就可以从 HttpSession 中获取之前保存的安全上下文,以确保同一会话中的所有请求都可以共享同一个安全上下文。
禁用session,或者没有session的情况下SecurityContextPersistenceFilter如何生效?
在禁用 session 的情况下,SecurityContextPersistenceFilter 仍然会生效。
- 当禁用 session 时,Spring Security 会使用一个名为 NullSecurityContextRepository 的 SecurityContextRepository 实现来代替 HttpSessionSecurityContextRepository。
- NullSecurityContextRepository 不会将安全上下文保存到 session 中,而是每次请求都会创建一个新的空的安全上下文,并将其绑定到当前线程的 ThreadLocal 中,以便在后续的请求处理中使用。
- 在请求处理结束后,SecurityContextPersistenceFilter 会将当前线程中的安全上下文解绑,并且不会将其保存到 HttpSession 中。
因此,即使禁用了 session,SecurityContextPersistenceFilter 仍然会起到将当前的安全上下文与请求线程绑定的作用,以确保在同一请求线程中共享同一个安全上下文。这也是 Spring Security 中实现安全认证和授权的基础,无论是否启用 session,都必须使用 SecurityContextPersistenceFilter 来确保在请求处理过程中,安全上下文得到正确的管理和使用。
public class SecurityContextPersistenceFilter extends GenericFilterBean {
// 确保该Filter在一个request处理过程中最多被调到用一次的机制:
// 一旦该Fitler被调用过,他会在当前request增加该属性值为true,利用此标记
// 可以避免Filter被调用二次。
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
// 安全上下文存储库
private SecurityContextRepository repo;
private boolean forceEagerSessionCreation = false;
// 默认使用http session 作为安全上下文对象存储
public SecurityContextPersistenceFilter() {
this(new HttpSessionSecurityContextRepository());
}
public SecurityContextPersistenceFilter(SecurityContextRepository repo) {
this.repo = repo;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 判断同一请求中是否已经执行过此Filter
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
// 设置Filter访问标记
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (this.logger.isDebugEnabled() && session.isNew()) {
this.logger.debug(LogMessage.format("Created session %s eagerly", session.getId()));
}
}
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
// 从安全上下文存储库(缺省是http session)中读取安全上下文对象
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
SecurityContextHolder.setContext(contextBeforeChainExecution);
if (contextBeforeChainExecution.getAuthentication() == null) {
logger.debug("Set SecurityContextHolder to empty SecurityContext");
}
else {
if (this.logger.isDebugEnabled()) {
this.logger
.debug(LogMessage.format("Set SecurityContextHolder to %s", contextBeforeChainExecution));
}
}
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
// 当前请求已经被处理完成了,清除SecurityContextHolder并将最新的安全上下文对象保存回安全上下文存储库(缺省是http session)
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
// 移除request中设置的标识
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
this.forceEagerSessionCreation = forceEagerSessionCreation;
}
}
5.4 ConcurrentSessionFilter
Spring Security的ConcurrentSessionFilter过滤器是用于检测用户Session是否已经过期或者被其他用户占用的过滤器。
当一个用户用同一个账号在不同的浏览器或者设备上登录时,如果开启了Concurrent Session Control,则只有一个Session是有效的,其他Session将会失效。
同时,当一个用户在同一个浏览器或者设备上开启了多个窗口或者标签页时,也只有一个Session是有效的。
此过滤器不配置默认不生效
ConcurrentSessionFilter过滤器可以通过以下方式进行配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.maximumSessions(1) //指定最大Session数量
.maxSessionsPreventsLogin(true) //指定如果Session数量超过最大值时是否阻止用户登录
.and()
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.csrf().disable();
}
}
上述代码中,首先在configure方法中使用sessionManagement()方法配置SessionManagementFilter过滤器。其中,
maximumSessions()方法用于指定最大Session数量
maxSessionsPreventsLogin()方法用于指定如果Session数量超过最大值时是否阻止用户登录。如果设置为true,则新的用户登录请求将会被拒绝,如果设置为false,则旧的用户Session将会被踢出。
需要注意的是,ConcurrentSessionFilter过滤器只有在开启了Concurrent Session Control功能后才会生效
。可以通过在configure方法中使用sessionManagement()方法进行配置来开启此功能。
5.5 HeaderWriterFilter
HeaderWriterFilter过滤器的作用是在请求完成后,向响应中添加一些HTTP头信息。这些HTTP头信息可以用于安全、缓存控制、浏览器兼容性等方面。
HTTP安全响应头是通过浏览器来生效的。
当Web应用向浏览器发送HTTP响应时,其中包含的HTTP安全响应头会被浏览器读取并解析,然后根据响应头中的设置来执行相应的安全策略。因此,HTTP安全响应头可以有效地防止各种Web攻击,如跨站脚本(XSS)攻击、点击劫持攻击、中间人攻击等,从而提高Web应用的安全性。
一些常见的HTTP头信息如下:
- X-Content-Type-Options:用于防止浏览器的MIME嗅探攻击,值为nosniff。
- X-Frame-Options:用于防止点击劫持攻击,值为SAMEORIGIN或DENY。
- X-XSS-Protection:用于启用浏览器的XSS保护机制,值为1;mode=block。
- Content-Security-Policy:用于控制资源加载的来源,防止跨站脚本攻击。
- Cache-Control:用于控制缓存的行为,如no-cache、no-store、max-age等。
- Expires:用于设置响应的过期时间,可以与Cache-Control联合使用。
HeaderWriterFilter过滤器可以很方便地添加这些HTTP头信息,提高Web应用的安全性和性能。
默认的3个响应头
HeaderWriterFilter可以很方便地添加或修改响应头信息,有以下几种常见的方式:
- 向HeaderWriterFilter过滤器中添加响应头配置,可以通过在WebSecurityConfigurerAdapter中进行配置。以下是一个添加响应头配置的示例:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.addHeaderWriter(new StaticHeadersWriter("X-Content-Type-Options", "nosniff"))
.addHeaderWriter(new StaticHeadersWriter("X-Frame-Options", "SAMEORIGIN"))
.addHeaderWriter(new StaticHeadersWriter("X-XSS-Protection", "1; mode=block"))
.addHeaderWriter(new StaticHeadersWriter("Content-Security-Policy", "default-src 'self'"))
.addHeaderWriter(new CacheControlHeadersWriter());
}
}
在上述示例中,通过addHeaderWriter()方法向HttpSecurity中添加多个HeaderWriter,用于添加响应头配置。
其中,
StaticHeadersWriter用于添加固定的响应头信息,
CacheControlHeadersWriter用于添加缓存控制相关的响应头信息,
ContentSecurityPolicyHeaderWriter用于添加控制资源加载的响应头信息。
- 使用HttpServletResponse.setHeader()方法:
在HttpServletResponse中,可以使用setHeader()方法来添加或修改响应头信息。可以在控制器方法或拦截器中,通过获取response对象来设置头信息,如下所示:
response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "SAMEORIGIN");
response.setHeader("X-XSS-Protection", "1; mode=block");
- 使用HttpHeaders类:
在Spring框架中,可以使用HttpHeaders类来操作响应头信息。可以在控制器方法或拦截器中,通过构造HttpHeaders对象,来设置头信息,如下所示:
HttpHeaders headers = new HttpHeaders();
headers.set("X-Content-Type-Options", "nosniff");
headers.set("X-Frame-Options", "SAMEORIGIN");
headers.set("X-XSS-Protection", "1; mode=block");
然后将HttpHeaders对象传递给ResponseEntity或ResponseEntityBuilder对象,最终生成响应体。
5.6 CorsFilter
Spring Security提供了CorsFilter过滤器,用于实现跨域资源共享(CORS)功能。CORS是一种机制,允许Web应用从不同的域名下获取或发送数据。通常情况下,浏览器的同源策略会阻止跨域请求,但通过设置CORS头信息,可以让浏览器接受跨域请求。
Spring Security的CorsFilter过滤器是在WebSecurityConfigurerAdapter配置类中添加的。可以通过以下方式添加CorsFilter过滤器:
以下代码中,首先在configure方法中添加了CorsFilter过滤器,并使用了cors()方法进行配置。
然后在corsConfigurationSource方法中,配置了允许跨域请求的源和请求方法。
需要注意的是,CorsFilter过滤器需要在Spring Security的过滤器链中的最前面执行,以确保其在请求到达其他过滤器之前拦截跨域请求。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and() //添加CorsFilter过滤器
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
//配置允许跨域请求的源和请求方法
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
configuration.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
5.7 LogoutFilter
Spring Security的LogoutFilter过滤器是用于处理注销请求的过滤器。
doFilter()首先对request的url与配置的登出的url进行比对,若是配置的退出登录的url则与request中的url不一致,则直接执行过滤链的后续过滤器,反之则进行退出登录操作,清除各种信息,发送一个退出登录的spring事件
源码:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//requiresLogout,对比请求url是不是登出的url
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Logging out [%s]", auth));
}
//会清除security上下文、用户认证信息、使Session失效、清空Remember-Me Cookie等操作
this.handler.logout(request, response, auth);
//发送推出登录事件
this.logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
chain.doFilter(request, response);
}
当用户发起注销请求时,LogoutFilter过滤器会清除用户认证信息、使Session失效、清空Remember-Me Cookie等操作。
通常情况下,LogoutFilter过滤器是在Spring Security的默认过滤器链中自动添加的,不需要额外的配置。
LogoutFilter过滤器可以通过以下方式进行配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.csrf().disable();
}
}
上述代码中,首先在configure方法中使用logout()方法配置注销功能。
其中,
logoutUrl()方法用于指定注销URL地址
logoutSuccessUrl()方法用于指定注销成功后重定向的URL地址
invalidateHttpSession()方法用于使Session失效
deleteCookies()方法用于删除指定的Cookie信息
需要注意的是,LogoutFilter过滤器默认情况下会在GET和POST请求中进行注销操作。如果需要使用其他请求方式进行注销,可以通过配置LogoutHandler来实现。例如:
http.logout()
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//处理注销请求
}
});
在上述代码中,通过addLogoutHandler()方法添加自定义的LogoutHandler,实现自定义的注销操作。
5.8 X509AuthenticationFilter
Spring Security的X509AuthenticationFilter过滤器是用于基于X.509证书对用户进行身份认证的过滤器。
X.509是一种公钥证书标准,用于在公开网络中进行数据安全传输,主要用于Web应用程序的SSL/TLS身份认证。
X509AuthenticationFilter过滤器可以在Web应用程序中使用X.509证书进行身份认证,这种方式通常用于客户端证书认证。
X509AuthenticationFilter过滤器可以通过以下方式进行配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.x509()
.subjectPrincipalRegex("CN=(.*?)(?:,|$)") //指定X.509证书中用于提取用户名的正则表达式
.userDetailsService(userDetailsService()) //指定从数据库或其他数据源中加载用户信息的服务
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
上述代码中,首先在configure方法中使用x509()方法配置X509AuthenticationFilter过滤器。其中,
subjectPrincipalRegex()方法用于指定X.509证书中用于提取用户名的正则表达式,
userDetailsService()方法用于指定从数据库或其他数据源中加载用户信息的服务。
在用户进行身份认证时,X509AuthenticationFilter过滤器将从请求中提取证书信息,并使用subjectPrincipalRegex()方法指定的正则表达式从证书中提取用户名,然后使用userDetailsService()方法指定的服务进行用户信息的加载和身份认证。
需要注意的是,使用X.509证书进行身份认证需要在Web服务器和客户端之间建立SSL/TLS连接,并配置客户端证书。在Tomcat、Jetty等Web服务器中,可以通过配置SSL/TLS连接的方式开启客户端证书认证。
5.9 AbstractPreAuthenticatedProcessingFilter
Spring Security的AbstractPreAuthenticatedProcessingFilter过滤器是一个抽象类,用于在用户进行身份认证之前对请求进行预处理的过滤器。
该过滤器主要用于集成第三方认证系统或者使用其他方式进行身份认证的场景,可以在用户进行登录认证之前对请求进行身份认证和授权。
自定义认证过滤器可以继承此过滤器,比如短信认证、人脸识别认证等
AbstractPreAuthenticatedProcessingFilter过滤器可以通过继承该类并实现相关方法来实现自定义的身份认证逻辑。
具体而言,需要实现getPreAuthenticatedPrincipal()方法和getPreAuthenticatedCredentials()方法,
getPreAuthenticatedPrincipal()方法用于从请求中提取用户身份信息,
getPreAuthenticatedCredentials()方法用于从请求中提取用户凭证信息。
以下是一个示例代码:
public class MyPreAuthenticatedProcessingFilter extends AbstractPreAuthenticatedProcessingFilter {
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
// 从请求中提取用户身份信息
String username = request.getHeader("username");
return username;
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
// 从请求中提取用户凭证信息
String password = request.getHeader("password");
return password;
}
}
上述代码中,我们自定义了一个MyPreAuthenticatedProcessingFilter过滤器,继承自AbstractPreAuthenticatedProcessingFilter类,并实现了getPreAuthenticatedPrincipal()方法和getPreAuthenticatedCredentials()方法。
在getPreAuthenticatedPrincipal()方法中,我们从请求头中提取了用户的用户名,而在getPreAuthenticatedCredentials()方法中,我们从请求头中提取了用户的密码。
需要注意的是,自定义的AbstractPreAuthenticatedProcessingFilter过滤器需要在Spring Security的配置类中进行注册
可以使用以下代码进行配置:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyPreAuthenticatedProcessingFilter myPreAuthenticatedProcessingFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilter(myPreAuthenticatedProcessingFilter)
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.csrf().disable();
}
}
上述代码中,我们在configure()方法中使用addFilter()方法将自定义的MyPreAuthenticatedProcessingFilter过滤器添加到Spring Security的过滤器链中。
具体应用可参考下面的第7节自定义手机短信认证登录
5.10 CasAuthenticationFilter
CasAuthenticationFilter是Spring Security提供的CAS认证过滤器,用于处理用户通过CAS认证的过程。在用户访问受保护资源时,CasAuthenticationFilter会判断用户是否已经通过CAS认证,如果已经通过认证则直接放行,否则将用户重定向到CAS服务器进行认证。
CasAuthenticationFilter会拦截 CAS Server 返回的验证请求,并将其转换为 Spring Security 可以识别的 Authentication 对象。该过滤器的主要职责包括:
- 拦截 CAS Server 返回的验证请求。
- 将 CAS Server 返回的验证请求转换为 Spring Security 可以识别的 Authentication 对象。
- 将 Authentication 对象传递给后续的过滤器进行处理。
- 如果验证失败,将用户重定向到 CAS Server 进行重新验证。
CasAuthenticationFilter 认证过程:
- 在用户访问受保护资源时,CasAuthenticationFilter会调用认证提供者CasAuthenticationProvider判断用户是否已经通过CAS认证。
- 如果已经通过认证则直接放行。否则会抛出CasAuthenticationException异常表示认证失败,CAS异常抛出后,ExceptionHandlingConfigurer就会调用authenticationEntryPoint方法进行重定向
- 用户请求被重定向到CAS服务器进行认证,认证通过后会被authenticationEntryPoint重定向回到浏览器,并带有ticket凭证
- 用户带着ticket凭证重新访问受保护资源,CasAuthenticationFilter会再次调用认证提供者CasAuthenticationProvider,提供者会拿着ticket凭证去CAS认证中心做校验,校验通过,认证成功
在CAS集成中,如果用户的CAS认证失败,CasAuthenticationFilter会一直重定向到CAS Server进行重新认证,直到认证成功或达到了重定向次数限制。
默认情况下,CasAuthenticationFilter的重定向次数限制为5次,可以通过设置authenticationFailureRedirectUrl属性来调整该限制,如下所示
spring.security.cas.filter.failure-maximum=3
还可以同时设置达到最大重定向次数后的等待时间,以下设置了CasAuthenticationFilter的"redirectAfterValidation"属性为10,表示最多重定向10次,并在达到重定向次数限制后等待10秒后再次尝试重定向
# CAS Filter配置,单位:秒
cas.filter.redirect-after-validation=10
但是还需要将以上的配置,配置到springBoot中
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CasProperties casProperties;
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
casAuthenticationFilter.setFilterProcessesUrl(casProperties.getLogin().getRedirectUrl());
casAuthenticationFilter.setServiceProperties(serviceProperties());
casAuthenticationFilter.setRedirectAfterValidation(casProperties.getFilter().getRedirectAfterValidation());
return casAuthenticationFilter;
}
// 其他安全配置
}
在 Spring Boot 项目中使用 Spring Security 的 CasAuthenticationFilter 过滤器,可以按照以下步骤进行配置:
- 在 pom.xml 文件中添加 Spring Security 和 CAS Client 的依赖:
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<!-- Spring Security dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
- 在 application.properties 或 application.yml 文件中添加 CAS Client 的配置:
cas:
#对服务端进行配置
server:
#cas server的主路径,这里指向我们的cas服务端
prefix: http://localhost:8080/cas
#指向cas服务端的登录页面
login: ${cas.server.prefix}/login
#指向cas服务端的退出登录页面
logout: ${cas.server.prefix}/logout
#对客户端进行配置
client:
#配置cas客户端url前缀
prefix: http://localhost:8081
#配置客户端登录地址
login: ${cas.client.prefix}/login/cas
#以相对路径的形式配置退出登录接口
relative: /logout/cas
#以绝对路径的形式配置退出登录接口
logout: ${cas.client.prefix}${cas.client.relative}
user:
#配置登录到cas服务端的用户名
inmemory: admin
server:
port: 8081
3.CAS客户端相关配置
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.validation.Cas20ProxyTicketValidator;
import org.jasig.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationEntryPoint;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import java.util.Arrays;
@Configuration
public class CasSecurityConfig {
@Value("${cas.server.prefix}")
private String casServerPrefix;
@Value("${cas.server.login}")
private String casServerLogin;
@Value("${cas.server.logout}")
private String casServerLogout;
@Value("${cas.client.login}")
private String casClientLogin;
@Value("${cas.client.logout}")
private String casClientLogout;
@Value("${cas.client.relative}")
private String casClientLogoutRelative;
@Value("${cas.user.inmemory}")
private String casUserInMemory;
/**
* 配置CAS认证入口,提供用户浏览器重定向的地址
* 用户浏览器发起登录请求,认证失败则重定向到认证中心,重新认证
*
* @param sp
* @return
*/
@Bean
@Primary
public AuthenticationEntryPoint authenticationEntryPoint(ServiceProperties sp) {
CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
// 配置CAS Server认证的登录地址
entryPoint.setLoginUrl(casServerLogin);
entryPoint.setServiceProperties(sp);
return entryPoint;
}
/**
* 配置CAS Client
*
* 用于描述当前应用程序的服务属性
* 在CAS认证中,服务名称和服务URL是非常重要的信息,用于生成CAS认证请求中的service参数,以及CAS认证返回中的service参数验证。
* 因此,正确设置ServiceProperties非常重要,可以确保CAS认证的准确性和安全性。
*
* ServiceProperties的主要属性包括:
* - service:当前应用程序的服务URL,用于接收CAS Server返回的ticket。,没有url就接受不到认证中心的返回信息了
* - sendRenew:是否强制CAS认证,如果设置为true,则CAS Server会要求用户重新认证。
* - artifactParameterName:CAS认证请求中携带的artifact参数名称,默认为“ticket”。
* - authenticateAllArtifacts:是否对所有的artifact进行认证。
* - serviceParameterName:CAS认证返回中携带的service参数名称,默认为“service”。
*
* @return
*/
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
// 与CasAuthenticationFilter监视的URL一致
serviceProperties.setService(casClientLogin);
// 是否关闭单点登录,默认为false,所以也可以不设置。
serviceProperties.setSendRenew(false);
return serviceProperties;
}
/**
* 配置ticket校验功能,需要提供CAS Server校验ticket的地址
*
* TicketValidator有一个validate验证方法,
* 会拿着ticket票据向CAS认证中心发送请求来验证这个ticket是否正确
* 不正确会抛异常,正确的话就继续执行security的后续流程
* @return
*/
@Bean
public TicketValidator ticketValidator() {
// 默认情况下使用Cas20ProxyTicketValidator,验证入口是${casServerPrefix}/proxyValidate
return new Cas20ProxyTicketValidator(casServerPrefix);
}
/**
* 在内存中创建一个用户并分配权限
* 用于CAS认证前后的security自定义校验
* @return
*/
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//用户名是yaml配置的admin
manager.createUser(User.withUsername(casUserInMemory).password("").roles("USER").build());
return manager;
}
/**
* 设置cas认证处理逻辑
*
* 此处就是Security的CAS过滤器权限提供者
* 在进行CAS认证时,CasAuthenticationProvider会将用户的请求转发给CAS Server进行认证,并根据CAS Server返回的认证结果来进行用户认证。
* 如果CAS Server返回的认证结果为失败,则会抛出CasAuthenticationException异常,表示用户认证失败
*
* @param sp
* @param ticketValidator
* @param userDetailsService
* @return
*/
@Bean
public CasAuthenticationProvider casAuthenticationProvider(ServiceProperties sp, TicketValidator ticketValidator, UserDetailsService userDetailsService) {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
//上一步配置的客户端属性
provider.setServiceProperties(sp);
//配置ticket校验功能,会从请求中获取ticket,然后拿它去cas服务端做认证
provider.setTicketValidator(ticketValidator);
//自定义用户名密码认证逻辑,此处会执行userDetailsServiceImpl验证请求的用户名密码是否正确
provider.setUserDetailsService(userDetailsService);
/*
* setKey方法用于设置CAS认证的密钥(Key),该密钥用于加密和解密与CAS服务器之间的通信。
* CAS服务器会使用该密钥对票据(Ticket)进行加密,以确保票据的安全性。
* 在客户端与CAS服务器之间的通信过程中,需要使用该密钥对票据进行解密,以验证票据的有效性和真实性。
*
* 通过调用setKey方法,您可以设置自定义的密钥,以提高CAS认证的安全性。您可以使用任何字符串作为密钥,但需要确保该密钥的保密性,
* 并与CAS服务器上的密钥保持一致。
* */
provider.setKey("admin");
return provider;
}
/**
* 提供CAS认证专用过滤器,过滤器的认证逻辑由CasAuthenticationProvider提供
*
* @param sp
* @param ap
* @return
*/
@Bean
public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties sp, AuthenticationProvider ap) {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setServiceProperties(sp);
filter.setAuthenticationManager(new ProviderManager(Arrays.asList(ap)));
return filter;
}
/**
* 将注销请求转发到cas server
*
* 在访问casClientLogoutRelative的地址时,过滤器会执行casServerLogout地址的注销操作
* 换句话说就是客户端执行注销,CAS服务端也跟着注销
*
* @return
*/
@Bean
public LogoutFilter logoutFilter() {
LogoutFilter logoutFilter = new LogoutFilter(casServerLogout, new SecurityContextLogoutHandler());
// 设置客户端注销请求的路径,用户在访问参数地址时,LogoutFilter会拦截该请求并执行退出登录的相关操作
logoutFilter.setFilterProcessesUrl(casClientLogoutRelative);
return logoutFilter;
}
/**
* 配置单点注销过滤器,接受cas服务端发出的注销请求
* 注销后,再次登录需要重新验证
*
* @return
*/
@Bean
public SingleSignOutFilter singleSignOutFilter() {
SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
singleSignOutFilter.setCasServerUrlPrefix(casServerPrefix);
singleSignOutFilter.setIgnoreInitConfiguration(true);
return singleSignOutFilter;
}
}
4.security配置
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.logout.LogoutFilter;
@EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationProvider authenticationProvider;
@Autowired
private AuthenticationEntryPoint entryPoint;
@Autowired
private CasAuthenticationFilter casAuthenticationFilter;
@Autowired
private SingleSignOutFilter singleSignOutFilter;
@Autowired
private LogoutFilter requestSingleLogoutFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasRole("USER")
.antMatchers("/login/cas", "/favicon.ico", "/error")
.permitAll()
.anyRequest()
.authenticated()
.and()
/**
* 在进行CAS认证时,CasAuthenticationProvider会将用户的请求转发给CAS Server进行认证,并根据CAS Server返回的认证结果来进行用户认证。
* 如果CAS Server返回的认证结果为失败,则会抛出CasAuthenticationException异常,表示用户认证失败
*
* ExceptionHandlingConfigurer是Spring Security提供的一个异常处理配置类,用于处理在Spring Security认证授权过程中发生的异常。
* CAS异常抛出后,ExceptionHandlingConfigurer就会调用authenticationEntryPoint方法进行重定向
*
* authenticationEntryPoint方法是ExceptionHandlingConfigurer中用于配置未认证用户访问受保护资源时的处理方式,
* 即认证入口点(AuthenticationEntryPoint)。具体来说,它的作用如下:
* 1. 在用户未登录或者登录失效时,将用户重定向到指定的认证入口点进行认证;
* 2. 认证入口点处理完成后,将用户重定向回来,并显示相应的错误信息,以便用户进行下一步操作。
*
* */
.exceptionHandling()
//设置认证入口,用来重定向
.authenticationEntryPoint(entryPoint)
.and()
//添加过滤器,执行单点登录处理逻辑,security默认不生成这个过滤器,需要配置
.addFilter(casAuthenticationFilter)
//处理认证逻辑
.addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
//处理退出登录
.addFilterBefore(requestSingleLogoutFilter, LogoutFilter.class);
}
}
5.11 UsernamePasswordAuthenticationFilter
详见第6个二级标题,Security的认证流程及原理
5.12 RequestCacheAwareFilter
RequestCacheAwareFilter
是 Spring Security 中的一个过滤器,它的作用是将当前的 HTTP 请求缓存起来,以便在用户成功登录之后,能够将用户重定向回之前被缓存的请求。
具体来说,RequestCacheAwareFilter
会在用户请求需要认证的资源时,将当前的请求信息(包括请求 URL、请求参数等)保存到请求缓存中。然后,当用户成功登录之后,会将缓存中的请求信息取出来,使用重定向的方式将用户重定向回之前被缓存的请求。
这个过滤器的作用在于,在用户进行登录操作之后,能够快速地将用户重定向回之前的页面,提高用户体验。同时,通过缓存请求信息,也可以防止在用户登录后,由于认证信息失效或过期等原因,而导致用户需要重新发起请求的情况。
在 Spring Boot 中使用 RequestCacheAwareFilter
过滤器的方式与在 Spring Security 配置文件中使用类似,只需要将过滤器作为一个 Bean 注入到 Spring 容器中即可。
以下是一个示例代码:
- 创建
RequestCache
和RequestCacheAwareFilter
的 Bean:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public RequestCache requestCache() {
return new HttpSessionRequestCache();
}
@Bean
public RequestCacheAwareFilter requestCacheAwareFilter() {
RequestCacheAwareFilter filter = new RequestCacheAwareFilter();
filter.setRequestCache(requestCache());
return filter;
}
// ...省略其他配置
}
- 在授权失败处理器中,使用
RequestCache
将当前请求缓存起来:
@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private RequestCache requestCache;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws ServletException, IOException {
// 将当前请求缓存起来
requestCache.saveRequest(request, response);
// 调用父类的方法,处理授权失败的逻辑
super.onAuthenticationFailure(request, response, exception);
}
}
- 在登录成功处理器中,使用
RequestCache
重定向到之前被缓存的请求:
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
// 调用父类的方法,处理登录成功的逻辑
super.onAuthenticationSuccess(request, response, authentication);
}
}
这样,在 Spring Boot 应用中就可以使用 RequestCacheAwareFilter
过滤器进行请求缓存和重定向操作了。
5.13 SecurityContextHolderAwareRequestFilter
SecurityContextHolderAwareRequestFilter
是 Spring Security 中的一个过滤器,它的作用是将当前请求与安全上下文相关联,以便在后续的请求处理过程中可以方便地获取到当前用户的安全上下文。
具体来说,SecurityContextHolderAwareRequestFilter
会在每个请求到达时,将当前请求与安全上下文相关联。这样,在后续的请求处理过程中,就可以通过 SecurityContextHolder.getContext()
方法获取到当前用户的安全上下文,包括认证信息、授权信息等。
这个过滤器的作用在于,方便在请求处理过程中获取到当前用户的安全上下文,从而进行相应的权限判断、访问控制等操作。
在 Spring Boot 中使用 SecurityContextHolderAwareRequestFilter
过滤器的方式与在 Spring Security 配置文件中使用类似,只需要将过滤器作为一个 Bean 注入到 Spring 容器中即可。
以下是一个示例代码:
- 创建
SecurityContextHolderAwareRequestFilter
的 Bean:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public SecurityContextHolderAwareRequestFilter securityContextHolderAwareRequestFilter() {
return new SecurityContextHolderAwareRequestFilter();
}
// ...省略其他配置
}
- 在请求处理方法中,通过
SecurityContextHolder.getContext()
方法获取当前用户的安全上下文:
@GetMapping("/hello")
public String hello() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
// 当前用户已认证
return "Hello, " + authentication.getName();
} else {
// 当前用户未认证
return "Hello, anonymous";
}
}
这样,在 Spring Boot 应用中就可以使用 SecurityContextHolderAwareRequestFilter
过滤器方便地获取到当前用户的安全上下文了。
与SecurityContextPersistenceFilter区别
SecurityContextPersistenceFilter是用来获取或创建SecurityContext的
SecurityContextHolderAwareRequestFilter是将当前请求关联到SecurityContext的
5.14 AnonymousAuthenticationFilter
AnonymousAuthenticationFilter
是 Spring Security 中的一个过滤器,它的作用是在用户未进行认证时,自动创建一个匿名用户的认证信息,并将该认证信息与当前请求相关联。
请求经过AnonymousAuthenticationFilter过滤器时,会判断一下SecurityContext上下文中身份认证信息是否为null,如果为null,则创建一个匿名的身份认证信息并放到SecurityContext上下文环境中
在 Spring Boot 中使用 AnonymousAuthenticationFilter
过滤器的方式与在 Spring Security 配置文件中使用类似,只需要将过滤器作为一个 Bean 注入到 Spring 容器中即可。
以下是一个示例代码:
- 创建
AnonymousAuthenticationFilter
的 Bean:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AnonymousAuthenticationFilter anonymousAuthenticationFilter() {
// 创建一个匿名用户的认证信息
AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken(
"anonymousUser", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
// 创建一个 AnonymousAuthenticationFilter,并将认证信息设置到其中
return new AnonymousAuthenticationFilter("anonymousKey", anonymousAuthenticationToken);
}
// ...省略其他配置
}
或者通过HttpSecurity.antMatchers("***").permitAll()
设置
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
httpSecurity
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
}
- 在请求处理方法中,通过
SecurityContextHolder.getContext().getAuthentication()
方法获取当前用户的认证信息:
@GetMapping("/hello")
public String hello() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !authentication.getName().equals("anonymousUser")) {
// 当前用户已认证
return "Hello, " + authentication.getName();
} else {
// 当前用户未认证
return "Hello, anonymous";
}
}
这样,在 Spring Boot 应用中就可以使用 AnonymousAuthenticationFilter
过滤器自动创建匿名用户的认证信息了,同时还可以方便地获取到当前用户的认证信息。需要注意的是,在创建 AnonymousAuthenticationFilter
的 Bean 时,需要指定一个唯一的 anonymousKey
,以便在后续的操作中可以区分不同的匿名用户。
5.15 SessionManagementFilter
SessionManagementFilter
是 Spring Security 中的一个过滤器,它的作用是管理用户的会话信息,包括会话超时、会话并发等情况。
具体来说,SessionManagementFilter
主要有以下几个作用:
- 监控用户的会话信息,包括会话创建、会话过期等情况;
- 控制用户的会话并发,可以限制同一用户只能在一个客户端登录;
- 处理会话过期或并发登录等情况,可以自定义处理方式,比如强制用户下线、重定向到登录页面等。
这个过滤器的作用在于,保障用户的会话安全,防止会话劫持等攻击,同时也提供了一些便捷的会话管理功能。
SessionManagementFilter是Spring Security中的一个过滤器,用于管理会话。它提供了以下配置选项:
- sessionFixation() - 会话固定攻击保护
- none():禁用会话固定攻击保护(默认值)。
- migrateSession():在用户登录后,将现有会话ID复制到新会话中,以保护免受会话固定攻击。
- newSession():在用户登录后,创建新的会话ID,以保护免受会话固定攻击。
- changeSessionId():在用户登录后,生成新的会话ID,并使旧会话无效。这提供了最高级别的保护,但可能会影响性能。
- maximumSessions() - 并发登录管理
- 指定允许的最大会话数。默认值为-1,表示不限制。
- 如果maximumSessions(1),则同一用户的多个会话将被阻止,并且只有一个会话将保持活动状态。如果maximumSessions(2),则允许用户同时保持两个活动会话。
- maxSessionsPreventsLogin() - 并发登录管理
- 如果设置为true,则当用户尝试创建新会话时,阻止用户登录,直到他们注销现有会话为止。默认值为false,这意味着用户可以创建多个会话。
- expiredUrl() - 无效的会话处理
- 指定当用户的会话过期时要重定向到的URL。例如,expiredUrl(“/login?expired”)表示当用户的会话过期时将其重定向到"/login?expired"页面。
- invalidSessionUrl() - 无效的会话处理
- 指定当用户的会话无效时要重定向到的URL。例如,invalidSessionUrl(“/login?invalid”)表示当用户的会话失效时将其重定向到"/login?invalid"页面。
- sessionRegistry() - 并发登录管理
- 指定使用的会话注册表。默认情况下,使用Spring Security提供的默认会话注册表。
- sessionCreationPolicy() - 会话创建策略
- ALWAYS:始终创建新的会话(默认值)。
- IF_REQUIRED:仅在需要时创建新会话。
- NEVER:不会创建新的会话。如果没有现有会话,则将请求视为未经身份验证。
- STATELESS:不使用会话,而使用基于令牌的身份验证。
这些选项可以在Java代码中使用http.sessionManagement()
方法进行配置,例如:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
http.sessionManagement()
.sessionFixation().migrateSession()
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredUrl("/login?expired")
.invalidSessionUrl("/login?invalid");
}
这里配置了会话固定攻击保护,最多允许一个活动会话,不阻止登录,会话过期时将用户重定向到/login?expired
页面,会话失效时将用户重定向到/login?invalid
页面。
这样,在 Spring Boot 应用中就可以使用 SessionManagementFilter
过滤器管理用户的会话信息了。需要注意的是,该过滤器需要配合一些会话管理策略一起使用,比如并发登录策略、无效会话跳转页面、会话过期跳转页面等。
5.16 ExceptionTranslationFilter
ExceptionTranslationFilter
是 Spring Security 中的一个过滤器,它的作用是处理在认证和授权过程中发生的异常,将异常转换为特定的响应格式或者重定向到指定的错误页面。该过滤器主要有以下几个作用:
- 监控认证和授权过程中发生的异常,比如
AccessDeniedException
、AuthenticationException
等;- 将异常转换为特定的响应格式,比如 JSON、XML 等;
- 将异常重定向到指定的错误页面,比如登录页面、403 页面等。
通过对异常进行处理,ExceptionTranslationFilter
可以在保障系统安全的同时,提供友好的错误提示和跳转页面。
需要注意的是,该过滤器需要在 FilterSecurityInterceptor
之前执行,因为 FilterSecurityInterceptor
是 Spring Security 中最核心的过滤器,它负责对请求进行认证和授权,而 ExceptionTranslationFilter
则是在认证和授权过程中进行异常处理的过滤器。
AuthenticationException与AccessDeniedException
AuthenticationException和AccessDeniedException都是Spring Security框架中的异常。
- AuthenticationException表示身份验证异常,通常是由于用户身份验证失败引起的。比如,用户提供的用户名或密码不正确,或者用户的账号已被禁用等等。
- AccessDeniedException表示访问拒绝异常,通常是由于用户访问受限资源时权限不足引起的。比如,一个普通用户试图访问管理员功能,或者一个没有登录的用户试图访问需要登录的资源等等。
这两个异常的处理方式不同:
- 对于AuthenticationException,通常需要将其转化为友好的提示信息,如“用户名或密码错误”等。
- 对于AccessDeniedException,通常需要将其转化为友好的提示信息,如“您没有权限访问该资源”等,并且可能需要跳转到登录页面或其他适当的页面。
-
如果该过滤器检测到AuthenticationException,则将会交给内部的AuthenticationEntryPoint去处理。
AuthenticationEntryPoint的默认实现是LoginUrlAuthenticationEntryPoint。
它是一个实现了AuthenticationEntryPoint接口的类,用于处理用户未经身份验证时的请求。**当用户试图访问受保护的资源但没有提供有效的身份验证凭证时,LoginUrlAuthenticationEntryPoint会将用户重定向到配置的登录页面。**其默认的登录页面是"/login",可以通过在Spring Security配置中配置loginPage()方法来进行更改。
-
如果检测到AccessDeniedException,需要先判断当前用户是不是匿名用户。
-
如果是匿名访问,则和前面一样运行AuthenticationEntryPoint。
-
不是匿名访问,会委托给AccessDeniedHandler去处理,而AccessDeniedHandler的默认实现,是AccessDeniedHandlerImpl。
AccessDeniedHandlerImpl是一个实现了AccessDeniedHandler接口的类,用于处理访问受限资源时权限不足的情况。**当用户试图访问受保护的资源,但没有足够的权限时,AccessDeniedHandlerImpl会返回一个HTTP 403 Forbidden响应。**其默认行为是向客户端发送一个简单的错误消息,但也可以通过覆盖handle()方法来自定义AccessDeniedHandler的行为,例如将用户重定向到另一个页面或返回一个自定义的错误消息
-
在 Spring Boot 中使用 ExceptionTranslationFilter
过滤器的方式与在 Spring Security 配置文件中使用类似,只需要将过滤器作为一个 Bean 注入到 Spring 容器中即可。
以下是一个示例代码:
- 创建
ExceptionTranslationFilter
的 Bean:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...省略其他配置
@Bean
public ExceptionTranslationFilter exceptionTranslationFilter() {
ExceptionTranslationFilter exceptionTranslationFilter = new ExceptionTranslationFilter(new Http403ForbiddenEntryPoint());
// 设置异常转换器
//处理AccessDeniedException异常
exceptionTranslationFilter.setAccessDeniedHandler(accessDeniedHandler());
//处理AuthenticationException异常
exceptionTranslationFilter.setAuthenticationEntryPoint(authenticationEntryPoint());
return exceptionTranslationFilter;
}
// ...省略其他配置
}
或者
@EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationProvider authenticationProvider;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login/cas", "/favicon.ico", "/error")
.permitAll()
.and()
//异常处理
.exceptionHandling()
.authenticationEntryPoint(entryPoint);
}
}
- 配置异常转换器:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...省略其他配置
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 处理访问被拒绝的异常
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"message\":\"" + accessDeniedException.getMessage() + "\"}");
}
};
}
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
return new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
// 处理未认证的异常
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"" + authenticationException.getMessage() + "\"}");
}
};
}
}
- 在请求处理方法中,通过抛出异常的方式模拟异常的发生:
@GetMapping("/hello")
public String hello() {
throw new AccessDeniedException("您没有访问该资源的权限");
}
这样,在 Spring Boot 应用中就可以使用 ExceptionTranslationFilter
过滤器处理异常了。需要注意的是,该过滤器需要配置异常转换器,将异常转换为特定的响应格式或者重定向到指定的错误页面,以提供友好的错误提示。
5.17 FilterSecurityInterceptor
详见第7个二级标题security授权原理
5.18 CsrfFilter
什么是CSRF攻击
CSRF攻击是通过利用受害者在已登录的状态下的身份验证信息来进行的。
攻击者通常会构造一个恶意网页或链接,并诱使受害者点击访问。这个恶意网页中包含了向目标网站发起请求的代码,而受害者在登录状态下浏览这个恶意网页时,浏览器会自动带上在目标网站上有效的身份验证信息(通常是Cookie),从而触发了未经授权的操作。
举个简单的例子:
假设受害者已经登录了银行网站,并且浏览器中保存了登录状态的Cookie。
攻击者制作了一个恶意网页,其中包含了一个不可见的图片标签,而这个图片的链接是银行网站执行转账操作的请求。
受害者在不知情的情况下访问了这个恶意网页,浏览器会自动加载图片,触发了转账请求,因为浏览器会自动带上在银行网站有效的Cookie。这样,攻击者就成功利用受害者的登录状态完成了转账操作。
要防止这种攻击,开发者可以采取上文提到的防御措施,如使用CSRF Token、设置SameSite属性、敏感操作确认等,来确保请求的合法性和安全性。
Security的CSRF防护
CsrfFilter
是Spring Security框架中的一个过滤器,用于防范跨站请求伪造(CSRF)攻击。它是Spring Security提供的一项安全功能,旨在保护应用程序免受CSRF攻击。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序。
Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
Security是通过CSRF Token来防止CSRF攻击的:
CSRF攻击者不会获取受害者的Cookie或其他信息,而是诱使受害者在自己不知情的情况下执行了恶意操作,从而使浏览器自动发送了包含受害者身份验证信息的请求。所以,攻击者无法获取CSRF Token,从而防止CSRF攻击。
CsrfFilter
默认使用HttpSessionCsrfTokenRepository策略。
- 在
doFilterInternal
方法中,前端第一次访问会为每个用户会话生成一个CSRF令牌,并将这个令牌添加到用户请求中的表单参数中。- 后续在服务器端接收到请求时,
CsrfFilter
会验证提交的CSRF令牌与用户会话中生成的令牌是否匹配,从而确保请求是合法的。
要在Spring Security中使用CsrfFilter
,通常需要在配置类中进行相应的配置,包括启用CSRF保护以及定义需要排除的URL等。以下是一个简单的示例:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() // 开启CSRF保护
.csrfTokenRepository(new HttpSessionCsrfTokenRepository())//将CSRF令牌存储在HttpSession中,这是默认实现可以不配置
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll() // 允许公开访问的URL
.anyRequest().authenticated() // 其他请求需要身份验证
.and()
.formLogin()
.and()
.logout()
.and()
.addFilterAfter(new CsrfFilter(), CsrfFilter.class); // 添加CsrfFilter
}
}
在上面的示例中,addFilterAfter
方法将CsrfFilter
添加到Spring Security的过滤器链中,确保请求中包含合法的CSRF令牌。
如果想要自定义CSRF防护,需要写CsrfTokenRepository的实现类并实现相关方法:generateToken生成token、saveToken保存token、loadToken获取token等。
比如重写saveToken方法,去数据库或redis保存csrf令牌
csrfTokenRepository令牌存储策略
可以通过csrfTokenRepository来修改令牌存储策略
在Spring Security中,csrfTokenRepository
接口有几种实现可以选择,用于存储和验证CSRF令牌。以下是常用的几种实现:
HttpSessionCsrfTokenRepository
:将CSRF令牌存储在HttpSession中。这是Spring Security的默认实现。它会在服务器端生成一个CSRF令牌,并将其存储在HttpSession中。每次需要验证CSRF令牌时,会从HttpSession中获取令牌并进行比较。CookieCsrfTokenRepository
:将CSRF令牌存储在客户端浏览器的Cookie中。它在用户访问应用程序时,将CSRF令牌存储在一个名为XSRF-TOKEN
的Cookie中,并通过HTTP响应。每次需要验证CSRF令牌时,会从请求头中获取令牌的值,并与Cookie中的令牌进行比较。这种方法避免了在服务器上存储令牌的开销,提供了一定程度的CSRF保护LazyCsrfTokenRepository
:作用是延迟加载CSRF令牌(Token),这样可以在需要时才生成和获取CSRF令牌,而不是一开始就在每个请求上都生成一个令牌。这有助于减轻服务器负担,并提高性能,因为并非所有请求都需要CSRF令牌
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(new HttpSessionCsrfTokenRepository())
.and()
// 其他配置...
}
}
CSRF Token如何响应至前端
至于如何返回CSRF token,Spring Security并没有提供直接的方式。
你需要自己在服务端生成CSRF token,然后通过某种方式将它传输到客户端,例如作为一个响应头、响应体的一部分,或者以cookie的形式发送。
如果你使用的是Spring Security的Thymeleaf集成,那么你可以在页面上直接使用<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
这样的方式来添加CSRF token。
如果你使用的是前后端分离的架构,你可能需要自己编写一个接口来返回CSRF token。
例如,你可以创建一个/csrf
的接口,当客户端向这个接口发送GET请求时,服务端返回一个包含CSRF token的JSON对象。
需要注意的是,无论你采取何种方式返回CSRF token,都需要确保它不会被恶意第三方获取和滥用。
通常,有两种主要方法将CSRF Token返回给前端并保存:
- HTTP头(Header)方式: Spring Security会将CSRF Token添加到响应的HTTP头中,通常使用名为"X-CSRF-TOKEN"的HTTP头字段。前端可以通过读取响应头,获取并保存CSRF Token。然后,在将来的请求中,前端需要将该Token放入请求头中,通常使用"X-CSRF-TOKEN"字段。
- Cookie方式: Spring Security还可以将CSRF Token作为一个安全的HttpOnly Cookie返回给前端。这个Cookie会由浏览器自动管理,前端无需干预。浏览器在发送请求时,会自动将该Cookie携带到请求头中。
Cookie方式
后端(Spring Security配置):
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 默认是HttpOnly的Cookie方式
.and()
// 其他配置...
}
}
前端(JavaScript示例):
// 在前端页面中,读取响应头中的CSRF Token 并保存
var csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
// 在以后的请求中,将Token放入请求头
xhr.setRequestHeader("X-CSRF-TOKEN", csrfToken);
请注意,前端保存的CSRF Token需要妥善保护,以防止跨站脚本攻击(XSS)。一般来说,这可以通过将Token存储在HttpOnly Cookie中,或者将Token放在JavaScript代码中而不是直接暴露在页面上来实现。
响应头返回CSRF Token:
可以在控制器或响应处理器中返回该令牌,通常是作为响应头的一部分。以下是一个可能的示例:
@RestController
public class ApiController {
@GetMapping("/get-csrf-token")
public ResponseEntity<String> getCsrfToken(HttpServletRequest request) {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
if (csrfToken != null) {
HttpHeaders headers = new HttpHeaders();
headers.add(csrfToken.getHeaderName(), csrfToken.getToken());
return new ResponseEntity<>("CSRF Token", headers, HttpStatus.OK);
}
return new ResponseEntity<>("CSRF Token not found", HttpStatus.NOT_FOUND);
}
}
6.Security的认证流程及原理
用户登录认证的过程
在认证过程中,用户需要提供用户名和密码,Spring Security通过UsernamePasswordAuthenticationFilter
将用户名和密码封装成Authentication
对象,并交由AuthenticationManager
进行认证。如果认证成功,则认证结果会存储在SecurityContextHolder
中。
用户登录认证的核心:
UsernamePasswordAuthenticationFilter
将用户名和密码封装成Authentication
,AuthenticationManager
负责获取AuthenticationProvider
来对Authentication
进行验证
6.1 首先,UsernamePasswordAuthenticationFilter
只会对登录请求进行过滤
UsernamePasswordAuthenticationFilter
源码,设定默认登录请求为/login,请求方式post并通过构造方法,将默认登录请求赋值到父类的requiresAuthenticationRequestMatcher属性
父类构造,修改requiresAuthenticationRequestMatcher属性
请求来临时会调用父类AbstractAuthenticationProcessingFilter
的doFilter方法进行过滤,而doFilter方法中,会判断该请求是不是设定的默认登录请求:
- requiresAuthentication判断如果不是默认登录请求就直接return结束方法,不进入attemptAuthentication,
- 而attemptAuthentication会调用
UsernamePasswordAuthenticationFilter
的用户名密码登陆验证
所以如果不是登录请求不会被UsernamePasswordAuthenticationFilter
处理
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
if (this.requiresAuthenticationRequestMatcher.matches(request)) {
return true;
}
if (this.logger.isTraceEnabled()) {
this.logger
.trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher));
}
return false;
}
需要注意,默认登录请求页面与默认登陆请求都是可以修改的,通过继承WebSecurityConfigurerAdapter重写configure方法
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().loginPage("/index.html") //自定义登录页面
.loginProcessingUrl("/user/login") //自定义登录页面的表单提交登录请求
.defaultSuccessUrl("/success.html").permitAll() //登录成功后跳转的页面
}
loginProcessingUrl(“/user/login”)会更改UsernamePasswordAuthenticationFilter
设定的默认登录请求
点击进入loginProcessingUrl()源码,到达AbstractAuthenticationFilterConfigurer类
进入红框的set方法,会进入UsernamePasswordAuthenticationFilter
的父类AbstractAuthenticationProcessingFilter
,修改requiresAuthenticationRequestMatcher属性
public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requestMatcher;
}
再进行判断时就是用loginProcessingUrl改过的请求路径了
6.2 源码流程
请求进入过滤器
进入AbstractAuthenticationProcessingFilter
的doFilter方法
调用UsernamePasswordAuthenticationFilter
的用户名密码登陆验证方法
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
//UsernamePasswordAuthenticationToken是Authentication的间接子类,所以他是Authentication权限对象,封装了用户名密码
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// 允许子类设置“details”属性
setDetails(request, authRequest);
//获取到ProviderManager,调用其验证方法
return this.getAuthenticationManager().authenticate(authRequest);
}
获取权限提供者
在下一步进入到ProviderManager的authenticate方法
获取AuthenticationProvider提供者来进行权限验证
AuthenticationProvider实现类很多,具体是通过supports方法来判断用哪个
supports方法对比class类型,一致则返回
我们用的权限类是UsernamePasswordAuthenticationToken,而我们重写了自定义认证逻辑使用数据库查询用户名密码做对比,所以调用DaoAuthenticationProvider的supports-(从父类AbstractUserDetailsAuthenticationProvider继承来的)方法,此方法内部写死了用UsernamePasswordAuthenticationToken做对比,所以返回DaoAuthenticationProvider来做提供者
权限提供者进行权限验证
下一步,使用DaoAuthenticationProvider的验证方法-(从父类AbstractUserDetailsAuthenticationProvider继承来的)
进入AbstractUserDetailsAuthenticationProvider 的 authenticate方法
进入DaoAuthenticationProvider的retrieveUser方法,真正调用了自定义用户认证逻辑,即我们自己写的UserDetailsService实现类
如果认证失败会抛出异常,认证成功会调用方法最后的:
return createSuccessAuthentication(principalToReturn, authentication, user);
来创建一个认证成功的Authentication对象
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
认证通过后与未认证Authentication区别
未认证的是用两参构造函数构建的,并且Authenticated凭证为false
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
认证通过后是用三参构造函数构建的,多了用户所具有的权限,并且Authenticated凭证为true
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
认证结果处理
认证结束后会到UsernamePasswordAuthenticationFilter
父类AbstractAuthenticationProcessingFilter
的doFilter
成功处理方法
AbstractAuthenticationProcessingFilter
----》successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
//保存认证信息至SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
/**
* 调用成功处理器的成功处理方法
* 成功处理器是个接口,默认实现为SavedRequestAwareAuthenticationSuccessHandler
*/
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
进入SavedRequestAwareAuthenticationSuccessHandler
的onAuthenticationSuccess方法
最终重定向到登录成功返回的页面,可自定义此页面
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
String targetUrl = savedRequest.getRedirectUrl();
//重定向
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
失败处理方法
AbstractAuthenticationProcessingFilter
----》unsuccessfulAuthentication
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
/**
* 调用失败处理器的处理方法
* 失败处理器也是个接口,SimpleUrlAuthenticationFailureHandler
*/
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
进入SimpleUrlAuthenticationFailureHandler
的 onAuthenticationFailure
最终重定向到登录失败返回的页面,可自定义此页面
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (this.defaultFailureUrl == null) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sending 401 Unauthorized error since no failure URL is set");
}
else {
this.logger.debug("Sending 401 Unauthorized error");
}
response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
return;
}
saveException(request, exception);
if (this.forwardToDestination) {
this.logger.debug("Forwarding to " + this.defaultFailureUrl);
request.getRequestDispatcher(this.defaultFailureUrl).forward(request, response);
}
else {
this.redirectStrategy.sendRedirect(request, response, this.defaultFailureUrl);
}
}
不用过滤器的流程
前后分离,或不用过滤器手动调用AuthenticationManager验证
需要编写代码处理登录请求,进行认证,相当于在Controller中调用UsernamePasswordAuthenticationFilter
的用户名密码登陆验证方法
@Resource
private AuthenticationManager authenticationManager;
//security的权限对象,需要传递到 AuthenticationManager 才可以进行身份验证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
// AuthenticationManager负责调用AuthenticationProvider来对Authentication进行验证
//如果重写了自定义认证,该方法会去调用UserDetailsServiceImpl.loadUserByUsername即自定义认证逻辑
authentication = authenticationManager.authenticate(authenticationToken);
进入WebSecurityConfigurerAdapter配置父类的authenticate方法,获取到ProviderManager
后面都一样,还是进入ProviderManager的authenticate方法,不过没用UsernamePasswordAuthenticationFilter
的话,用户认证信息不会保存到SecurityContextHolder
7.security授权原理
WebSecurity
的performBuild方法会构造一条security的过滤链,最后一个**FilterSecurityInterceptor
**过滤器就是用来判断用户是否具有某个权限的
使用**@PreAuthorize(“hasAnyAuthority(‘…’)”)注解,请求进入过滤器,到达FilterSecurityInterceptor
的doFilter**方法
doFilter方法做了如下事情
先获取attributes注解内容,也就是访问接口需要的权限字符串
获取SecurityContextHolder中保存的权限对象
进行权限的授权认证:
调用决策管理器
AccessDecisionManager
,使用它默认实现类AffirmativeBased
来进行投票
AffirmativeBased
的投票方法又会调用投票器AccessDecisionVoter
,使用投票器针对**@PreAuthorize**注解的实现类PreInvocationAuthorizationAdviceVoter
来执行投票
PreInvocationAuthorizationAdviceVoter
执行投票的过程中,会构建表达式对象MethodSecurityExpressionRoot
,并把从SecurityContextHolder中获取的权限对象,赋予到表达式对象中最后调用表达式对象中对应**@PreAuthorize注解表达式的方法,对比权限对象中的权限信息与@PreAuthorize**注解中的权限字符串,一致则投票成功也就是授权认证成功,否则失败抛出异常授权认证失败
详细过程:
使用**@PreAuthorize(“hasAnyAuthority(‘…’)”)注解,请求进入过滤器,到达FilterSecurityInterceptor
的doFilter方法,执行invoke**
在invoke中执行super.beforeInvocation方法
进入到父类**AbstractSecurityInterceptor
**的 beforeInvocation 方法中
1.先获取attributes注解内容,也就是访问接口需要的权限信息
2.在获取SecurityContextHolder中保存的权限对象
3.进行权限的授权认证
1中的attributes注解内容权限信息
上图的this.obtainSecurityMetadataSource()
方法,会调用MethodSecurityInterceptor
实现的obtainSecurityMetadataSource
方法,返回一个MethodSecurityMetadataSource,
而MethodSecurityMetadataSource
的getAttributes
,在其实现类PrePostAnnotationSecurityMetadataSource
中,会去发现诸如**@PreAuthorize**的注解
2中获取的UsernamePasswordAuthenticationToken对象
3进行授权认证
调用**AbstractSecurityInterceptor
的 beforeInvocation 中的attemptAuthorization**
将用户信息 authenticated、请求携带对象信息 object、访问接口所需的权限信息 attributes,传入到 decide 方法
decide () 是决策管理器 AccessDecisionManager
定义的一个方法
决策管理器有三个实现类
- AffirmativeBased 表示一票通过,
这是 AccessDecisionManager 默认类
; - ConsensusBased 表示少数服从多数;
- UnanimousBased 表示一票反对;
调用**AffirmativeBased
** 的 decide 方法来进行投票vote
使用**AccessDecisionVoter
投票器进行vote**投票
这里用到委托设计模式,即 AffirmativeBased 类会委托投票器进行选举,然后将选举结果返回赋值给 result,然后判断 result 结果值,若为 1,等于 ACCESS_GRANTED 值时,则表示可一票通过,也就是,允许访问该接口的权限。
这里,ACCESS_GRANTED 表示同意、ACCESS_DENIED 表示拒绝、ACCESS_ABSTAIN 表示弃权:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;//表示同意
int ACCESS_ABSTAIN = 0;//表示弃权
int ACCESS_DENIED = -1;//表示拒绝
}
而投票器有多个实现类
这里简单介绍两个常用的:
RoleVoter
: 这是用来判断 url 请求是否具备接口需要的角色,这种主要用于使用注解 @Secured 处理的权限;PreInvocationAuthorizationAdviceVoter
:针对类似注解 @PreAuthorize (“hasAuthority (‘sys:user:add’) AND hasAuthority (‘sys:user:edit’)”) 处理的权限;
因为我用的是@PreAuthorize注解,所以进入**PreInvocationAuthorizationAdviceVoter
的vote**投票方法
this.preAdvice.before进行投票
进入before,使用表达式处理器expressionHandler
默认实现为**DefaultMethodSecurityExpressionHandler
**
进入上图红框中的表达式处理器的createEvaluationContext方法
代码在**AbstractSecurityExpressionHandler
**–》createEvaluationContext
AbstractSecurityExpressionHandler
的createEvaluationContext
会再进入到
DefaultMethodSecurityExpressionHandler
的createSecurityExpressionRoot
DefaultMethodSecurityExpressionHandler会创建一个**MethodSecurityExpressionRoot
**对象,后者用于构建注解的表达式
@PreAuthorize("hasAnyAuthority('pone','ptwo')")
MethodSecurityExpressionRoot
对象的构造过程会调用父类的构造方法,并传入从SecurityContextHolder中获取的权限信息UsernamePasswordAuthenticationToken
MethodSecurityExpressionRoot的父类**SecurityExpressionRoot
**
**SecurityExpressionRoot
**中含有各种表达式的比对方法
比如hasAuthority、hasAnyAuthority、hasRole、hasAnyRole等等
而@PreAuthorize("hasAnyAuthority('*','*')")
注解,则会调用hasAnyAuthority方法
hasAnyAuthority又向下调用hasAnyAuthorityName
**hasAnyAuthorityName **的 getAuthoritySet方法,会获取用户权限,做对比
并与注解hasAnyAuthority('*','*')
中括号参数字符串对比,一致就是有权限则为true,否则为false
最后一路返回到投票管理器,因为我调试时的权限不匹配,所以投票失败,返回ACCESS_DENIED 拒绝
最后抛出授权认证不通过异常
如果授权通过就进入方法进行访问了
8.自定义手机短信认证登录
自定义过滤器SmsAuthenticationFilter
自定义权限类SmsAuthenticationToken
自定义权限管理器AuthenticationManager
自定义权限提供者SmsAuthenticationProvider
思路:
- 先经过SmsAuthenticationFilter ,构造一个没有鉴权的 SmsAuthenticationToken,然后交给
AuthenticationManager处理。- AuthenticationManager通过 for-each 挑选出一个合适的 provider进行处理,当然我们希望这个
provider要是 SmsAuthenticationProvider。- 验证通过后,重新构造一个有鉴权的 SmsAuthenticationToken,并返回给
SmsAuthenticationFilter。- filter 根据上一步的验证结果,跳转到成功或者失败的处理逻辑。
也可以不自定义过滤器,在配置类设置手机号登录请求匿名访问,直接Controller调用
自定义权限管理器AuthenticationManager
获取自定义权限提供者SmsAuthenticationProvider
来验证自定义权限类SmsAuthenticationToken
,验证失败抛异常,通过则返回token
8.1 SmsAuthenticationToken
首先我们编写 SmsAuthenticationToken,这里直接参考 UsernamePasswordAuthenticationToken源码,直接粘过来,改一改
步骤:
principal原本代表用户名,这里改成mobile,代表了手机号码。
credentials 原本代表密码,短信登录用不到,直接删掉。
SmsCodeAuthenticationToken() 两个构造方法一个是构造没有鉴权的,一个是构造有鉴权的。
剩下的几个方法去除无用属性即可。
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* 短信登录 AuthenticationToken,模仿 UsernamePasswordAuthenticationToken 实现
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
* 在这里就代表登录的手机号码
*/
private final Object principal; //存放认证信息。
/**
* 构建一个没有鉴权的 SmsCodeAuthenticationToken
*/
//mobile:表示手机号。
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* 构建拥有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
8.2 SmsAuthenticationFilter
然后编写 SmsAuthenticationFilter,参考 UsernamePasswordAuthenticationFilter的源码,直接粘过来,改一改。
步骤:
认证请求的方法必须为POST
从request中获取手机号
封装成自己的Authenticaiton的实现类SmsCodeAuthenticationToken(未认证)
调用 AuthenticationManager的 authenticate方法进行验证(即SmsCodeAuthenticationProvider)
重点是请求的匹配器,设定只有/authentication/mobile请求才会被此过滤器处理
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* form表单中手机号码的字段name
*/
public static final String WU_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = "mobile";
private boolean postOnly = true; //只处理Post请求。
//请求的匹配器。
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//是否仅 POST 方式
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//取出手机号
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
//去除空格
mobile = mobile.trim();
//这里封装未认证的Token
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
//将请求信息也放入到Token中。
this.setDetails(request, authRequest);
//首先进入方法这里会找到我们自己写的SmsCodeAuthenticationProvider.
//最后将结果放回到这里之后,经过AbstractAuthenticationProcessingFilter,这个抽象类的doFilter,然后调用处理器。成功调用成功处理器,失败调用失败处理器。
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 获取手机号的方法
* @param request
* @return
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
//将请求信息也放入到Token中。
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String mobileParameter() {
return this.mobileParameter;
}
}
8.3 SmsAuthenticationProvider
这个方法比较重要,这个方法首先能够在使用短信验证码登陆时候被 AuthenticationManager
挑中,其次要在这个类中处理验证逻辑。
步骤:
- 实现 AuthenticationProvider接口,实现 authenticate() 和 supports() 方法。
- supports() 方法决定了这个 Provider 要怎么被 AuthenticationManager挑中,我这里通过
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication),处理所有
SmsCodeAuthenticationToken及其子类或子接口。- authenticate()方法处理验证逻辑。
首先将 authentication强转为 SmsCodeAuthenticationToken。
从中取出登录的 principal,也就是手机号。
如果此时仍然没有异常,通过调用 loadUserByUsername(mobile) 读取出数据库中的用户信息。
如果仍然能够成功读取,没有异常,这里验证就完成了。
重新构造鉴权后的 SmsCodeAuthenticationToken,并返回给 SmsCodeAuthenticationFilter。- SmsCodeAuthenticationFilter的父类在 doFilter()方法中处理是否有异常,是否成功,根据处理结果跳转到登录成功/失败逻辑。
关键是supports方法
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
*/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
String mobile = (String) authenticationToken.getPrincipal();
//此处自己去实现
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
// 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
8.4 成功与失败处理逻辑
上面最后说到,在 SmsCodeAuthenticationFilter 的父类,会根据验证结果跳转到成功或失败处理逻辑,现在我们就编写下这个的处理。
验证成功处理:
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
验证失败处理:
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
logger.info("登陆失败");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
}
}
8.5 SmsCodeAuthenticationSecurityConfig
下面我们需要把我们自己写的这么多类添加进 Spring Security 框架中,在以往,我们都是直接往 WebSecurityConfig 中加,但是这样会导致 WebSecurityConfig 内容太多,难以维护。
因此我们可以为每种登录方式都建议一个专属于它的配置文件,再把这个配置文件加入到 WebSecurityConfig 中,进行解耦。
因此建立短信验证码登录的配置文件SmsCodeAuthenticationSecurityConfig :
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
//指定权限管理器AuthenticationManage
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//指定成功与失败处理逻辑
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
/************************************如果没有自定义过滤器,只需要配置下面的*****************************************/
//设置权限提供者的自定义认证逻辑,userDetailsService可以做短信Redis验证
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
//为security添加权限提供者,并将自定义过滤器加入过滤链,放在UsernamePasswordAuthenticationFilter豁免
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
下面我们就需要把自己写的 SmsCodeAuthenticationSecurityConfig加入到 SecurityConfig 中了。
首先将 SmsCodeAuthenticationSecurityConfig 注入进来,然后通过 http.apply(xxx) 添加进去。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
//apply应用上面的配置
http.apply(smsCodeAuthenticationSecurityConfig).and().authorizeRequests()
// 如果有允许匿名的url,填在下面
.antMatchers("/sms/**").permitAll()
.anyRequest().authenticated()
.and()
// 设置登陆页
.formLogin().loginPage("/login")
// 设置登陆成功页
.defaultSuccessUrl("/").permitAll()
.and()
.logout().permitAll();
// 关闭CSRF跨域
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers("/css/**", "/js/**");
}
}
9.用户的权限何时指定
用户的权限是在自定义登录逻辑中,创建 User 对象时指定的
/**
* 自定义用户认证逻辑实现,必须实现UserDetailsService接口
*
* Service注解中名字必须要与config中的userDetailsService一致
* */
@Service("userDetailsService")
public class testUserDetailsService implements UserDetailsService {
@Autowired
private wzyUserMapper wzyUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* 此处去查数据库,查出来的用户名密码封装到下面的User去
* */
wzyUser wzy = wzyUserMapper.selectByName(username);
if(wzy==null){
throw new UsernameNotFoundException("用户不存在");
}
//权限与角色都在这里设置
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role,ROLE_troe,ROLE_sectwo,pone");
/**
* 返回的User会去与页面输入的做对比,如果用户名密码一致则登录成功
* */
return new User(wzy.getName(),passwordEncoder.encode(wzy.getPassword()),auths);
}
}
或者在配置类中指定
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//从前台请求拿到用户名密码,去userDetailsService中进行验证
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
auth
// 使用基于内存的 InMemoryUserDetailsManager
//.inMemoryAuthentication()
//基于JDBC的认证方式
.jdbcAuthentication()
// 配置用户
.withUser("fox").password("password").roles("admin").authorities("a1")
// 配置其他用户
.and()
.withUser("fox2").password("password").roles("user").authorities("a2");
}