Security(五)过滤器详解:过滤器链、自定义过滤器、个人见解、模仿原生授权过滤器生成自定义授权过滤器
前言 :主要讲解:过滤器链、自定义过滤器。
文章目录
前提:
请学习大佬文章:
https://blog.csdn.net/u011066470/article/details/119086893?fromshare=blogdetail&sharetype=blogdetail&sharerId=119086893&sharerefer=PC&sharesource=weixin_44399264&sharefrom=from_link
一、获取自己项目的过滤器链执行顺序
在你的SecurityConfig配置上加上注解:@EnableWebSecurity(debug = true),启动项目后,浏览器访问项目,即可每次访问前打印出此次访问的执行的过滤器
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
......
二、常用(我的项目)过滤器详解
| 过滤器名称 | 过滤器作用 |
|---|---|
| DisableEncodeUrlFilter | 该过滤器禁用 URL 编码。这通常用于确保某些特定的 URL 不会被编码,从而影响它们的行为。 |
| WebAsyncManagerIntegrationFilter | 该过滤器将 Spring Security 的 SecurityContext 绑定到异步处理线程。这对于处理异步请求非常重要,确保 SecurityContext 在异步处理过程中仍然可用。 |
| SecurityContextPersistenceFilter | 该过滤器负责在请求开始时从 SecurityContextRepository(如 HttpSessionSecurityContextRepository)恢复 SecurityContext,并在请求结束时将其保存回去。这是 Spring Security 中最重要的过滤器之一,用于管理 SecurityContext 的生命周期。 |
| HeaderWriterFilter | 该过滤器负责写入响应头。它可以用于添加各种安全相关的头部,例如 X-Frame-Options 或 Content-Security-Policy。 |
| CorsFilter | 该过滤器处理跨域资源共享(CORS)。它允许或拒绝来自不同源的请求,确保跨域请求的安全性。 |
| LogoutFilter | 该过滤器处理注销请求。当用户发起注销请求时,它会调用指定的 logoutHandler 来处理注销逻辑。 |
| RentFailureFilter | 自定义的过滤器,用于处理租户相关的失败逻辑。具体作用需要查看该过滤器的实现。 |
| JwtAuthenticationTokenFilter | 该过滤器负责解析 JWT 并设置 SecurityContext。它通常是自定义的过滤器,用于处理 JWT 认证。 |
| RequestCacheAwareFilter | 该过滤器处理请求缓存。如果用户尝试访问需要认证的资源,该过滤器会缓存原始请求,并重定向用户到登录页面。登录成功后,用户将被重定向回原始请求。 |
| SecurityContextHolderAwareRequestFilter | 该过滤器为 HttpServletRequest 提供一些额外的方法,使得 SecurityContext 更容易访问。它扩展了 HttpServletRequest 接口,提供了更多的安全相关方法。 |
| AnonymousAuthenticationFilter | 该过滤器在 SecurityContext 中提供匿名身份验证。如果当前没有认证的用户,它会创建一个匿名用户并将其放入 SecurityContext。 |
| SessionManagementFilter | 该过滤器管理会话。它可以配置会话固定保护、会话超时等策略,确保会话的安全性。 |
| ExceptionTranslationFilter | 该过滤器处理认证和授权异常。如果在过滤器链中发生认证或授权异常,该过滤器会捕获这些异常并进行相应的处理,例如重定向到登录页面或返回 HTTP 401 状态码。 |
| AuthorizationFilter | 该过滤器处理授权逻辑。它检查用户是否有权限访问某个资源,并根据授权规则决定是否允许访问。 |
三、个人见解:
1、SecurityContextPersistenceFilter是为了从session中获取访问信息,但是我的项目用的是token,所以这一步是获取不到数据的,那么我就得自己写个过滤器把认证信息设置到SecurityContextHolder中,所以有了自定义的JwtAuthenticationTokenFilter,实现如下:
package com.tdxx.framework.security.filter;
import ***
/**
* token过滤器 验证token有效性
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
2、security默认配置的登陆路径是/login,我的配置信息如下,可以看到并没有调整登录路径。当未认证的访问请求需要权限的url时候,那么security会自动跳转到/login页面。
package com.tdxx.framework.config;
import ***
/**
* spring security配置
*/
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 租户过滤器
*/
@Autowired
private RentFailureFilter rentFailureFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 身份验证实现
*/
@Bean
public AuthenticationManager authenticationManager()
{
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
return new ProviderManager(daoAuthenticationProvider);
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception
{
return httpSecurity
// CSRF禁用,因为不使用session
.csrf(csrf -> csrf.disable())
// 禁用HTTP响应标头
.headers((headersCustomizer) -> {
headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
})
// 认证失败处理类
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
// 基于token,所以不需要session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 注解标记允许匿名访问的url
.authorizeHttpRequests((requests) -> {
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage", "/sendPhoneCode", "/test/all/**", "/common/minio/downloadFile").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
.addFilterBefore(rentFailureFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(corsFilter, LogoutFilter.class)
.build();
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
}
3、通过上面配置信息可以看到我并没有设置UsernamePasswordAuthenticationFilter这个过滤器,我的过滤器链中也未打印出这个过滤器。
原因:因为在JwtAuthenticationTokenFilter 中已经设置了认证信息,后续就算设置了UsernamePasswordAuthenticationFilter,也不会执行这个过滤器(认证信息已经存在,则不会再次认证)。而访问匿名url不要权限,访问鉴权url又会自动跳转到登陆页面强制认证,所以UsernamePasswordAuthenticationFilter没必要存在。
4、登录页面的认证代码如下,可以看到认证调用authenticationManager.authenticate(authenticationToken);,而这里的authenticationManager是上面SecurityConfig中的authenticationManager()方法,其中配置的是常用的密码认证提供者:daoAuthenticationProvider,其中设置了加密方法、数据库查询用户的接口。
public String login(String username, String password, String code, String uuid, String orgCode) {
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
//租户有效性校验
loginRentCheck(orgCode);
// // 手机验证码校验
// validatePhoneCode(username,phoneCode);
// 用户验证
Authentication authentication;
try {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(authenticationToken);
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
} else {
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
throw new ServiceException(e.getMessage());
}
} finally {
AuthenticationContextHolder.clearContext();
}
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
recordLoginInfo(loginUser.getUserId());
// 生成token
return tokenService.createToken(loginUser);
}
三、自定义过滤器

实现过滤器可以选择实现OncePerRequestFilter、GenericFilterBean、Filter,网络上自定义过滤器分别实现这三个的写法都有,这里我建议选择实现OncePerRequestFilter,因为:
1、OncePerRequestFilter作为底层的抽象类,其中已经完善了很多功能,不需要重复实现;
2、OncePerRequestFilter位于package org.springframework.web.filter;,这就说明只要你开发的web项目,你就一定有OncePerRequestFilter,而GenericFilterBean、Filter也是在这个包内,所以GenericFilterBean、Filter在位置上并无优势;
3、当我们实现OncePerRequestFilter后,无需关注其他事情,只需要实现doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)方法即可,在逻辑上和美观度上有很大优势。
自定义过滤器示例:
package com.tdxx.framework.security.filter;
import com.tdxx.common.constant.Constants;
import com.tdxx.common.constant.UserConstants;
import com.tdxx.common.core.domain.model.LoginUser;
import com.tdxx.common.utils.MessageUtils;
import com.tdxx.common.utils.StringUtils;
import com.tdxx.framework.manager.AsyncManager;
import com.tdxx.framework.manager.factory.AsyncFactory;
import com.tdxx.framework.web.service.TokenService;
import com.tdxx.system.domain.TdxxRent;
import com.tdxx.system.mapper.TdxxRentMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 租户过滤器 验证租户有效性
*/
@Component
public class RentFailureFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Autowired
private TdxxRentMapper tdxxRentMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser)) {
Long tdxxRentId = loginUser.getUser().getTdxxRentId();
if (tdxxRentId != null && tdxxRentId != UserConstants.RENT_TOP) {
TdxxRent tdxxRent = tdxxRentMapper.selectTdxxRentById(tdxxRentId);
if (tdxxRent == null || tdxxRent.getRentDelayEndDate().getTime() < System.currentTimeMillis()) {
tokenService.delLoginUser(loginUser.getToken());
AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginUser.getUsername(), Constants.RENT_EXPIRE, MessageUtils.message("rent.expire")));
}
}
}
chain.doFilter(request, response);
}
}
四、模仿原生授权过滤器生成自定义授权过滤器
有时候你需要的自定义过滤器不是独特的业务,而是需要模仿原有的过滤器去实现。
比如说模仿授权过滤器:FilterSecurityInterceptor,生成Oauth2FilterSecurityInterceptor,那么就需要模仿 FilterSecurityInterceptor 实现,继承 AbstractSecurityInterceptor 和实现 Filter 接口。但是授权还是建议注解授权。示例代码如下:
package com.fengxuechao.examples.auth.authorization;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.web.FilterInvocation;
import javax.servlet.*;
import java.io.IOException;
/**
* 比较核心的过滤器: 主要负责web应用鉴权的工作。
* 需要依赖:
* - AuthenticationManager:认证管理器,实现用户认证的入口;
* - AccessDecisionManager:访问决策器,决定某个用户具有的角色,是否有足够的权限去访问某个资源;
* - FilterInvocationSecurityMetadataSource:资源源数据定义,即定义某一资源可以被哪些角色访问.
*/
@Slf4j
public class Oauth2FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
private Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if (log.isInfoEnabled()) {
log.info("Oauth2FilterSecurityInterceptor init");
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (log.isInfoEnabled()) {
log.info("Oauth2FilterSecurityInterceptor doFilter");
}
FilterInvocation filterInvocation = new FilterInvocation(request, response, chain);
invoke(filterInvocation);
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// filterInvocation里面有一个被拦截的url
// 里面调用 Oauth2AccessDecisionManager 的 getAttributes(Object object) 这个方法获取 filterInvocation 对应的所有权限
// 再调用 Oauth2AccessDecisionManager 的 decide方法来校验用户的权限是否足够
InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(filterInvocation);
try {
// 执行下一个拦截器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
} finally {
super.afterInvocation(interceptorStatusToken, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
/**
* 资源源数据定义,设置为自定义的 SecureResourceFilterInvocationDefinitionSource
*
* @return
*/
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return securityMetadataSource;
}
public void setOauth2AccessDecisionManager(Oauth2AccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
public void setSecurityMetadataSource(Oauth2FilterInvocationSecurityMetadataSource securityMetadataSource) {
this.securityMetadataSource = securityMetadataSource;
}
}
参考链接如下:
https://blog.csdn.net/Little_fxc/article/details/92763518?fromshare=blogdetail&sharetype=blogdetail&sharerId=92763518&sharerefer=PC&sharesource=weixin_44399264&sharefrom=from_link

过滤器详解:过滤器链、自定义过滤器、模仿原生授权过滤器生成自定义授权过滤器,个人心得&spm=1001.2101.3001.5002&articleId=145468172&d=1&t=3&u=8a11393db1ef4218be505fca0e4cb0ce)
1129

被折叠的 条评论
为什么被折叠?



