I am trying to test the access of one of my #RestController which is secured by a custom Spring Security configuration. My use case is the following: A HTTP GET to /someEndpoint is secured with authentification, but a HTTP POST request to the same endpoint is not secured. It's working fine when I boot application and test it with my frontend or Postman.
Now I am trying to write tests with MockMvc with the security configuration. I already made it through a lot of answers on StackOverflow, but nothing helped me.
My test setup looks like the following:
#RunWith(SpringRunner.class)
#WebMvcTest(controllers = MyController.class)
#WebAppConfiguration
#ContextConfiguration
public class AssessmentControllerTest {
private MockMvc mockMvc;
#Autowired
private WebApplicationContext webApplicationContext;
#Before
public void init() throws Exception {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.alwaysDo(print())
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
// some test methods
}
With this setup all my endpoints are secured and even a HTTP POST is returning a 401 instead of 201. I also enabled the debug log for security and in the debug logs it says that the test uses the default configure(HttpSecurity) and I can't find any of my AntMatchers in the logs:
2018-07-04 19:20:02.829 DEBUG 2237 --- [ main] s.s.c.a.w.c.WebSecurityConfigurerAdapter : Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).
2018-07-04 19:20:03.097 DEBUG 2237 --- [ main] edFilterInvocationSecurityMetadataSource : Adding web access control expression 'authenticated', for org.springframework.security.web.util.matcher.AnyRequestMatcher#1
2018-07-04 19:20:03.127 DEBUG 2237 --- [ main] o.s.s.w.a.i.FilterSecurityInterceptor : Validated configuration attributes
2018-07-04 19:20:03.130 DEBUG 2237 --- [ main] o.s.s.w.a.i.FilterSecurityInterceptor : Validated configuration attributes
2018-07-04 19:20:03.161 INFO 2237 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher#1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#5a75ec37, org.springframework.security.web.context.SecurityContextPersistenceFilter#3f736a16, org.springframework.security.web.header.HeaderWriterFilter#529c2a9a, org.springframework.security.web.csrf.CsrfFilter#7f93dd4e, org.springframework.security.web.authentication.logout.LogoutFilter#707b1a44, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#26c89563, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter#1e0a864d, org.springframework.security.web.authentication.www.BasicAuthenticationFilter#22ebccb9, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#53abfc07, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#4aa21f9d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#2c05ff9d, org.springframework.security.web.session.SessionManagementFilter#26bbe604, org.springframework.security.web.access.ExceptionTranslationFilter#4375b013, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#a96d56c]
2018-07-04 19:20:03.236 INFO 2237 --- [ main] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring FrameworkServlet ''
2018-07-04 19:20:03.237 INFO 2237 --- [ main] o.s.t.web.servlet.TestDispatcherServlet : FrameworkServlet '': initialization started
Is it in general possible to use my concrete Spring Security configuration during a MockMvc test or do I have to boot the whole Spring context during the test with #SpringBootTest ? I am using (Spring Boot 2.0.3.RELEASE with Java 1.8)
Thanks in advance!
With the spring-boot 2.x it is not possible to switch of security with a property anymore. You have to write an own SecurityConfiguration which has to be added to your test context. This security config should allow any request without authentication.
#Configuration
#EnableWebSecurity
public class TestSecurityConfiguration extends WebSecurityConfigurerAdapter{
#Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests().anyRequest().permitAll();
}
#Override
public void configure(WebSecurity web) throws Exception{
web.debug(true);
}
}
test class annotation:
#ContextConfiguration(classes = { ..., TestSecurityConfiguration.class })
public class MyTests {...
Related
I have post this question but with different format and got no answer and I think I didn't provide enough info.
What I am trying to do is parse a login request payload that contain a JSON with username and password. So, I had to extend UsernamePasswordAuthenticationFilter and replace the UsernamePasswordAuthenticationFilter in filter chain with my custom filter.
I've done that and registered my custom filter as you can see in the log from DefaultSecurityFilterChain:
2023-01-30T22:33:13.993+03:00 INFO 20436 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#1ec09a68, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#7da9b32c, org.springframework.security.web.context.SecurityContextHolderFilter#2b6ff016, org.springframework.security.web.header.HeaderWriterFilter#118c1faa, org.springframework.security.web.authentication.logout.LogoutFilter#4f5df012, ali.yousef.chatty.config.security.JsonUsernamePasswordAuthenticationFilter#7e5c8c80, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#7a491a60, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#4a0f4282, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#46df794e, org.springframework.security.web.access.ExceptionTranslationFilter#470866d1, org.springframework.security.web.access.intercept.AuthorizationFilter#1d0fc0bc]
Until now everything works fine.
Here is the code:
SecurityConfig.java
#Configuration
#EnableWebSecurity
public class SecurityConfig
{
#Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception {
JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter = new JsonUsernamePasswordAuthenticationFilter();
jsonUsernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager);
http
.csrf().disable()
.formLogin().disable()
.addFilterAfter(jsonUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests()
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll();
return http.build();
}
}
JsonUsernamePasswordAuthenticationFilter.java
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
Logger logger = LoggerFactory.getLogger(JsonUsernamePasswordAuthenticationFilter.class);
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UsernamePasswordDto usernamePasswordDto;
try {
usernamePasswordDto = new ObjectMapper().readValue(request.getInputStream(), UsernamePasswordDto.class);
logger.info(usernamePasswordDto.toString());
} catch (IOException ioe) {
throw new AuthenticationServiceException(ioe.getMessage(), ioe);
}
UsernamePasswordAuthenticationToken authToken =
UsernamePasswordAuthenticationToken.unauthenticated(usernamePasswordDto.getUsername(), usernamePasswordDto.getPassword());
Authentication result = this.getAuthenticationManager().authenticate(authToken);
for (GrantedAuthority a : result.getAuthorities()) {
logger.info(a.getAuthority());
}
return result;
}
}
TestController.java
#RestController
public class TestController
{
#GetMapping("/")
public String home() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null)
return "null";
return auth.getName();
}
}
I send a request to /login and I get anonymousUser response. I thought I missed something in the custom filter so I printed the authorities the user has and got ROLE_USER.
Here is the full output
2023-01-30T22:48:30.329+03:00 INFO 20900 --- [ main] ali.yousef.chatty.ChattyApplication : No active profile set, falling back to 1 default profile: "default"
2023-01-30T22:48:30.851+03:00 INFO 20900 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-01-30T22:48:30.903+03:00 INFO 20900 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 37 ms. Found 1 JPA repository interfaces.
2023-01-30T22:48:31.355+03:00 INFO 20900 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-01-30T22:48:31.361+03:00 INFO 20900 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-01-30T22:48:31.361+03:00 INFO 20900 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.5]
2023-01-30T22:48:31.431+03:00 INFO 20900 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-01-30T22:48:31.432+03:00 INFO 20900 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1064 ms
2023-01-30T22:48:31.559+03:00 INFO 20900 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-01-30T22:48:31.591+03:00 INFO 20900 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.1.6.Final
2023-01-30T22:48:31.696+03:00 WARN 20900 --- [ main] org.hibernate.orm.deprecation : HHH90000021: Encountered deprecated setting [javax.persistence.sharedCache.mode], use [jakarta.persistence.sharedCache.mode] instead
2023-01-30T22:48:31.781+03:00 INFO 20900 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-01-30T22:48:31.983+03:00 INFO 20900 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl#77ea806f
2023-01-30T22:48:31.984+03:00 INFO 20900 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2023-01-30T22:48:32.032+03:00 INFO 20900 --- [ main] SQL dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2023-01-30T22:48:32.847+03:00 INFO 20900 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-01-30T22:48:32.853+03:00 INFO 20900 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-01-30T22:48:33.070+03:00 WARN 20900 --- [ main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-01-30T22:48:33.220+03:00 INFO 20900 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2023-01-30T22:48:33.519+03:00 INFO 20900 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.session.DisableEncodeUrlFilter#5cd6a827, org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#6cd65042, org.springframework.security.web.context.SecurityContextHolderFilter#5c30decf, org.springframework.security.web.header.HeaderWriterFilter#5aefdb9e, org.springframework.security.web.authentication.logout.LogoutFilter#50d6af87, ali.yousef.chatty.config.security.JsonUsernamePasswordAuthenticationFilter#7674f9d4, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#4a0f4282, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#356ab368, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#4b5ceb5d, org.springframework.security.web.access.ExceptionTranslationFilter#99f75e4, org.springframework.security.web.access.intercept.AuthorizationFilter#7da9b32c]
2023-01-30T22:48:33.688+03:00 INFO 20900 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-01-30T22:48:33.689+03:00 INFO 20900 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Starting...
2023-01-30T22:48:33.690+03:00 INFO 20900 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry#5d3a238]]
2023-01-30T22:48:33.690+03:00 INFO 20900 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : Started.
2023-01-30T22:48:33.696+03:00 INFO 20900 --- [ main] ali.yousef.chatty.ChattyApplication : Started ChattyApplication in 3.673 seconds (process running for 3.952)
2023-01-30T22:48:52.209+03:00 INFO 20900 --- [nio-8080-exec-3] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-01-30T22:48:52.209+03:00 INFO 20900 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-01-30T22:48:52.209+03:00 INFO 20900 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
2023-01-30T22:48:52.250+03:00 INFO 20900 --- [nio-8080-exec-3] JsonUsernamePasswordAuthenticationFilter : UsernamePasswordDto(username=user01, password=password)
2023-01-30T22:48:52.421+03:00 INFO 20900 --- [nio-8080-exec-3] JsonUsernamePasswordAuthenticationFilter : ROLE_USER
What I think is after finishing the authentication process and sending a redirect to homepage the context is deleted somehow or cleared, but I'm not Spring Security expert.
I've done this in older versions with WebSecurityConfigurerAdapter without any problem but using this way of configuration didn't work.
I really tried everything I could and I would appreciate any help.
The SecurityContext is not saved by default after successful authentication. Moreover, the UsernamePasswordAuthenticationFilter is a form-login-based mechanism, so the Authentication object cannot be detected by the SessionManagementFilter, as the filter is not invoked during the authenticating request. So, you query the wrong Authentication object which in your case defaults to an Anonymous user.
So, first, after a successful authentication you have to save the authenticated Authentication object(returned from the attemptAuthentication() method) in the Security Context. You can do that by overriding the successfulAuthentication() method of your filter.
#Override
protected void successfulAuthentication(jakarta.servlet.http.HttpServletRequest request, jakarta.servlet.http.HttpServletResponse response, jakarta.servlet.FilterChain chain, Authentication authResult) throws IOException, jakarta.servlet.ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
//SecurityContextHolder.setContext(context);
HttpSessionSecurityContextRepository secRepo = new HttpSessionSecurityContextRepository();
secRepo.saveContext(context, request, response);
super.successfulAuthentication(request, response, chain, authResult);
}
You have to save it, in a persistent way, i.e. it has to be preserved during the whole session. A way to do so is via using an instance of the HttpSessionSecurityContextRepository class and using its saveContext() method to save it.
After that, in your handler method for the default “/” home(), you can obtain the saved SecurityContextHolder, again by using an instance of the HttpSessionSecurityContextRepository class and via its loadDeferredContext() method.
Note, that for this purpose, first, it is necessary to bring the request object (returned from your filter) in your handler home() method. (For completeness you can do the same for the response object as well).
After that, you can obtain the authenticated user from the extracted security context.
#GetMapping("/")
public String home(jakarta.servlet.http.HttpServletRequest request,
jakarta.servlet.http.HttpServletResponse response) {
//Authentication auth = SecurityContextHolder.getContext().getAuthentication();
HttpSessionSecurityContextRepository secRepo = new HttpSessionSecurityContextRepository();
Authentication auth = secRepo.loadDeferredContext(request).get().getAuthentication();
if (auth == null) return "null";
return (String) auth.getPrincipal();
}
One more thing that should be mentioned, is that if you want you can invalidate the session in your handler. This is a "must" for security reasons, especially if you use a STATELESS application e.g.a REST API application.
Hope this helped you!
I got the solution for my problem from Marcus when I posted an issue on the spring security repository on github because I thought it was a bug. If you want to check the answer yourself you can go here
The issue was that the my custom filter was using RequestAttributeSecurityContextRepository but the default was DelegatingSecurityContextRepository, hence made the authentication not getting saved between requests.
Setting the SecurityContextRepository in jsonUsernamePasswordAuthenticationFilter to DelegatingSecurityContextRepository will solve the issue.
jsonUsernamePasswordAuthenticationFilter.setSecurityContextRepository(new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(),
new HttpSessionSecurityContextRepository()
));
I've found a very strange behavior in Spring Boot when trying to serve static files with spaces (or any other special chars, like accents) in file names.
I'm using Spring Boot 2.6.1 with Spring Web MVC and the following customization:
#Configuration
public class MyConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/repo/**")
.addResourceLocations("file:///srv/intranet/repo/");
}
}
I have two files under /srv/intranet/repo, named foo.txt and foo bar.txt (note the space in the second file name).
When I start my application, I can access the first file at http://localhost:8080/repo/foo.txt. But I cannot access the second file (the one with the space in the file name) at http://localhost:8080/repo/foo%20bar.txt (I get a 404 error).
BUT if I put the file foo bar.txt under src/main/resources/static, then I can acces the file at http://localhost:8080/foo%20bar.txt.
I'm aware that Spring Boot configures several directories by default to serve static content (one of them being classpath:/static/), so I'm wondering: what is the difference between the preconfigured directories and the one I'm adding in my #Configuration class via addResourceHandler().addResourceLocations()? Am I missing some details when adding the new resourceHandler?
WORKAROUND
You can set the following property in your application.properties (or equivalent .yml) to get the old behavior in Spring Boot (pre-v2.6.0):
spring.mvc.pathmatch.matching-strategy=ant-path-matcher
UPDATE
I believe this is probably a bug in PathPattern, which replaces AntPathMatcher, and was introduced in Spring Framework 5.3 and adopted in Spring Boot 2.6.0. I submitted a bug report.
UPDATE (2022-06-04)
The bug has been fixed. The fix will be included in Spring Framework 5.3.21.
I have found a workaround for this issue.
Just add the following in your Spring Boot configuration file application.properties:
spring.mvc.pathmatch.matching-strategy=ant-path-matcher
The documentation for this property states that ant-path-matcher is the default value, but it is not. The source code shows that the default value is path-pattern-parser. I submitted an issue.
I have following configuration with Spring Boot 2.6.1 and it successfully loads file with a space in name.
#Configuration
#EnableWebMvc
#ComponentScan(basePackages = { "com.example.sw" })
public class WebConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/files/**").addResourceLocations("file:D:/img/");
}
}
I have an image with name "SO 1.png" (note the space). When hitting the application I get the image
You can probably troubleshoot by putting Spring web on TRACE level where it emits below information while serving the file.
logging.level.org.springframework.web.servlet=TRACE
Below are the logs
2021-12-07 14:24:24.544 TRACE 17200 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/files/SO%201.png", parameters={}, headers={masked} in DispatcherServlet 'dispatcherServlet'
2021-12-07 14:24:24.568 TRACE 17200 --- [nio-8080-exec-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to HandlerExecutionChain with [ResourceHttpRequestHandler [URL [file:D:/img/]]] and 3 interceptors
2021-12-07 14:24:24.580 TRACE 17200 --- [nio-8080-exec-1] o.s.w.s.r.ResourceHttpRequestHandler : Applying default cacheSeconds=-1
2021-12-07 14:24:24.626 TRACE 17200 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : No view rendering, null ModelAndView returned.
2021-12-07 14:24:24.626 DEBUG 17200 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK, headers={masked}
in Spring 5 a new PathPatternParser was introduced
try replacing
registry.addResourceHandler("/files/**")
with:
registry.addResourceHandler("/files/{*path}")
Spring boot 2.5.4 I used #PostConstruct for the very first time in my service class. As following:-
#Slf4j
#Service
#AllArgsConstructor
public class FileMonitorService {
private final AppProperties appProperties;
private final WatchService watchService;
private final RestTemplate restTemplate;
#PostConstruct
#Async
public void startMonitoring() {
FileUtils.setAppProperties(appProperties);
FileUtils.setRestTemplate(restTemplate);
FileUtils.readFilesForDirectory();
log.info("START_MONITORING");
try {
WatchKey key;
while ((key = watchService.take()) != null) {
for (WatchEvent<?> event : key.pollEvents()) {
log.info("Event kind: {}; File affected: {}", event.kind(), event.context());
if((event.kind() == StandardWatchEventKinds.ENTRY_CREATE ||
event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) &&
event.context().toString().contains(".xml")){
try {
restTemplateRequest(event.context().toString()+" processing");
FileUtils.readXml(Path.of(FileUtils.getFileAbsolutePath(appProperties.getDataIn()),
event.context().toString()));
}catch (Exception e){
log.error("startMonitoring Exception: "+e.getMessage());
}
}
}
key.reset();
}
} catch (InterruptedException e) {
log.warn("startMonitoring: interrupted exception for monitoring service: "+e.getMessage());
}
}
}
This method is called as soon as app launched. That is my requirements to process all file as soon as the app starts. I have controller as following:-
#RestController
#RequestMapping("/xml")
public class FileController {
#Autowired
FileMonitorService fileMonitorService;
#SneakyThrows
#GetMapping("/restart")
public String restartFileMonitoring(){
fileMonitorService.startMonitoring();
return "File monitoring restarted started successfully";
}
}
My app starts on port 8080 and no exception at all. But when I get call this end point localhost:8080/xml/restart
It is not reachable. If I comment out the #PostConstruct then I can call the end point. I am confused how to use this annotation properly. What is wrong in my code?
Update info:-
:: Spring Boot :: (v2.5.4)
2021-09-14 18:23:21.521 INFO 71192 --- [ main] c.f.i.task.BatchProcessorApplication : Starting BatchProcessorApplication using Java 14.0.2 on dev with PID 71192 (/home/dev/Desktop/batch-processor/batch-processor/target/classes started by dev in /home/dev/Desktop/batch-processor/batch-processor)
2021-09-14 18:23:21.523 INFO 71192 --- [ main] c.f.i.task.BatchProcessorApplication : No active profile set, falling back to default profiles: default
2021-09-14 18:23:22.485 INFO 71192 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2021-09-14 18:23:22.495 INFO 71192 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2021-09-14 18:23:22.495 INFO 71192 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.52]
2021-09-14 18:23:22.564 INFO 71192 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2021-09-14 18:23:22.564 INFO 71192 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 988 ms
File to monitor: /home/dev/Desktop/batch-processor/batch-processor/data/in
2021-09-14 18:23:22.647 INFO 71192 --- [ main] c.f.i.task.config.FileMonitorConfig : MONITORING_DIR: /home/dev/Desktop/batch-processor/batch-processor/data/in/
2021-09-14 18:23:22.667 INFO 71192 --- [ main] c.f.i.task.service.FileMonitorService : START_MONITORING
That is the log when I run the app. After debugging I found that while ((key = watchService.take()) != null) { call never returns until I copy some XML file as this app process xml files. Then I copy any xml file in the monitoring dir. I was expecting that #Async it will run in back ground thread in async mode. How to monitory this dir in background thread? So the caller of this method won't be blocked.
PostContstruct semantics
The PostConstruct annotation is part of JSR 330 (Dependency Injection) and is not a Spring custom annotation.
The annotation specification dictates that the annotated method MUST run before the service being injected into context or translated into a service.
Spring supports the PostConstruct lifecycle hook allowing to perform extra post-initialization actions once a bean has been initialized, i.e., it had all its dependencies injected.
Async semantics
The Async annotation on the other hand is a Spring specific annotation allowing to mark a method or a type as being a candidate for asynchronous execution.
Alternative
In a case where you are interested into starting a background process as long as you application starts, you should better use the application lifecycle events and more specifically the ApplicationReadyEvent to spin your monitoring activity:
#Slf4j
#Service
#AllArgsConstructor
public class FileMonitorService {
private final AppProperties appProperties;
private final WatchService watchService;
private final RestTemplate restTemplate;
#EventListener(ApplicationReadyEvent.class)
#Async
public void startMonitoring() {
// ...
}
}
And don't forget to add the #EnableAsync annotation on your Spring Boot configuration type to activate the asynchronous processing feature.
For your case, you don't need to use #PostConstruct and that is why its working when removing the #PostConstruct
to simplify, #PostConstruct is considered as a class empty constructor but it make sure all the Beans are loaded before being called
I'm playing around with Spring Boot and the reactive jdbc driver called r2dbc. In my main application I'm using Postgres as a database and now I want to the use h2 for the tests. And the Flyway migration is working with the setup but when the Spring application is able to insert records.
Here is my setup and code
#SpringBootTest
class CustomerRepositoryTest {
#Autowired
CustomerRepository repository;
#Test
void insertToDatabase() {
repository.saveAll(List.of(new Customer("Jack", "Bauer"),
new Customer("Chloe", "O'Brian"),
new Customer("Kim", "Bauer"),
new Customer("David", "Palmer"),
new Customer("Michelle", "Dessler")))
.blockLast(Duration.ofSeconds(10));
}
}
Here is the error that I'm getting
:: Spring Boot :: (v2.3.4.RELEASE)
2020-10-14 15:59:18.538 INFO 25279 --- [ main] i.g.i.repository.CustomerRepositoryTest : Starting CustomerRepositoryTest on imalik8088.fritz.box with PID 25279 (started by imalik in /Users/imalik/code/private/explore-java/spring-example)
2020-10-14 15:59:18.540 INFO 25279 --- [ main] i.g.i.repository.CustomerRepositoryTest : No active profile set, falling back to default profiles: default
2020-10-14 15:59:19.108 INFO 25279 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode.
2020-10-14 15:59:19.273 INFO 25279 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 160ms. Found 1 R2DBC repository interfaces.
2020-10-14 15:59:19.894 INFO 25279 --- [ main] o.f.c.internal.license.VersionPrinter : Flyway Community Edition 6.5.0 by Redgate
2020-10-14 15:59:20.052 INFO 25279 --- [ main] o.f.c.internal.database.DatabaseFactory : Database: jdbc:h2:mem:///DBNAME (H2 1.4)
2020-10-14 15:59:20.118 INFO 25279 --- [ main] o.f.core.internal.command.DbValidate : Successfully validated 1 migration (execution time 00:00.022s)
2020-10-14 15:59:20.131 INFO 25279 --- [ main] o.f.c.i.s.JdbcTableSchemaHistory : Creating Schema History table "PUBLIC"."flyway_schema_history" ...
2020-10-14 15:59:20.175 INFO 25279 --- [ main] o.f.core.internal.command.DbMigrate : Current version of schema "PUBLIC": << Empty Schema >>
2020-10-14 15:59:20.178 INFO 25279 --- [ main] o.f.core.internal.command.DbMigrate : Migrating schema "PUBLIC" to version 1.0.0 - schma
2020-10-14 15:59:20.204 INFO 25279 --- [ main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.036s)
2020-10-14 15:59:20.689 INFO 25279 --- [ main] i.g.i.repository.CustomerRepositoryTest : Started CustomerRepositoryTest in 2.466 seconds (JVM running for 3.326)
2020-10-14 15:59:21.115 DEBUG 25279 --- [ main] o.s.d.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [INSERT INTO customer (first_name, last_name) VALUES ($1, $2)]
org.springframework.data.r2dbc.BadSqlGrammarException: executeMany; bad SQL grammar [INSERT INTO customer (first_name, last_name) VALUES ($1, $2)]; nested exception is io.r2dbc.spi.R2dbcBadGrammarException: [42102] [42S02] Tabelle "CUSTOMER" nicht gefunden
Table "CUSTOMER" not found; SQL statement:
INSERT INTO customer (first_name, last_name) VALUES ($1, $2) [42102-200]
My src/test/resources/application.yaml is looking like this:
spring:
r2dbc:
url: r2dbc:h2:mem:///DBNAME?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
flyway:
url: jdbc:h2:mem:///DBNAME
baseline-on-migrate: true
user: sa
password:
Any ideas whats missing missing or whats wrong with the setup? If further information is needed please let me know.
Addition/Solution:
The url pattern is different between jdbc and r2dbc. The working solution for me is as follows:
url: r2dbc:h2:file:///./tmp/test-database
url: jdbc:h2:file:./tmp/test-database
And In order to setup Flyway you have to Configure Flyway:
// Flyway is not compatible with r2dbc yet, therefore this config class is created
#Configuration
public class FlywayConfig {
private final Environment env;
public FlywayConfig(final Environment env) {
this.env = env;
}
#Bean(initMethod = "migrate")
public Flyway flyway() {
return new Flyway(Flyway.configure()
.baselineOnMigrate(true)
.dataSource(
env.getRequiredProperty("spring.flyway.url"),
env.getRequiredProperty("spring.flyway.user"),
env.getRequiredProperty("spring.flyway.password"))
);
}
}
I've faced the same issue to setup and access to h2 database in memory for tests:
Liquibase for database migration using JDBC driver
Tests Reactive Crud Repository using R2DBC driver
Error encoutred:
org.springframework.data.r2dbc.BadSqlGrammarException: executeMany; bad SQL grammar [INSERT INTO MY_TABLE... Table "MY_TABLE" not found ...
Inspired by Chris's solution, i configured my src/testresources/application.properties file as follow:
spring.r2dbc.url=r2dbc:h2:mem:///~/db/testdb
spring.r2dbc.username=sa
spring.r2dbc.password=
spring.liquibase.url=jdbc:h2:mem:~/db/testdb;DB_CLOSE_DELAY=-1
spring.liquibase.user=sa
spring.liquibase.password=
spring.liquibase.enabled=true
I am currently having the same problem using r2dbc with liquibase. I am suspecting that the JDBC url points to a different database due to a slightly different syntax between R2DB and JDBC. I can manage to get h2 running from the file system though...
url: r2dbc:h2:file:///~/db/testdb
...
url: jdbc:h2:file:~/db/testdb
EDIT:
In non-reactive Spring Data I'd usually populate the Schema into the H2 memory database using a schema.sql/data.sql pair. This is also possible with R2DBC, but you have to configure the populator yourself.
It's also in the Getting Started R2DBC Tutorial. Basically you have to register a ConnectionFactoryInitializer bean.
#Bean
public ConnectionFactoryInitializer initializer(#Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
var initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
var populator = new CompositeDatabasePopulator();
populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema.sql")));
populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("data.sql")));
initializer.setDatabasePopulator(populator);
return initializer;
}
I was able to get it working.
First of all I created following test configuration class (because I want to execute tests only agains H2, on production mode I am using PostgreSQL):
#TestConfiguration
public class TestConfig {
#Bean
#Profile("test")
public ConnectionFactory connectionFactory() {
System.out.println(">>>>>>>>>> Using H2 in mem R2DBC connection factory");
return H2ConnectionFactory.inMemory("testdb");
}
#Bean(initMethod = "migrate")
#Profile("test")
public Flyway flyway() {
System.out.println("####### Using H2 in mem Flyway connection");
return new Flyway(Flyway.configure()
.baselineOnMigrate(true)
.dataSource(
"jdbc:h2:mem:testdb",
"sa",
"")
);
}
}
As you can see in the code above, both beans are scoped to the "test" profile only. As you can imagine I have pretty much the same beans in a regular ApplicationConfiguration class but annotated as a #Profile("default") and configured to use a PostgreSQL.
Second thing is that I created annotation which combines several other annotations to not repeat myself and to easily pickup beans declared in the TestConfig class:
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Inherited
#SpringBootTest
#ActiveProfiles("test")
#Import(TestConfig.class)
public #interface IntegrationTest {
}
Now the test itself:
#IntegrationTest
class CartsIntegrationTest {
// test methods here ....
}
I believe the main hint is to use H2ConnectionFactory.inMemory("testdb");
Flyway currently only supports the blocking JDBC APIs, and it is not compatible with the reactive r2dbc if possbile do not mix them in the same application.
Try to register a ConnectionFactoryInitializer to initiate the database schema and data as #Chris posted, my working example can be found here.
Try nkonev/r2dbc-migrate which is trying to migrate the flyway to the R2dbc world.
There were 2 issues I was experiencing in my project.
I needed to include the dependency:
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
<scope>test</scope>
</dependency>
I needed to change the value for spring.r2dbc.url to r2dbc:h2:mem:///test_db
With these changes, rd2bc worked with an in memory h2 database for testing. See also:
https://github.com/r2dbc/r2dbc-h2
A CODE SAMPLE TO HELP FIX THE PROBLEM: https://github.com/Suwappertjes/SpringSample
Problem
When trying to implement jwt-security in a Spring Boot application, I run into the following problem:
When I try to login with x-www-form-urlencoded through Postman I get a "Bad client credentials" error, whilst I know the credentials to be correct.
When I look in my log, I see that BCrypt gave a "Empty Encoded Password" warning. This is odd, considering I see correctly encrypted passwords in the database when I look at it through the MySQL interpreter.
Info
I am using Hibernate to build a MySQL database.
compile 'org.springframework.security.oauth:spring-security-oauth2:2.3.6.RELEASE'
compile 'org.springframework.security:spring-security-jwt:1.0.10.RELEASE'
compile 'org.springframework.boot:spring-boot-starter-data-jpa'
Java 1.8.0_212
What I tried
Before starting to implement security, the controllers, repositories, and MySQL database were all functioning correctly.
When I search for this problem online, some people suggest it has to do with the "loadUserByUsername" function. But when I debug that function I notice it is not being called at all.
I also tried allowing every single path in my program AND disabling crsf
in case it had something to do with access rights, but both didn't change anything. (http.requestMatchers.andMatcher("/**").permitAll().and().csrf().disable();)
Update: When putting NO users in the database, I still get the same error.
Some code:
The loadUserByUsername method:
#Override
public UserDetails loadUserByUsername(String userName) {
return userRepository.findByUsername(userName)
.map(user -> new User(
user.getUsername(),
user.getPassword(),
UserRoleAuthority.getAuthorities(user.getRoles())
))
.orElseThrow(() -> new UsernameNotFoundException("Unknown user: " + userName));
}
The authenticationprovider and passwordencoder:
#Bean
public DaoAuthenticationProvider getAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
#Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
The signing key:
#Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
JwtAccessTokenConverter result = new JwtAccessTokenConverter();
result.setSigningKey(signingKey);
return result;
}
Adding a new user:
userRepository.save(new User()
.withUsername("test")
.withPassword(passwordEncoder.encode("password"))
.withFirst("Admin")
.withLast("Nator")
.withEmail("test#notadmin.com")
.withRole(new HashSet<>(Arrays.asList(Role.Admin, Role.NotAdmin)))
);
And the Http configuration:
#Override
public void configure(HttpSecurity http) throws Exception {
http.requestMatchers()
.antMatchers("/iungo/*/**")
.and()
.authorizeRequests()
.antMatchers("/iungo/system/**")
.permitAll()
.antMatchers("/iungo/**");
}
#Override
public void configure(HttpSecurity http) throws Exception {
// We don't use this endpoint but we have to define it anyway in order to not enable web security on the whole application.
http
.userDetailsService(userDetailsService)
.requestMatchers()
.antMatchers("/oauth/authorize");
}
And finally, my console:
2019-06-12 10:29:58.460 INFO 25207 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {5.3.9.Final}
2019-06-12 10:29:58.461 INFO 25207 --- [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found
2019-06-12 10:29:58.532 INFO 25207 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-06-12 10:29:58.599 INFO 25207 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQL5Dialect
2019-06-12 10:29:59.092 INFO 25207 --- [ main] o.h.t.schema.internal.SchemaCreatorImpl : HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl#c30f26d'
2019-06-12 10:29:59.098 INFO 25207 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-06-12 10:29:59.382 WARN 25207 --- [ main] o.s.s.o.p.t.s.JwtAccessTokenConverter : Unable to create an RSA verifier from verifierKey (ignoreable if using MAC)
2019-06-12 10:29:59.595 INFO 25207 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/oauth/token'], Ant [pattern='/oauth/token_key'], Ant [pattern='/oauth/check_token']]], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#56a72887, org.springframework.security.web.context.SecurityContextPersistenceFilter#1ddba7a0, org.springframework.security.web.header.HeaderWriterFilter#7adbec34, org.springframework.security.web.authentication.logout.LogoutFilter#296bfddb, org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter#22ab1b8a, org.springframework.security.web.authentication.www.BasicAuthenticationFilter#54033a65, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#7dfec0bc, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#42734b71, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#3c8dea0b, org.springframework.security.web.session.SessionManagementFilter#6fe9c048, org.springframework.security.web.access.ExceptionTranslationFilter#526fc044, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#690e4b00]
2019-06-12 10:29:59.600 INFO 25207 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/iungo/*/**']]], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#1978b0d5, org.springframework.security.web.context.SecurityContextPersistenceFilter#69a3bf40, org.springframework.security.web.header.HeaderWriterFilter#3186f8f5, org.springframework.security.web.authentication.logout.LogoutFilter#a4dcede, org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationProcessingFilter#760c777d, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#2c731a16, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#2a341e3d, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#6556471b, org.springframework.security.web.session.SessionManagementFilter#467cd4b9, org.springframework.security.web.access.ExceptionTranslationFilter#3f3f554f, org.springframework.security.web.access.intercept.FilterSecurityInterceptor#d0e4972]
2019-06-12 10:29:59.603 INFO 25207 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: OrRequestMatcher [requestMatchers=[Ant [pattern='/oauth/authorize']]], [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter#6cf3b3d7, org.springframework.security.web.context.SecurityContextPersistenceFilter#462f8fe9, org.springframework.security.web.header.HeaderWriterFilter#24f2608b, org.springframework.security.web.csrf.CsrfFilter#713497cd, org.springframework.security.web.authentication.logout.LogoutFilter#56193e3a, org.springframework.security.web.savedrequest.RequestCacheAwareFilter#3c6fc4cd, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter#b2e1df3, org.springframework.security.web.authentication.AnonymousAuthenticationFilter#2e785b28, org.springframework.security.web.session.SessionManagementFilter#12a9e864, org.springframework.security.web.access.ExceptionTranslationFilter#4b762988]
2019-06-12 10:29:59.731 INFO 25207 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-06-12 10:29:59.902 INFO 25207 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-06-12 10:29:59.903 INFO 25207 --- [ main] nl.highway.iungomain.Application : Started Application in 2.937 seconds (JVM running for 3.345)
2019-06-12 10:29:59.923 INFO 25207 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
2019-06-12 10:30:12.550 INFO 25207 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-06-12 10:30:12.550 INFO 25207 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2019-06-12 10:30:12.554 INFO 25207 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2019-06-12 10:30:12.632 WARN 25207 --- [nio-8080-exec-1] o.s.s.c.bcrypt.BCryptPasswordEncoder : Empty encoded password
I found the problem. I was using Spring boot 2.1.4.RELEASE but this setup only works in 1.5.12.RELEASE. Of course downgrading is not very good practise so I will still try to get it to work with 2.1.4.
you have to add bCryptPasswordEncoder in the configureGlobal method
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder());
}
I was facing the exact same problem. In my case, the cause was my CustomUserDetails class constructor, which didn't set the password to the class, so it was always null.
Credits to this anwser:
why spring security gives empty password to password encoder?
I think #ghazouanbadr's solution is correct. Although, I implemented it differently in my practice project:
#EnableWebSecurity
#Configuration
public class WebSecurity extends WebSecurityConfigurerAdapter {
#Autowired
UserService userService;
#Autowired
BCryptPasswordEncoder bCryptPasswordEncoder;
public WebSecurity(UserService userService, BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userService = userService;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
#Override // Authorization
protected void configure(HttpSecurity http) throws Exception {
...
}
#Override // Authentication
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userService(userService).passwordEncoder(bCryptPasswordEncoder);
}
}
Be warned, I was only about a month into learning Java when I created this class. Don't remember all to much about security, so it's not my place to tell you that this is the best way to do it. Since I recall dealing with the same issue I figured it might guide you in the right direction.