spring-guides / tut-spring-security-and-angular-js

Spring Security and Angular:: A tutorial on how to use Spring Security with a single page application with various backend architectures, ranging from a simple single server to an API gateway with OAuth2 authentication.

Home Page:https://spring.io/guides/tutorials/spring-security-and-angular-js/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Natural routes with path variables in spring-boot modular project does not work

iwanzarembo opened this issue · comments

I followed the blog about Spring Security and Angular JS and there is a chapter about "natural" routes. Here a small quote:

the login page is specified as a route in hello.js as "/login" and this translates into "/#/login" in the actual URL (the one you see in the browser window). This is so that the JavaScript in the index.html, loaded via the root path "/", stays active on all routes.

Quote from Using "Natural" Routes

The example works great as long you are using simple routes like:

          $routeProvider.when('/', {  
            templateUrl : 'js/home/home.html',  
            controller : 'home'  
          }).when('/login', {  
            templateUrl : 'js/navigation/login.html',  
            controller : 'navigation'  
          }).otherwise('/');`  

In that you can call in your browser http://..../login and Spring (server side) will forward you to the correct page using the RequestMapping below:

    @RequestMapping(value = "/{[path:[^\\.]*}")
    public String redirect() {
      return "forward:/";
    }

Unfortunately this only works if you are not using any path variables. But I would like to use routes like:

          $routeProvider.when('/', {
            templateUrl : 'js/home/home.html',
            controller : 'home'
          }).when('/users', {
                templateUrl: 'js/usermanagement/list.html',
                controller: 'userList'
            }).when('/users/:id', {
                templateUrl: 'js/usermanagement/userDetails.html',
                controller: 'userDetails'
            }).when('/users/:id/edit', {
                  templateUrl: 'js/usermanagement/edit.html',
                  controller: 'userEdit'
            }).otherwise('/');

If I start my application and the route is used without server side redirect then I am able to navigate to http://..../users/some_id
but if I hit refresh, then I get the a 404.

I tried to fix it by adding another RequestMapping with a different value, but I could not manage it to work.

You can reproduce it by changing three files in the Modular Project, see Changes in Files via Patebin
Then do the following:

  1. start the spring-boot application and login as usual
  2. Click on the Show message via URL link
    Here you will see that the message content will show the value "link_msg" as shown in the URL.
  3. Hit refresh
    Now you will see the Whitelabel Error Page

I do not know how to fix this, but I hope you can.

You can boil this question down to "How do I redirect from /users/{id} and /users/{id}/edit to the home page". You can do that in a number of ways. Simplest would be to provide a @RequestMapping for those paths. Is that difficult? What did you try that didn't work?

Thanks for the pastebin. It doesn't contain any new request mappings in the back end, so there is no mapping for your new endpoint.

@dsyer You are right that it works if I add a new RequestMapping with the value /users/{id}
But I have a problem with that one, because I would need to add request mappings for every possible case. This is not that efficient and error prone (people tend to forget).
I am looking for a redirect which would always work in case I add an additional path to /roles or /user/{id}/someaction and so on.
My current workaround is another redirect like this:

   @RequestMapping(value = "/{[path:[^\\.]*}/{[id:[^\\.]*}")
    public String redirectWithParams(HttpServletRequest request) {
        // Also forward to home page so that route is preserved.
        return "forward:/";
    }

But I am not sure if that is the best approach from security and overview approach. It is never that great to open too many possible calls.

So another alternative would be to add another "redirect" controller for each module I would like to work with.
For example: The application has a controller for the API which would start with something like /api/users and then add for each API controller an additional redirect controller.

// the api controller
@RestController
@RequestMapping(value = "/api/users")
public class UserController {

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public UserBean getUser(@PathVariable("id") UUID id) {
        .....
    }
}

// the redirect controller
@Controller
@RequestMapping(value = "/users")
public class UserController {

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String userRedirect(HttpServletRequest request) {
         return "forward:/";
    }
}

Advantage:

  • You have something like a white list to allow only paths you really know and support
  • Good overview of the paths

Disadvantage:

  • More work because you have to maintain every path
  • More classes and with this bigger memory footprint
  • Not that efficient
  • Error prone (people tend to forget)

Right now I tend to use the workaround with the general redirect.

Feedback welcomed :)

Security is orthogonal to request mapping, so I think you can ignore any concern related to that. Note that I only used @RequestMapping in the sample because it's quick and easy to set up and easy to see why it works. You could just as easily use a Filter and write your own matcher.

You could also argue that the limitations of @RequestMapping values (no "/" in regexes) are also overly restrictive, and look for a solution via a change to Spring MVC. You an take that up in JIRA if you like (I'd be surprised if there wasn't already an issue covering it).

I am sorry if I start writing crap, but I think did not understand when I have to to the forward. I ended up in an endless loop with my filter.
You wrote in your tutorial that the RequestMappings will only be taken if no other filter were able to work with the request. How do you do that with a filter?

About the limitation of RequestMapping. I am ok with it, because then you can do different things for certain parts of the path and you do not need to take case of the slash.

How do you do that with a filter?

You can do anything with a filter. If you find that the request matches the ones you need to forward to index.html, you forward it, otherwise let the chain continue.

I'm not really following most of the rest of that last comment, sorry.

It took me a while, but I was able to write a working filter (only as an example):

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
             ....
             .addFilterAfter(new OncePerRequestFilter() {
                   // add the values you want to redirect for
                   private Pattern patt = Pattern.compile("/user/.*");

                   @Override
                   protected void doFilterInternal(HttpServletRequest request, 
                                                   HttpServletResponse response, 
                                                   FilterChain filterChain)
                                    throws ServletException, IOException {
                        if(this.patt.matcher(request.getRequestURI()).matches()) {
                            RequestDispatcher rd = request.getRequestDispatcher("/");
                            rd.forward(request, response);
                        } else {
                            filterChain.doFilter(request, response);
                        }
                 }
        }, FilterSecurityInterceptor.class)
        ....

Of course I can add more specific regex for other routes.

About my comment "limitation of RequestMapping"
I think the limitation is ok and useful.
e.g. if I have an URL like http://localhost:8080/user/id/whatever
Then this URL is already parsed and processed for the request mapping annotation (in this case something like RequestMapping("/{user}/{id}/{whatever}")). In that case the user does not really need slashes in the regex for each parameter.
If the developer needs more logic, then a filter (like the one above) can be used as an alternative.

Could you please add a statement in your blog about the possible navigation problems if you agree with the suggested filter alternative?

For me, the solution presented was "close" but not quite.

return "forward:/";

That will forward to a view / but what's mapped there? So I have a variant of the solution listed that I'm happier with:

    @RequestMapping(value = "{path:[^\\.]*}")
    public View redirect() {
        log.info("DEFAULT FORWARD");
        // Forward to home page so that route is preserved.
        return new InternalResourceView("/app/index.html");
    }

The idea is similar, but more specific. This works for me because I've mapped my static resources to /app. In angular I set then to wrap it up I set the default path this way:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter  {
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/app/**")
                .addResourceLocations("classpath:/static/");
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addRedirectViewController("/", "/app/index.html");
    }
    ...

With all those things in place:

  • If someone goes to / it redirects to /app/index.html
  • If someone goes to /app/some/route/x/y/z it directly (e.g. pasted URL, bookmark, link sharing) it still feeds them index.html
  • By mapping this to any non-dot paths under /app I don't interfere with any other spring @controller or @RestController paths ... whether they dots in them or not.

I'm just not convinced it has to be this complicated .................

@wz2b, does your solution work with parameters as well?
WebMvcConfigurerAdapter is now deprecated too, do you have a modified version for the current spring release?

The problem can be solved with a simple PathMatcher: {path:(?:(?!api|.).)*}/** where api is our base ref api route.
For more info about it : https://blog.impulsebyingeniance.io/software-technology/spring-boot-angular-comment-gerer-les-url-html-5/

I had trouble with everything posted so far because some calls from angular had the same path as some made in unit tests using restTemplate call. Finally, this is what worked for me:

public OncePerRequestFilter angularForwardFilter() {
    return new OncePerRequestFilter() {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String mode = request.getHeader("Sec-Fetch-Mode");
            RequestDispatcher rd = request.getRequestDispatcher("/");
            if (mode != null && mode.equals("navigate")) { // angular navigation
                rd.forward(request, response);
            } else {
                filterChain.doFilter(request, response);
            }
        }
    };
}