Skip to content

Instantly share code, notes, and snippets.

@brunopk
Last active March 5, 2024 15:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save brunopk/37ef22a18c08ccda647e4cbecc972735 to your computer and use it in GitHub Desktop.
Save brunopk/37ef22a18c08ccda647e4cbecc972735 to your computer and use it in GitHub Desktop.
Spring

Ver contenido de bases H2 mientras se corren test de Spring

Para usar una base de datos H2 en memoria, y poder ver el contenido mientras se está corriendo un test, es decir hacer consultas mientras la ejecución de un test está bloqueada en un breakpoint, se debe tener la siguiente configuración en el archivo de properties:


spring:
  h2:
    console:
      enabled: true
      path: /h2-console

Una forma de acceder a una base de datos H2 es por cualquier browser a través de la URL : http://localhost:8080/h2-console (ver final de este archivo para obtener el puerto correcto).


Screen Shot 2022-06-06 at 13 41 55

Para poder visualizar bien el cliente web que dispone H2, también se debe editar las clases de configuración de Spring correcta. En este caso, se debe editar la clase (bean) de configuración que extienda de WebSecurityConfigurationLocal y exista en el ambiente (profile) de test que esta siendo utilizado:

package x;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * WebSecurity Configuration Local.
 */
@Configuration
@Profile({"local | integration_test"})
public class WebSecurityConfigurationLocal extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity.headers().frameOptions().disable();
    httpSecurity.csrf().disable().authorizeRequests().anyRequest().permitAll();
  }

}

Lo importante aquí es que se invoque frameOptions().

Finalmente, se debe agregar un loop infinito while(true) {} enseguida después de la parte de código que se quiera analizar, por ejemplo, después de hacer un INSERT en base, sobre el cual se quiera saber bien que y como es lo que se está insertando.

¿Como saber el puerto para visualizar el cliente H2?

El puerto es el mismo en el que el servidor (ejemplo Jetty) dispone la aplicación. Se puede encontrar en la salida del servidor, buscando la palabra port:

Screen Shot 2022-06-06 at 13 41 40

Responder con archivos

Endpoint que retorna un archivo (byte[]) a partir de una ruta absoluta (/Users/brpiaggio/Downloads/latest.tar.gz) :

@GetMapping(value = "/latest.tar.gz", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public @ResponseBody byte[] getFile() throws IOException {
  InputStream inputStream = Files.newInputStream(Paths.get("/Users/brpiaggio/Downloads/latest.tar.gz"));
  return inputStream.readAllBytes();
}

Scheduled tasks

package com.your.package;

import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class TestTasks {

  @Async
  @Scheduled(cron = "*/10 * * * * *")
  public void testTask() {
    String currentThread = Thread.currentThread().getName();
    System.out.println(String.format("Starting testTask on thread %s...", currentThread));

    long timeElapsed = 0;
    long currentTimeMillis = System.currentTimeMillis();
    long seconds = 0;
    long secondsAux = 0;

    while (timeElapsed < 8 * 1000) {
      if (secondsAux > seconds) {
        seconds = secondsAux;
        System.out.println(String.format("Seconds elapsed (thread %s): %s", currentThread, secondsAux));
      }
      timeElapsed = System.currentTimeMillis() - currentTimeMillis;
      secondsAux = timeElapsed / 1000;
    }

    while (8 * 1000 <= timeElapsed && timeElapsed < (8 * 1000) + 1000 * 1000) {
      if (secondsAux > seconds) {
        seconds = secondsAux;
        System.out.println(String.format("Seconds elapsed (thread %s): %s", currentThread, secondsAux));
      }
      timeElapsed = System.currentTimeMillis() - currentTimeMillis;
      secondsAux = timeElapsed / 1000;
    }

    System.out.println("Finalizing testTask...");
  }
}

Logging incomming requests

Using CommonsRequestLoggingFilter class

  1. Set the CommonsRequestLoggingFilter filter with a Spring bean in a configuration file (@Configuration annotated class):
    @Bean
    public FilterRegistrationBean<CommonsRequestLoggingFilter> loggingFilterRegistration() {
      CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
      filter.setIncludeQueryString(true);
      filter.setIncludePayload(true);
      filter.setBeforeMessagePrefix("PAYLOAD: ");
    
      FilterRegistrationBean<CommonsRequestLoggingFilter> registrationBean = new FilterRegistrationBean<>();
      registrationBean.setName("CommonsRequestLoggingFilter");
      registrationBean.setFilter(filter);
      registrationBean.setOrder(2);
      registrationBean.addUrlPatterns("*/bigqueue/consumer");
      return registrationBean;
    }
  2. Set DEBUG log level for CommonsRequestLoggingFilter in your properties:
    spring:
      logging:
        level:
          org:
            springframework:
              web:
                filter:
                  CommonsRequestLoggingFilter: 'INFO'
  3. Configure Spring to log all set with DEBUG, for example if you have a logback-spring.xml, log level is set with something like this :
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="false">
      <!-- use Spring default values -->
      <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    
      <springProfile name="local | integration_test">
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
          <encoder>
            <!-- @formatter:off -->
            <pattern>
                %clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(--){faint} %clr([%1.15t]){faint} %clr(%-1.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}
            </pattern>
            <!-- @formatter:on -->
            <charset>utf8</charset>
          </encoder>
        </appender>
        <root level="DEBUG">
          <appender-ref ref="STDOUT"/>
        </root>
      </springProfile>
      ...

Creating a custom filter

  1. Create the CachedHttpServletRequest class :

    package yourpackage;
    
    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import org.springframework.util.StreamUtils;
    
    public class CachedHttpServletRequest extends HttpServletRequestWrapper {
    
      private byte[] cachedPayload;
    
      public CachedHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedPayload = StreamUtils.copyToByteArray(requestInputStream);
      }
    
      @Override
      public ServletInputStream getInputStream() {
        return new CachedServletInputStream(this.cachedPayload);
      }
    
      @Override
      public BufferedReader getReader() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedPayload);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream));
      }
    
    }
  2. Create the CachedServletInputStream class :

    package yourpackage;
    
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class CachedServletInputStream extends ServletInputStream {
    
      private final static Logger LOGGER = LoggerFactory.getLogger(CachedServletInputStream.class);
    
      private InputStream cachedInputStream;
    
      public CachedServletInputStream(byte[] cachedBody) {
        this.cachedInputStream = new ByteArrayInputStream(cachedBody);
      }
    
      @Override
      public boolean isFinished() {
        try {
          return cachedInputStream.available() == 0;
        } catch (IOException exp) {
          LOGGER.error(exp.getMessage());
        }
        return false;
      }
    
      @Override
      public boolean isReady() {
        return true;
      }
    
      @Override
      public void setReadListener(ReadListener readListener) {
        throw new UnsupportedOperationException();
      }
    
      @Override
      public int read() throws IOException {
        return cachedInputStream.read();
      }
    }
  3. Create the RequestCachingFilter class:

    package com.mercadolibre.chronos_consistency_checker;
    
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.apache.commons.io.IOUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    public class RequestCachingFilter extends OncePerRequestFilter {
    
    private final static Logger LOGGER = LoggerFactory.getLogger(RequestCachingFilter.class);
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
          throws ServletException, IOException {
            CachedHttpServletRequest cachedHttpServletRequest = new CachedHttpServletRequest(request);
            LOGGER.info("REQUEST DATA: " + IOUtils.toString(cachedHttpServletRequest.getInputStream(), StandardCharsets.UTF_8));
            filterChain.doFilter(cachedHttpServletRequest, response);
        }
    }
  4. Set the custom filter with a Spring bean in a configuration file (@Configuration annotated class):

      @Bean
      public FilterRegistrationBean<RequestCachingFilter> loggingFilterRegistration() {
        RequestCachingFilter requestCachingFilter = new RequestCachingFilter();
    
        FilterRegistrationBean<RequestCachingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setName("CommonsRequestLoggingFilter");
        registrationBean.setFilter(requestCachingFilter);
        registrationBean.setOrder(2);
        registrationBean.addUrlPatterns("*/bigqueue/consumer");
        return registrationBean;
      }

Links

Authenticating endpoints

In order to authenticate endpoints in Spring 3, some configurations must be done which are different to deprecated security configurations in previous versions of Spring which indicates to extend the class WebSecurityConfigurerAdapter which is deprecated. The provided configurations below are suitable for custom security configurations. In general, these configurations are commonly used for basic authorization with a username and password, as well as other standard or common methods of authorization.

Steps

  1. Add required dependencies to pom.xml :

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  2. Create a class to implement the Authentication interface :

    package com.your.package;
    
    import jakarta.servlet.http.HttpServletRequest;
    import java.util.Collection;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    
    @Slf4j
    public class CustomAuthentication implements Authentication {
    
      /**
       * Constructor.
       */
      public CustomAuthentication() {
      }
    
      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
        throw new UnsupportedOperationException();
      }
    
      @Override
      public Object getCredentials() {
        throw new UnsupportedOperationException();
      }
    
      @Override
      public Object getDetails() {
        throw new UnsupportedOperationException();
      }
    
      @Override
      public Object getPrincipal() {
        return null;
      }
    
      @Override
      public boolean isAuthenticated() {
        // Implement custom authenticatin logic
      }
    
      @Override
      public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        throw new UnsupportedOperationException();
      }
    
      @Override
      public String getName() {
        throw new UnsupportedOperationException();
      }
    }

    For this simple case just implement isAuthenticated. Throwing exceptions in getPrincipal can generate issues on responses.

  3. Create a Spring filter using previously implemented authentication:

    package com.your.package;
    
    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    import javax.validation.constraints.NotNull;
    import org.springframework.lang.NonNullApi;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    /**
     * For each request, creates an instance of {@code CustomAuthentication} which implements {@code Authentication}
     * and makes it available for Spring Security by setting it on the {@code SecurityContext}.
     */
    public class AuthenticationFilter extends OncePerRequestFilter {
    
      public AuthenticationFilter() {
      }
    
      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
        // ...
        Authentication authentication = new CustomAuthentication();
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
      }
    }

    Important: don't forget to set the Authentication instance on the security context and invoke doFilter at the end in order to allow Spring to follow the correct filter chain

  4. Set the filter through a Spring bean in @Configuration annotated class:

    ...
    
    @Bean
    @Profile({"!integration_test & !local"})
    @Autowired
    public SecurityFilterChain furySecurityFilterChain(HttpSecurity http, SecurityService securityService) throws Exception {
      return http
        .csrf(CsrfConfigurer::disable)
        .addFilterAfter(new AuthenticationFilter(), LogoutFilter.class)
        .authorizeHttpRequests(request ->
          request.requestMatchers("topics/**").authenticated()
            .anyRequest().permitAll())
        .build();
    }
    
    ...

    Important:

    • Without csrf(CsrfConfigurer::disable) Spring will block all POST endpoints (see this Stack Overflow thread for more information).
    • Requests patterns used in requestMatchers method may contain **, not regular expresions.
    • The LogoutFilter.class argument tells Spring in which order to execute filters, which is mandatory.

Links

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment