Spring boot and Jersey, can't make ExceptionMapper work - java

I am using Spring boot + Spring Security + Jersey.
I am trying to do something always that an Unathorized error happens with an ExceptionMapper, but it doesn't seem to work. However, other Mappers that I've done work perfectly.
This is my code:
Unauthorized Excepcion:
package com.ulises.usersserver.rest.exceptionsmappers;
import com.ulises.usersserver.rest.dto.ErrorDTO;
import com.ulises.usersserver.rest.dto.ErrorDTOBuilder;
import javax.ws.rs.NotAuthorizedException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import static com.ulises.usersserver.constants.Constants.REQUEST_ERROR_UNATHORIZED;
public class NotAuthorizedMapper implements ExceptionMapper<NotAuthorizedException> {
#Override
public Response toResponse(NotAuthorizedException e) {
final ErrorDTO errorDTO = ErrorDTOBuilder.builder()
.message(REQUEST_ERROR_UNATHORIZED)
.build();
return Response.status(Response.Status.UNAUTHORIZED)
.type(MediaType.APPLICATION_JSON_TYPE)
.entity(errorDTO)
.build();
}
}
Other custom mapper that works perfectly:
package com.ulises.usersserver.rest.exceptionsmappers;
import com.ulises.usersserver.rest.dto.ErrorDTO;
import com.ulises.usersserver.rest.dto.ErrorDTOBuilder;
import com.ulises.usersserver.services.exceptions.UserNotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import static com.ulises.usersserver.constants.Constants.REQUEST_ERROR_USER_DOESNT_EXIST;
public class UserNotFoundExceptionMapper implements ExceptionMapper<UserNotFoundException> {
#Override
public Response toResponse(UserNotFoundException e) {
final ErrorDTO errorDTO = ErrorDTOBuilder.builder()
.message(REQUEST_ERROR_USER_DOESNT_EXIST)
.build();
return Response.status(Response.Status.NOT_FOUND).entity(errorDTO).build();
}
}
They are of course registered in Jersey's config:
#Bean
public ResourceConfig jerseyConfig() {
final ResourceConfig resourceConfig = new ResourceConfig();
...
resourceConfig.register(NotFoundMapper.class);
resourceConfig.register(NotAuthorizedMapper.class);
...
return resourceConfig;
}
I don't seem to be able to map other exceptions such as InternalServerErrorException (I managed to map this one by doing
ExceptionMapper<Exception>, which doesn't look very correct to me.
Anyone knows why is this happening? I've checked all possible questions here and none of them solved this.
Thanks in advance.

Okay, it seems Jersey has no control over Spring Security's exceptions.
The way to solve this (had to dig alot) is to override the method from AuthenticationEntryPoint that S-Security uses to return the 401 response if an user isn't authorized.
So I created the following class implementing that interface:
public class CustomEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest request,
HttpServletResponse response, AuthenticationException authException) throws IOException {
JSONObject json = new JSONObject();
json.put("Message", "You don't have access to view this section. Please, log in with an authorized account.");
response.addHeader("Content-Type", "application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().println(json);
}
}
And then just add this configuration to S-Security to use this class as entry point:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().authenticationEntryPoint(new CustomEntryPoint());
..... any other config you had .....
}

Related

Spring Boot Thymeleaf download file after successful login

I want to provide a webservice with spring boot, which is able to download a dump from a MySQL database.
But the download shouldn´t be able for everybody. So I need a kind of login. It´s not quite sure where the credentials will be stored, there is the possibility, that they´re just hard coded in the application itself.
I´m kind of lost and don´t know how to implement it.
Here is what i got so far:
#Controller
public class EBMysqldumpController {
private EBMysqldumpService mysqldumpService;
#Autowired
public EBMysqldumpController(EBMysqldumpService mysqldumpService) {
this.mysqldumpService = mysqldumpService;
}
#GetMapping("/login")
public String showLoginForm(Model model) {
model.addAttribute("userDto", new UserDto());
return "mysqldump-login";
}
#PostMapping(path = "/login")
public String validateLoginForm(#Valid UserDto userDto, BindingResult result, HttpServletRequest request) {
if (result.hasErrors()) {
return "mysqldump-login";
}
if (!this.mysqldumpService.checkLogin(userDto)) {
result.addError(new ObjectError("", "Wrong username and/or password"));
return "mysqldump-login";
}
return "redirect:/file";
}
#PostMapping(path = "/file")
public ResponseEntity<Resource> startMysqlDump(#Valid UserDto userDto, Model model) throws IOException, InterruptedException, MysqldumpException {
if (!this.mysqldumpService.checkLogin(userDto)) {
return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
}
File mysqlDump = this.mysqldumpService.getMysqlDumpFile();
ByteArrayResource byteArrayResource = this.mysqldumpService.getByteArrayResourceFromFile(mysqlDump);
HttpHeaders headers = getMysqldumpHeaders(mysqlDump.getName());
ResponseEntity<Resource> body = ResponseEntity.ok()
.headers(headers)
.contentLength(mysqlDump.length())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(byteArrayResource);
this.mysqldumpService.deleteFile(mysqlDump);
return body;
}
private HttpHeaders getMysqldumpHeaders(String filename) {
ContentDisposition contentDisposition = ContentDisposition.inline().filename(filename).build();
HttpHeaders headers = new HttpHeaders();
headers.setContentDisposition(contentDisposition);
return headers;
}
}
For now the controller shows a login page at "/login". After a form is submitted, the controller checks if the credentials are correct, if not it shows again the login page with the error messages.
But my problem is that I don´t what to do after the login was successful. The download is working when I call it directly, but with redirect it´s not working, because I made a post request to make sure just logged in user can download the file. If I make a get request, everybody can use the link.
I have the feeling, I´m handling the problem wrong. What would you recommend? How could I start the download, after the login was successful?
I would recommend to add Spring Security to your project and have a single hardcoded user in there:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Autowired
private PasswordEncoder passwordEncoder;
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)
.withUser("user1").password("my-secret-pwd").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(
registry -> registry.mvcMatchers("/**").authenticated()
)
.formLogin();
}
#Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}

Spring Boot - How to track all requests and responses with exceptions and save into database?

I have two micro services(Spring Boot) for integration between two Product.My architecture is like below.
Product1 <-------> microservice1/microservice2 <------> Product1.
My task is I need to track requests with input prams (with methods, eg. GET, POST, etc), request path, query string, corresponding class method of this request, also response of this action, both success and errors and save into database table.
I have tried spring boot actuator. But no luck.
Please suggest how to achieve that.
I suggest using a HandlerInterceptor that you can register to the two micro-services REST controllers. It has a nice lifecycle call-backs that you can use to serialize the data and save it into the database. Here's the custom interceptor class:
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
#Component
public class DBRequestInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// DB logic
return super.preHandle(request, response, handler);
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
// DB logic
super.afterCompletion(request, response, handler, ex);
}
}
Once you had developed this re-usable handler, you can register in the web configurer. Here's the code:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#Configuration
public class WebConfiguration implements WebMvcConfigurer {
#Autowired
private DBRequestInterceptor interceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**/");
}
}
This code registers the interceptor on all of your URL mappings. You can customize it to some specific services URLs.
You might find an exception in the original code once you read the request stream in the above interceptor. In that case, you can leverage the Spring wrapper classes ContentCachingRequestWrapper
Hope that helps.

Spring 4.3.6 STOMP / WebSockets with auth-token

SOLVED: see comment
I am currently trying to open a WebSockets connection between a spring server and an angular client with a x-auth-token. Therefore I have the following pieces of code:
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
LOGGER.info("registering websockets");
registry.addEndpoint("/api/v1/websocket").setAllowedOrigins("*").withSockJS();
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exceptio) {
LOGGER.info("ERRORORRRRRRRR");
exception.printStackTrace();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
LOGGER.info("in override");
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String authToken = accessor.getFirstNativeHeader("x-auth-token");
LOGGER.info("Header auth token: " + authToken);
// Principal user = ... ; // access authentication header(s)
//accessor.setUser(user);
}
return message;
}
});
}
}
As you can see from my WebSocketConfig I am currently not really processing the x-auth-token, but rather trying to log it once. In the end I was trying to follow the Spring documentation: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-authentication-token-based
The only problem I have with this is that in configureClientInboundChannel() the preSend() method seems not get called - and I cannot figure out why.
On the client side, the following error is displayed:
As you can see by comparing the error log and the endpoint defined in registerStompEndpoints() shows the actual endpoint is the correct one. Still I do not understand, why LOGGER.info("in override"); in configureClientInboundChannel() is not called.
I would really like to go with the auth-token being transferred via the stomp headers instead of the hacky token-as-URL-paramter-solution. Does someone here have an idea? Did I miss something in the already linked spring documentation?
Thanks in advance for your help!

Disable HTTP OPTIONS method in spring boot application

I had developed rest API on spring boot application. The APIs accept only GET , and POST , but on requesting using OPTIONS method , API responding 200 status (instead of 405). I googled this issue , but none of the solution was springboot based .
Response:
Allow: OPTIONS, TRACE, GET, HEAD, POST
Public: OPTIONS, TRACE, GET, HEAD, POST
Need to disable OPTIONS method.
Previous answer is for tomcat only, so adding mine as well. You can disable the method cross-container by, for example, using a standard servlet filter:
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
#Component
public class MethodFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getMethod().equals("OPTIONS")) {
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
} else {
filterChain.doFilter(request, response);
}
}
}
Note: it is assumed that this class is componentscanned by Spring. If not, you can use other registration methods as detailed in here.
Try this; in allowedMethods you can filter methods which are required:
#Configuration
public class CorsConfiguration {
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(origins u want to allow)
.allowCredentials(false).allowedMethods("POST", "GET", "PUT");
}
};
}
}
I tried this and it worked.
#Bean
public EmbeddedServletContainerCustomizer containerCustomizer() {
return new EmbeddedServletContainerCustomizer() {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
if (container.getClass().isAssignableFrom(TomcatEmbeddedServletContainerFactory.class)) {
TomcatEmbeddedServletContainerFactory tomcatContainer = (TomcatEmbeddedServletContainerFactory) container;
tomcatContainer.addContextCustomizers(new ContextSecurityCustomizer());
}
}
};
}
private static class ContextSecurityCustomizer implements TomcatContextCustomizer {
#Override
public void customize(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
SecurityCollection securityCollection = new SecurityCollection();
securityCollection.setName("restricted_methods");
securityCollection.addPattern("/*");
securityCollection.addMethod(HttpMethod.OPTIONS.toString());
constraint.addCollection(securityCollection);
constraint.setAuthConstraint(true);
context.addConstraint(constraint);
}
}
If you are using spring security, you can use the method below:
#Bean
public HttpFirewall configureFirewall() {
StrictHttpFirewall strictHttpFirewall = new StrictHttpFirewall();
strictHttpFirewall.setAllowedHttpMethods(Arrays.asList("GET","POST","OPTIONS"));
return strictHttpFirewall;
}

Custom Spring Security using annotation: REST api to pass in username password instead of login dialog box

I am trying to secure my REST based application using Spring Security using no XML. This is what I have so far -
-- AppConfig.java
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
#Configuration
#ComponentScan("com.trans.*")
#EnableWebMvc
#Import({SecurityConfig.class})
public class AppConfig {
}
-- WebAppInitializer.java --
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
#Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { AppConfig.class };
}
#Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
#Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
-- SecurityConfig.java --
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("john").password("123456").roles("USER");
auth.inMemoryAuthentication().withUser("admin").password("123456").roles("ADMIN");
auth.inMemoryAuthentication().withUser("dba").password("123456").roles("DBA");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/user/**").access("hasRole('ROLE_ADMIN')")
.and().formLogin()
.and().logout().logoutUrl("/j_spring_security_logout")
.and().httpBasic();
}
}
-- SpringSecurityInitializer.java --
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SpringSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
}
After implementing this much I got the basic Spring functionality working -
After going into a browser if I enter htttp://localhost:8080/Spring4/user/1 browser popsup a login dialog to enter username and password.
I can logout by htttp://localhost:8080/Spring4/logout
What I am trying to do is -
Instead of the dialog I can pass in username and password as json.
should return a token so that on the next api calls I don't have to pass in username and password.
WHat should be the apporach to achieve this?
--Thanks.
You have to use Spring security OAuth 2.0 to secure rest services.
http://projects.spring.io/spring-security-oauth/
1) This dialog appears as a result bad login attempt. To avoid showing pop-up at client side, you should create your own BasicAuthenticationEntryPoint implementation like this:
#Service
public class YourBasicAuthenticationEntryPoint extends BasicAuthenticationEntryPoint {
#Override
public void commence(final HttpServletRequest request, final HttpServletResponse response,
final AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}
This will avoid putting WWW-Authenticate header in your HTTP response, which causes the pop-up.
Providing username/password in json formatted body needs object mapping which is not performed in authentication phase yet. As you already supported HTTP Basic authentication in your SecurityConfig class, passing Base64 encoded username/password in HTTP request's Authentication header will cover your requirement.
2) By default, Spring security creates stateful sessions, thus you do not need an extra implementation. However, if you need to force client to authenticate for every request in future, you can switch to stateless policy as below:
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

Categories

Resources