车祸现场:整合spring security的时候,自定义一个filter,启动后发现一次请求filter会重复执行了两遍,最终查阅资料得到解决,记录一下。
security的config配置如下:
/**
* 软件版权:流沙~~
* 修改日期 修改人员 修改说明
* ========= =========== =====================
* 2019/11/26 liusha 新增
* ========= =========== =====================
*/
package com.sand.security.web.config;
import com.sand.security.web.filter.MyAuthenticationTokenGenericFilter;
import com.sand.security.web.handler.MyAccessDeniedHandler;
import com.sand.security.web.provider.MyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 功能说明:自定义Spring Security配置
* 开发人员:@author liusha
* 开发日期:2019/11/26 10:34
* 功能描述:安全认证基础配置,开启 Spring Security
* 方法级安全注解 @EnableGlobalMethodSecurity
* prePostEnabled:决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
* secureEnabled:决定是否Spring Security的保障注解 [@Secured] 是否可用
* jsr250Enabled:决定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.
*/
@Configurable
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 用户信息服务
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证管理器:使用spring自带的验证密码的流程
* <p>
* 负责验证、认证成功后,AuthenticationManager 返回一个填充了用户认证信息(包括权限信息、身份信息、详细信息等,但密码通常会被移除)的 Authentication 实例。
* 然后再将 Authentication 设置到 SecurityContextHolder 容器中。
* AuthenticationManager 接口是认证相关的核心接口,也是发起认证的入口。
* 但它一般不直接认证,其常用实现类 ProviderManager 内部会维护一个 List<AuthenticationProvider> 列表,
* 存放里多种认证方式,默认情况下,只需要通过一个 AuthenticationProvider 的认证,就可被认为是登录成功
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码验证方式
* 默认加密方式为BCryptPasswordEncoder
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(6);
}
/**
* 加载自定义的验证失败处理方式
*
* @return
*/
@Bean
public MyAccessDeniedHandler myAccessDeniedHandler() {
return new MyAccessDeniedHandler();
}
/**
* 加载自定义的token校验过滤器
*
* @return
*/
@Bean
public MyAuthenticationTokenGenericFilter myAuthenticationTokenGenericFilter() {
return new MyAuthenticationTokenGenericFilter();
}
/**
* 静态资源
* 不拦截静态资源,所有用户均可访问的资源
*/
@Override
public void configure(WebSecurity webSecurity) {
webSecurity.ignoring().antMatchers("/", "/css/**", "/js/**", "/images/**");
}
/**
* 密码验证方式
* 将用户信息和密码加密方式进行注入
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
// 关闭密码验证方式
// .passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
MyAuthenticationProvider authenticationProvider = new MyAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
httpSecurity
// 关闭crs允许跨越访问
.csrf().disable()
// 自定义登录认证方式
.authenticationProvider(authenticationProvider)
// 自定义验证处理器
.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler()).and()
// 不创建HttpSession,不使用HttpSession来获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 允许登录接口post访问
.antMatchers(HttpMethod.POST, "/auth/login").permitAll()
// 允许验证码接口post访问
.antMatchers(HttpMethod.POST, "/valid/code/*").permitAll().and();
// // 任何尚未匹配的URL只需要验证用户即可访问
// .anyRequest().authenticated()
httpSecurity.addFilterBefore(myAuthenticationTokenGenericFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
自定义的filter配置如下:
/**
* 软件版权:流沙~~
* 修改日期 修改人员 修改说明
* ========= =========== =====================
* 2020/4/19 liusha 新增
* ========= =========== =====================
*/
package com.sand.security.web.filter;
import com.sand.common.util.lang3.StringUtil;
import com.sand.security.web.IUserAuthenticationService;
import com.sand.security.web.handler.MyAuthExceptionHandler;
import com.sand.security.web.util.AbstractTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
/**
* 功能说明:token过滤器
* 开发人员:@author liusha
* 开发日期:2020/4/19 17:30
* 功能描述:用户合法性校验
*/
@Slf4j
public class MyAuthenticationTokenGenericFilter extends GenericFilterBean {
/**
* MyAuthenticationTokenGenericFilter标记
*/
private static final String FILTER_APPLIED = "__spring_security_myAuthenticationTokenGenericFilter_filterApplied";
/**
* TODO 过滤元数据,后续自己实现
*/
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
/**
* 用户基础服务接口
*/
@Autowired
private IUserAuthenticationService userAuthenticationService;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 确保每个请求仅应用一次过滤器:spring容器托管的GenericFilterBean的bean,都会自动加入到servlet的filter chain,
// 而WebSecurityConfig中myAuthenticationTokenGenericFilter定义的bean还额外把filter加入到了spring security中,所以会出现执行两次的情况。
// if (httpRequest.getAttribute(FILTER_APPLIED) != null) {
// chain.doFilter(httpRequest, httpResponse);
// return;
// }
// httpRequest.setAttribute(FILTER_APPLIED, Boolean.TRUE);
log.info("~~~~~~~~~用户合法性校验~~~~~~~~~");
// 白名单直接验证通过
if (isPermitUrl(httpRequest, httpResponse, chain)) {
chain.doFilter(httpRequest, httpResponse);
return;
}
try {
// 非白名单需验证其合法性(非白名单请求必须带token)
String authHeader = httpRequest.getHeader(AbstractTokenUtil.TOKEN_HEADER);
final String authToken = StringUtil.substring(authHeader, 7);
userAuthenticationService.checkAuthToken(authToken);
chain.doFilter(httpRequest, httpResponse);
} catch (Exception e) {
log.error("MyAuthenticationTokenGenericFilter异常", e);
MyAuthExceptionHandler.accessDeniedException(e, httpResponse);
}
}
/**
* 是否是白名单
*
* @param request request
* @param response response
* @param chain chain
* @return true-是白名单 false-不是白名单
*/
public boolean isPermitUrl(ServletRequest request, ServletResponse response, FilterChain chain) {
if (Objects.isNull(filterInvocationSecurityMetadataSource)) {
try {
// 获取security配置的白名单信息
Class clazz = chain.getClass();
Field field = clazz.getDeclaredField("additionalFilters");
field.setAccessible(true);
List<Filter> filters = (List<Filter>) field.get(chain);
for (Filter filter : filters) {
if (filter instanceof FilterSecurityInterceptor) {
filterInvocationSecurityMetadataSource = ((FilterSecurityInterceptor) filter).getSecurityMetadataSource();
}
}
} catch (Exception e) {
log.error("security过滤元数据获取异常,白名单验证失败", e);
return false;
}
}
FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
Collection<ConfigAttribute> permitUrls = filterInvocationSecurityMetadataSource.getAttributes(filterInvocation);
boolean isPermitUrl = false;
if (!CollectionUtils.isEmpty(permitUrls)) {
isPermitUrl = permitUrls.toString().contains("permitAll");
}
if (isPermitUrl) {
log.info("白名单请求url:{}", ((HttpServletRequest) request).getRequestURI());
} else {
log.info("非白名单请求url:{}", ((HttpServletRequest) request).getRequestURI());
}
return isPermitUrl;
}
}
分析原因:MyAuthenticationTokenGenericFilter是继承自GenericFilterBean,由spring容器托管,会自动加入到servlet的filter chain中,而spring security的config配置中又把filter注册到了spring security的容器中,因此在调用UsernamePasswordAuthenticationFilter鉴权之前和鉴权之后先后会各执行一次。
@Bean
public MyAuthenticationTokenGenericFilter myAuthenticationTokenGenericFilter() {
return new MyAuthenticationTokenGenericFilter();
}
解决方案:
1)、security的config配置更改如下代码
// @Bean
// public MyAuthenticationTokenGenericFilter myAuthenticationTokenGenericFilter() {
// return new MyAuthenticationTokenGenericFilter();
// }
httpSecurity.addFilterBefore(new MyAuthenticationTokenGenericFilter(), UsernamePasswordAuthenticationFilter.class);
2)、或者更改自定义的filter配置代码,将以下代码注释打开
if (httpRequest.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(httpRequest, httpResponse);
return;
}
httpRequest.setAttribute(FILTER_APPLIED, Boolean.TRUE);
推荐使用第2种,因为在实际开发过程中可能会需要用到MyAuthenticationTokenGenericFilter,启动的时候注册好方便调用。。。
千万不要试图去研究 研究了很久都整不明白的东西,或许是层次不到,境界未到,也或许是从未在实际的应用场景接触过,这种情况下去研究,只会事倍功半,徒劳一番罢了。能做的就是不断的沉淀知识,保持一颗积极向上的学习心态,相信终有一天所有的困难都会迎刃而解。