使用 Spring 的 AbstractAuthenticationProcessingFilter 成功认证后解决 404

Posted

技术标签:

【中文标题】使用 Spring 的 AbstractAuthenticationProcessingFilter 成功认证后解决 404【英文标题】:Resolved 404 after successfulAuthentication using AbstractAuthenticationProcessingFilter of Spring 【发布时间】:2021-07-30 02:12:21 【问题描述】:

我有一个问题:我正在尝试基于两个 HEADER 实现身份验证,一个 HMAC-SHA256 使用商家的共享密钥和一个商家 ID 签名。 我想要的所有以 /api/payment/** 开头的路径都以这种方式检查。

当我在 POST /fcabpo/api/payment/hmac-validation 中调用我的服务时 似乎验证正确,因为调用了扩展 AbstractAuthenticationProcessingFilter 的我的类的方法successAuthentication gas bene,但似乎调用被转发到GET到上下文根/fcabpo,所以我收到了404。

我编写了扩展 AuthenticationProvider 的类 HMACAuthenticationProvider

@Component
@Slf4j
public class HMACAuthenticationProvider implements AuthenticationProvider 

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException 
        HMACAuthenticationToken hmacAuthenticationToken = (HMACAuthenticationToken)authentication;
        String merchandId = hmacAuthenticationToken.getPrincipalMerchantId();
        HMACSha256Credential hmacSha256Credential = hmacAuthenticationToken.getHmacSha256Credential();
        // TODO check the hmac
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        grantedAuthorities.add(new SimpleGrantedAuthority("MERCHANT"));
        Authentication auth = new UsernamePasswordAuthenticationToken(merchandId, hmacSha256Credential.getHmacSha256(), grantedAuthorities);
        return auth;
    

    @Override
    public boolean supports(Class<?> authentication) 
        return HMACAuthenticationToken.class.equals(authentication);
    


我还写了一个扩展 AbstractAuthenticationProcessingFilter 的 HMACRestSecurityFilter

@Slf4j
public class HMACRestSecurityFilter extends AbstractAuthenticationProcessingFilter 

    public static final String  HEADER_KEY_MERCHANT_ID  = "merchantId";
    public static final String  HMAC_SHA256                         = "HMAC-SHA256";
    public static final String  EPOCH_TIME                          = "EPOCH-TIME";

    public HMACRestSecurityFilter(RequestMatcher requiresAuthenticationRequestMatcher) 
        super(requiresAuthenticationRequestMatcher);
    

    public HMACRestSecurityFilter(String defaultFilterProcessesUrl) 
        super(defaultFilterProcessesUrl);
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException 
        MultiReadHttpServletRequest multiReadHttpServletRequest = ServletUtil.wrapRequest(request);
        String merchantId = multiReadHttpServletRequest.getHeader(HEADER_KEY_MERCHANT_ID);
        String hmacSha256 = multiReadHttpServletRequest.getHeader(HMAC_SHA256);
        String epochTimeStr = multiReadHttpServletRequest.getHeader(EPOCH_TIME);
        if (StringUtils.isBlank(merchantId) || StringUtils.isBlank(hmacSha256) || StringUtils.isBlank(epochTimeStr)) 
            unsuccessfulAuthentication(multiReadHttpServletRequest, response, new BadHMACAuthRequestException("Authentication attempt failed! Request missing mandatory headers."));
        
        long epochTime = 0;
        try 
            epochTime = Long.parseLong(epochTimeStr);
         catch (NumberFormatException e) 
            unsuccessfulAuthentication(multiReadHttpServletRequest, response, new BadHMACAuthRequestException("Authentication attempt failed! Request missing mandatory headers."));
        
        HMACSha256Credential hmacSha256Credential = new HMACSha256Credential();
        byte[] content = multiReadHttpServletRequest.getInputStreamCopy();
        String contentString = new String(content, multiReadHttpServletRequest.getCharacterEncoding());
        hmacSha256Credential.setBody(contentString);
        hmacSha256Credential.setEpochTime(epochTime);
        hmacSha256Credential.setHmacSha256(hmacSha256);
        hmacSha256Credential.setHttpMethod(multiReadHttpServletRequest.getMethod());
        hmacSha256Credential.setUri(multiReadHttpServletRequest.getRequestURI());
        HMACAuthenticationToken hmacAuthenticationToken = new HMACAuthenticationToken(merchantId, hmacSha256Credential);
        return hmacAuthenticationToken;
    

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException 
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        MultiReadHttpServletRequest multiReadHttpServletRequest = ServletUtil.wrapRequest(request);
        super.doFilter(multiReadHttpServletRequest, response, chain);
    

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException 
        super.successfulAuthentication(request, response, chain, authResult);
    

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException 
        super.unsuccessfulAuthentication(request, response, failed);
    


和扩展 AbstractAuthenticationToken 的 HMACAuthenticationToken 以及扩展 WebSecurityConfigurerAdapter 以配置路径 e 过滤器的类

@Configuration
public class ActuatorSecurity extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.httpBasic().and().authorizeRequests().antMatchers("/api/payment/**").hasAnyAuthority("MERCHANT").and().addFilterBefore(new HMACRestSecurityFilter("/api/payment/**"),
            UsernamePasswordAuthenticationFilter.class);

        http.authorizeRequests().anyRequest().permitAll().and().csrf().disable();
    

我不知道这可能是什么问题,也找不到任何有用的东西可以在网上解决。

【问题讨论】:

请启用spring security调试日志,然后发布你的完整日志、启动日志和来自请求的日志,这样我们就可以看到启动时加载了什么,以及它如何记录你的传入请求。 【参考方案1】:

解决方案是调用 chain.doFilter(request, response);在成功认证中

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException 
        super.successfulAuthentication(request, response, chain, authResult);
        chain.doFilter(request, response);
    

并注册一个新的 RedirectStrategy

public class NoRedirectStrategy implements RedirectStrategy 

    @Override
    public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException 


配置类为:

package it.reply.pay.fcabpo.rs.util.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import it.reply.pay.fcabpo.rs.util.security.signature.SignatureAuthenticationProvider;
import it.reply.pay.fcabpo.rs.util.security.signature.SignatureResponseFilter;
import it.reply.pay.fcabpo.rs.util.security.signature.SignatureRestSecurityFilter;
import it.reply.pay.fcabpo.util.constants.ProjectConstants;
import it.reply.pay.fcabpo.util.enums.RoleEnum;

@Configuration
public class ProjectSecurityConfiguration extends WebSecurityConfigurerAdapter 

    public static final String                          PAYMENT_PATH_WILD_CARDS = ProjectConstants.Path.Payment.PAYMENT + "/**";

    @Autowired
    private SignatureAuthenticationProvider authenticationProvider;

    // Se viene aggiunta questa configurazione per AuthenticationManagerBuilder l'autenticatore viene chiamato 2 volte erroneamente in caso di errore
    // .authenticationProvider(authenticationProvider) prima di addFilterBefore
    // verificare se è una configurazione necessaria quando ci sono più autenticatori
    // @Override
    // protected void configure(AuthenticationManagerBuilder auth) 
    // auth.authenticationProvider(authenticationProvider);
    // 

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        // al posto di .hasAnyAuthority(RoleEnum.ROLE_MERCHANT.name()) si puo' anche usare .authenticated() e poi sopra il metodo rest indicare quale sia il ruolo da usare @Secured("ROLE_USER")
        RequestMatcher paymReqMatcher = new AntPathRequestMatcher(PAYMENT_PATH_WILD_CARDS);
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().exceptionHandling().defaultAuthenticationEntryPointFor(forbiddenEntryPoint(),
            paymReqMatcher).and().addFilterBefore(signatureRestSecurityFilter(), AnonymousAuthenticationFilter.class).authorizeRequests().antMatchers(PAYMENT_PATH_WILD_CARDS).hasAnyAuthority(
                RoleEnum.ROLE_MERCHANT.name()).and().csrf().disable().formLogin().disable().httpBasic().disable().logout().disable();

        http.authorizeRequests().anyRequest().permitAll().and().csrf().disable();
    

    @Bean
    FilterRegistrationBean<SignatureRestSecurityFilter> disableAutoRegistration(SignatureRestSecurityFilter filter) 
        FilterRegistrationBean<SignatureRestSecurityFilter> registration = new FilterRegistrationBean<SignatureRestSecurityFilter>(filter);
        // set as false to avoid multiple filter calls
        registration.setEnabled(false);
        return registration;
    

    @Bean
    AuthenticationEntryPoint forbiddenEntryPoint() 
        return new HttpStatusEntryPoint(HttpStatus.FORBIDDEN);
    

    @Bean
    public FilterRegistrationBean<SignatureResponseFilter> loggingFilter() 
        FilterRegistrationBean<SignatureResponseFilter> registrationBean = new FilterRegistrationBean<SignatureResponseFilter>();

        registrationBean.setFilter(new SignatureResponseFilter());
        registrationBean.setOrder(1);
        registrationBean.addUrlPatterns(ProjectConstants.Path.Payment.PAYMENT + "/*");

        return registrationBean;
    

    @Bean
    SignatureRestSecurityFilter signatureRestSecurityFilter() throws Exception 
        SignatureRestSecurityFilter filter = new SignatureRestSecurityFilter(PAYMENT_PATH_WILD_CARDS);
        filter.setAuthenticationManager(authenticationManager());
        filter.setAuthenticationSuccessHandler(successHandler());
        return filter;
    

    @Bean
    SimpleUrlAuthenticationSuccessHandler successHandler() 
        SimpleUrlAuthenticationSuccessHandler successHandler = new SimpleUrlAuthenticationSuccessHandler();
        successHandler.setRedirectStrategy(new NoRedirectStrategy());
        return successHandler;
    


【讨论】:

以上是关于使用 Spring 的 AbstractAuthenticationProcessingFilter 成功认证后解决 404的主要内容,如果未能解决你的问题,请参考以下文章

Spring-使用JAVA的方式配置Spring-代理模式

8 -- 深入使用Spring -- 7...1 启动Spring 容器

Spring Framework,Spring Security - 可以在没有 Spring Framework 的情况下使用 Spring Security?

[Spring实战系列](15)使用Spring基于Java的配置

Spring 总览及 IOC 容器的使用 —— Spring 官方文档解读

Spring 框架介绍和使用