Is there a way to secure a forward URL?
To be clear, I've an error handler:
#Component
public class MyAuthenticationFailureHandler implements
AuthenticationFailureHandler {
#Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
if (exception.getClass().isAssignableFrom(
MyException.class)) {
MyException myException = (MyException) exception;
RequestDispatcher dispatcher = request.getRequestDispatcher("/signup/exception");
request.setAttribute("userID", myException.getUserID());
dispatcher.forward(request, response);
}
}
}
and a web controller:
#Controller
#RequestMapping("/signup")
public class SignupController {
#RequestMapping("/exception")
public ModelAndView signup() {
ModelAndView model = new ModelAndView(new InternalResourceView("/WEB-INF/jsp/signup.jsp", true));
return model;
}
}
I'd like that the route http://{hostname:port}/signup/exception will be accessible only as forward from my own handler, not directly (by writing URL and params on browser bar).
Add an attribute like
request.setAttribute("isForwarded","True")
in handler and check that attribute inside your controller.
If yes, go ahead else redirect to appropriate page.
I didn't test it, but I know that servlet containers refuse to answer to request relative to WEB-INF, but that you can forward to jsp servlets under WEB-INF. May be you could try to use an url like /WEB-INF/signup/exception ?
Alternatively, I think you're using Spring Security. I do not think that security filters are applied to forwarded requests. What gives the following intercept-url ?
<intercept-url pattern="/signup/exception" access="denyAll" />
Related
I'm building an OAuth2 Authorization server that supports Restful API with Spring Authorization Server and Spring Security.
I want a SPA application built by React to provide a login interface at /login and submit the login information to the /api/login path with a Post request.
I extend UsernamePasswordauthenticationFilter to support Restful-style Post requests to parse Json data from body:
public class RestfulUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public RestfulUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
private String jsonUsername;
private String jsonPassword;
#Override
protected String obtainPassword(HttpServletRequest request) {
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
return this.jsonPassword;
} else {
return super.obtainPassword(request);
}
}
#Override
protected String obtainUsername(HttpServletRequest request) {
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
return this.jsonUsername;
} else {
return super.obtainUsername(request);
}
}
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if ("application/json".equals(request.getHeader("Content-Type"))) {
try {
/*
* HttpServletRequest can be read only once
*/
//json transformation
Map<String, String> requestMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);
this.jsonUsername = requestMap.get("username");
this.jsonPassword = requestMap.get("password");
} catch (Exception e) {
throw new AuthenticationServiceException(e.getMessage(), e);
}
}
return super.attemptAuthentication(request, response);
}
}
In the configuration, I replaced the custom RestfulUsernamePasswordauthenticationFilter with UsernamePasswordauthenticationFilter, and used .loginProcessUrl to set the path for processing Post requests to /api/login:
#Override
protected void configure(HttpSecurity http)
throws Exception {
http.addFilterAt(new RestfulUsernamePasswordAuthenticationFilter(authenticationManagerBean()),
UsernamePasswordAuthenticationFilter.class);
http.authorizeRequests()
.antMatchers("/api/all")
.permitAll()
.antMatchers("/api/auth")
.hasRole("USER")
.antMatchers("/api/admin")
.hasRole("ADMIN")
.antMatchers("/login", "/register", "/api/login")
.permitAll();
http.formLogin()
.loginPage("/login")
.loginProcessingUrl("/api/login");
http.csrf().disable();
}
The problem is that although I set the path to process Post requests through .loginProcessingUrl, it doesn't seem to work.
When a Post request is submitted to /api/login, it will be redirected to/login like all unauthenticated requests, and the request submitted to /login will take effect normally.
In the process of debugging, I found that .loginProcessingUrl will register this path in a UsernamePasswordconfirationFilter, but will not be processed by my custom RestfulUsernamePasswordShareationFilter. In the process of debugging, I found that .loginProcessingUrl will register this path in a UsernamePasswordFilter.
I want to know if there is any way to make .loginProcessingUrl work on my custom AuthenticationFilter.
At the same time, can I easily customize the path to which they accept requests when I add more custom Filter?
Maybe I will add more AuthenticationProvider that need to read Restful information in the future. how should I design the architecture of these Filter and Provider to make it easier to expand?
I think I solved the problem after read this blog. As it said:
After you provided your custom filter, you can no longer use the Spring HttpSecurity builder. If you still use it, you’ll configure the default Filter, not yours!
We tell Spring to use our implementatin by the “addFIlterBefore” function.
After this little modification, the test APIs work the same way, the only difference is that you should provide ‘login’ and ‘password’ params in the POST request body (and not the query string).
So I have to manually set the AntPathRequestMatcher withsetRequiresAuthenticationRequestMatcher of the filter class like this:
RestfulUsernamePasswordAuthenticationFilter restfulFilter = new RestfulUsernamePasswordAuthenticationFilter(
this.authenticationManagerBean());
restfulFilter.setRequiresAuthenticationRequestMatcher(
new AntPathRequestMatcher("/api/login", "POST"));
http.addFilterBefore(restfulFilter,
UsernamePasswordAuthenticationFilter.class);
By this way, you can configure the path to handle the POST request just like you use loginProcessingUrl in the formal way.
You can also use same method to add more custom filter and authenticationProvider in the future, just configure them manually.
I've written a few extensions of ExceptionHandlerExceptionResolver, it intercepts all exceptions that it should, but instead of returning only error message and HTTP status code it makes really weird redirect by its own URL built upon users requested URL. For instance:
user's url -> .../myModule/api/myEntity/123 (it's an id)
resolver's redirect url -> .../myModule/api/myEntity/myEntity/123
Server doesn't have such resource and obviously it will respond with 404.
The question is: why it makes redirect and how to configure it to return only a message and status code?
My resolver:
public class BusinessLayerExceptionHandler extends ExceptionHandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
ModelAndView wrappedResponse = new ModelAndView();
wrappedResponse.addObject("errorMessage", ex.getMessage());
wrappedResponse.setStatus(HttpStatus.BAD_REQUEST);
return wrappedResponse;
}
}
I guess the usage of ModelAndView assumes redirection. At least that's a method description that I found in DispatcherServlet.
...
* #return a corresponding ModelAndView to forward to
* #throws Exception if no error ModelAndView found
*/
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
...
If so, how to make it return just error message and HTTP status code?
You can return just error message and HTTP status code by creating a custom View.
public class YourCustomView implements View {
private final String errorMessage;
public YourCustomView(String errorMessage) {
this.errorMessage = errorMessage;
}
#Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType("text/plain;charset=UTF-8");
try (PrintWriter pw = response.getWriter()) {
pw.write(errorMessage);
}
}
}
You need to put the custom View object into ModelAndView object in HandlerExceptionResolver#resolveException.
public class BusinessLayerExceptionHandler implements HandlerExceptionResolver {
#Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
ModelAndView wrappedResponse = new ModelAndView();
wrappedResponse.setStatus(HttpStatus.BAD_REQUEST);
wrappedResponse.setView(new YourCustomView(ex.getMessage()));
return wrappedResponse;
}
}
why it makes redirect
It seems that Spring recognize the view name as a defaultViewName and forwards to it (by calling RequestDispatcher#forward).
In DispatcherServlet#processHandlerException, a defaultViewName is set to the view name of a ModelAndView returned by resolveException when it doesn't have View object. A defaultViewName is got from DispatcherServlet#getDefaultViewName that translates a HTTP request into a view name.
Another Solution
I think you may be able to use #ControllerAdvice and #ExceptionHandler instead. It also can handle an exception thrown from a controller.
#ControllerAdvice
public class YourControllerAdvice {
#ExceptionHandler
public ResponseEntity<Map<String, String>> handleBusinessLayerException(
Exception exception) {
Map<String, String> body = Map.of("errorMessage", exception.getMessage());
return ResponseEntity.badRequest().body(body);
}
}
See Also
Spring Web MVC document about HandlerExceptionResolver
Spring Web MVC document about ControllerAdvice
How to make jersey and #webservlet working together ?
jersey ResourceConfig:
#ApplicationPath("/*")
public class ApplicationConfig extends ResourceConfig {
public ApplicationConfig() {
register(Greetings.class);
}
}
jersey Resource registered in resourceConfig:
#Path("/login")
public class Greetings {
#GET
public Response getHelloGreeting(#Context HttpServletRequest httpRequest) {
System.out.println("In the Greetings resource");
String url= "http://"+httpRequest.getServerName()+":"+httpRequest.getServerPort()+httpRequest.getContextPath();
String newURL = url+"/login.jsp";
System.out.println(newURL);
return Response.seeOther(URI.create(newURL)).build();
}
}
web servlet
#WebServlet(name = "LoginServlet", urlPatterns = { "/hello" })
public class LoginServlet extends HttpServlet {
#Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doPost(request, response);
}
#Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ServletContext servletContext = getServletContext();
System.out.println("inside login servlet");
request.getRequestDispatcher("/login.jsp").forward(request, response);
System.out.println("request forwarded");
}
//other functions not important so deleted
}
Case 1: on accessing this
http://localhost:8088/ResponseFilterweb/login
console logs:
In the Greetings resource
http://localhost:8088/ResponseFilterweb/login.jsp (no ui comes)
on accessing this
http://localhost:8088/ResponseFilterweb/hello
(nothing happens 404 error)
Case 2: Changing application config resource path:
#ApplicationPath("/auth")
public class ApplicationConfig extends ResourceConfig {
public ApplicationConfig() {
register(Greetings.class);
}
}
on accessing this
http://localhost:8088/ResponseFilterweb/auth/login
In the Greetings resource
http://localhost:8088/ResponseFilterweb/login.jsp (Ui comes)
on accessing this
http://localhost:8088/ResponseFilterweb/hello
inside login servlet (Ui comes)
userid is
Encoded string
request forwarded
doubts:
don't know why login.jsp is blocked in the first case:
why http://localhost:8088/ResponseFilterweb/login not showing any ui .. i think it should come ?
why http://localhost:8088/ResponseFilterweb/hello not showing any ui ?
If you were using web.xml, this or this would be your solution, but since you're not, this might be your only option. The problem is that when you use /* as the servlet mapping for Jersey, it hogs up all the requests. So the request to /hello would go to Jersey and not the LoginServlet. What those solutions I linked to do is cause Jersey to forward the request if it can't find it within the Jersey application. The other solution is to just change the servlet mapping to something like /api/* (which is pretty common), then you would just prefix your API requests with /api.
Using asterisk (*) won't work using #ApplicationPath
If you use /*, then you're making it too greedy and saying it should match everything all the time, and the default servlet will never be invoked
Use #ApplicationPath("/") instead
If you use /, then you're replacing the container's default servlet
I want to hide the Apache Camel Servlet behind Spring MVC Controller entry point, because I have some proprietary components I have to use that depend on Sping Boot and Spring MVC Controller.
I have the following Apache Camel Route, which is working fine
<route id="Route">
<from uri="servlet:messages?httpMethodRestrict=POST"/>
<process ref="..."></process>
<to uri="{{storage.service.endpoint}}?bridgeEndpoint=true"/>
</route>
I have registered the Camel Servlet without any URL mappings, because I don't want to be accessible directly. The Spring Bean:
#Bean
ServletRegistrationBean servletRegistrationBean() {
CamelHttpTransportServlet camelServlet = new CamelHttpTransportServlet();
ServletRegistrationBean servletBean = new ServletRegistrationBean(camelServlet, false, new String[]{});
servletBean.setName("CamelServlet");
return servletBean;
}
In Spring Controller Entry point I just need to forward to Camel Servlet:
#RequestMapping(method=RequestMethod.POST, value="/api/v1/*")
public void wrapper(HttpServletRequest request, HttpServletResponse response) throws Exception{
context.getNamedDispatcher("CamelServlet").forward(request, response);
}
The problem is that Camel Servlet relies on ServletResolveConsumerStrategy using request.getPathInfo(), which is always null in Spring Controller entry point. I have tried different paths in #RequestMapping, but always request.getServletPath() has the full path and the pathInfo is null
P.S. The application is running on Tomcat 8.
Modify the Spring Controller entry point using path variables and set the value of the path variable a pathInfo() in a custom HttpServletRequestWrapper:
#RequestMapping(path="/api/v1/{messages}" , method=RequestMethod.POST)
public void wrapper2(HttpServletRequest request, HttpServletResponse response, #PathVariable String messages) throws Exception{
request = new PathInfoRequestWrapper(request, messages);
context.getNamedDispatcher("CamelServlet").forward(request, response);
}
class PathInfoRequestWrapper extends HttpServletRequestWrapper{
private String pathInfo;
public PathInfoRequestWrapper(HttpServletRequest request, String pathInfo) {
super(request);
this.pathInfo = pathInfo;
}
#Override
public String getPathInfo(){
String origPathInfo = super.getPathInfo();
if(origPathInfo == null || origPathInfo.equals("")){
return this.pathInfo;
}else{
return origPathInfo;
}
}
}
One solution is to create custom ServletResolveConsumerStrategy, which relies only on request.getServletPath(), to find the appropriate route. Unfortunately only setting the Strategy with org.apache.camel.http.common.CamelServlet.setServletResolveConsumerStrategy(ServletResolveConsumerStrategy) does not work, because the it is overwritten in org.apache.camel.component.servlet.CamelHttpTransportServlet.init method. SO you need to override the init in the Camel Servlet as well:
CamelHttpTransportServlet camelServlet = new CamelHttpTransportServlet(){
#Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
ServletResolveConsumerStrategy servletResolveConsumerStrategy = new CamelSpringResolveConsumerStrategy();
setServletResolveConsumerStrategy(servletResolveConsumerStrategy );
}
};
I have a filter which processes requests in order to log them, so I can keep track of which session hit a page at what time with what request parameters. works great... posting from jsp to jsp, or making a direct call to a jsp. When a form is posted to a servlet which forwards that request to a new jsp, however, I am unable to see which jsp the request was forwarded to.
For example, suppose I have a login page, which posts to a LoginServlet, which then forwards the request to either index.jsp or index1.jsp. How can I determine from the request whether LoginServlet is returning index.jsp or index1.jsp?
This is in a java 1.5 environment using the 2.3 servlet specification.
public class PageLogFilter implements Filter {
FilterConfig filterConfig = null;
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
public void destroy() {
this.filterConfig = null;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
if (request instanceof HttpServletRequest) {
HttpServletRequest req = (HttpServletRequest) request;
HttpSession session = req.getSession(false);
//For non-forwards, I can call req.getRequestURI() to determine which
//page was returned. For forwards, it returns me the URI of the
//servlet which processed the post. I'd like to also get the URI
//of the jsp to which the request was forwarded by the servlet, for
//example "index.jsp" or "index1.jsp"
}
} catch (Exception e {
System.out.println("-- ERROR IN PageLogFilter: " + e.getLocalizedMessage());
}
chain.doFilter(request, response);
}
}
If you are OK with performing an additional check then you can use attribute to set/get original request URI.
In your LoginServlet set the attribute:
//LoginServlet
public void doFilter(...) {
HttpServletRequest oReq = (HttpServletRequest)request;
..
...
//save original request URI
oReq.setAttribute("originalUri", oReq.getRequestURI());
}
and in your PageLogFilter check if originalUri attribute has value then consider this value as the request URI
//PageLogFilter
public void doFilter(...) {
HttpServletRequest req = (HttpServletRequest) request;
if(req.getAttribute("originalUri") != null) {
String strOriginalUri = (String) req.getAttribute("originalUri");
//set it back to null
req.setAttribute("originalUri", null);
}
}
Although it wont help solve your immediate problem, Servlet 2.4 added further detailed control on the dispatcher, which is what you want.
With it, you can configure the filter to add the following dispatcher element, which would cause the filter to also apply to forwards.
<filter-mapping>
<filter-name>PageLogFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
The following article explains it in more detail
http://www.ibm.com/developerworks/java/library/j-tomcat2/