Security(五)过滤器详解:过滤器链、自定义过滤器、模仿原生授权过滤器生成自定义授权过滤器,个人心得

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

练习两年半的攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值