简体   繁体   中英

Spring Security login with Java and Angular2

I am having some trouble integrating spring security (namely the login part) in my web application. My BackEnd is running on localhost:8080 and the FrontEnd (Ionic 2 with AngularJS 2) is running on localhost:8100 . I managed to log in (at least I think so) but the rest of the requests become unavailable and I get the following error in the Chrome Developer Console:

XMLHttpRequest cannot load http://localhost:8080/company/getAll. Response for preflight is invalid (redirect)

When testing with Postman it seems to be working, I log in and then I am able to perform POST requests on http://localhost:8080/company/getAll and everything works as intended.

Presumably I'm missing something (like a token) but can't figure out what. I am new to both Ionic and Spring Security so please forgive me if this is something trivial. I tried googling various tutorials but was unable to find anything (most of them were using JSP).

How could I get this to work? How should my requests from the frontend look like?

Here is my login method from the BackEnd controller:

@RequestMapping(value = "/login", method = RequestMethod.GET)
    @CrossOrigin(origins = "*")
    public ResponseEntity<GeneralResponse> login(Model model, String error, String logout) {
        System.out.println("LOGIN"+model.toString());

        if (error != null) {
            System.out.println("1.IF");
            model.addAttribute("error", "Your username and password is invalid.");
        }

        if (logout != null) {
            System.out.println("2.IF");
            model.addAttribute("message", "You have been logged out successfully.");
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new GeneralResponse(true, "FAILURE: Error"));
    }

This is my WebSecurityConfig:

@Configuration
@EnableWebSecurity
@ComponentScan("com.SAB.service")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/resources/**", "/registration").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/user/login")
            .permitAll()
            .and()
            .logout()
            .permitAll();
        http
            .csrf().disable();
        http.formLogin().defaultSuccessUrl("http://localhost:8100/", true);
        //.and().exceptionHandling().accessDeniedPage("/403")
        //.and().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

And finally my FrontEnd code responsible for logging in:

  login(){
    var body = 'username='+this.loginForm.controls['username'].value+'&password='+this.loginForm.controls['password'].value;
    var headers = new Headers();
    headers.append('Content-Type', 'application/x-www-form-urlencoded');
    //headers.append("Access-Control-Allow-Origin", "*");

    this.http
      .post('http://localhost:8080/user/login',
        body, {
          headers: headers
        })
      .subscribe(data => {
        console.log('ok');
        console.log(data)
      }, error => {
        console.log(JSON.stringify(error.json()));
      });
  }

If you require any additional details please let me know and I will provide them.

Thanks in advance!

I don't get the Access-COntroll-Origin error but I tried to change it according to the comments anyway and now I get a "Invalid CORS request"

Request and Response Header for login from postman: 在此输入图像描述

EDIT: Here are the Spring Security logs. However Spring only registers a OPTIONS request and no POST even though the FrontEnd is calling POST.

************************************************************

Request received for OPTIONS '/login':

org.apache.catalina.connector.RequestFacade@18859793

servletPath:/login
pathInfo:null
headers: 
host: localhost:8080
connection: keep-alive
access-control-request-method: POST
origin: http://localhost:8100
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36
access-control-request-headers: access-control-allow-origin
accept: */*
referer: http://localhost:8100/
accept-encoding: gzip, deflate, sdch, br
accept-language: en-US,en;q=0.8


Security filter chain: [
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  CorsFilter
  LogoutFilter
  UsernamePasswordAuthenticationFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
  FilterSecurityInterceptor
]


************************************************************
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 1 of 12 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 2 of 12 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2017-05-12 19:18:28.527 DEBUG 16500 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : No SecurityContext was available from the HttpSession: null. A new one will be created.
2017-05-12 19:18:28.529 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 3 of 12 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2017-05-12 19:18:28.529 DEBUG 16500 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy        : /login at position 4 of 12 in additional filter chain; firing Filter: 'CorsFilter'
2017-05-12 19:18:28.541 DEBUG 16500 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@5baaaa2b
2017-05-12 19:18:28.541 DEBUG 16500 --- [nio-8080-exec-1] w.c.HttpSessionSecurityContextRepository : SecurityContext is empty or contents are anonymous - context will not be stored in HttpSession.
2017-05-12 19:18:28.541 DEBUG 16500 --- [nio-8080-exec-1] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed

UPDATE: So after adding the CORSFilter suggested in the accepted answer I also had to modify my requests in the frontend in order for the cookie with the JSESSIONID to be sent on each request. Bassically I had to add the following to ALL my reqquests: let options = new RequestOptions({ headers: headers, withCredentials: true });

This seems to be problem related to CORS configuration. Browser does make the OPTION preflight request before your actual GET call. I don't have much knowledge in this area, but you could try adding the filter below to the spring back end.

public class CORSFilter extends GenericFilterBean {

    public void doFilter(ServletRequest request, ServletResponse res, FilterChain chain)
        throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse response = (HttpServletResponse) res;
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.addHeader("Access-Control-Max-Age", "3600");
        response.addHeader("Access-Control-Allow-Headers", "X-Requested-With, X-Auth-Token");
        response.addHeader("Access-Control-Allow-Credentials", "true");

        if(req.getMethod().equalsIgnoreCase("options")){
             return;
        }
        chain.doFilter(request, response);
    }
}

And add the line below in the configure method of your WebSecurityConfig.

.addFilterBefore(new CORSFilter(), ChannelProcessingFilter.class);

The solution is very simple. The username and password on POST needs to be submitted as FormData and not part of body. I had the same issue with my app and after a long struggle I created a HTML form and did a POST then it worked like a charm.

See below live example. You should be able to signup with any id and try to login and debut it in your browser. It uses Spring boot for server side and Angular 4 on UI.

Live Demo: http://shop-venkatvp.rhcloud.com/#/

Login HTML: https://github.com/reflexdemon/shop/blob/master/src/app/login/login.component.html#L7-L31

Spring Security Configuration: https://github.com/reflexdemon/shop/blob/master/src/main/java/org/shop/WebSecurityConfig.java#L20-L34

Login controller: https://github.com/reflexdemon/shop/blob/master/src/main/java/org/shop/page/controller/LoginController.java

Hope this is helpful.

I'm in the middle of writing an Angular 4 and spring boot integration article. I don't have the article finished yet but the source code is done. I'm not sure why you are trying to serve your application from two different hosts, this unnecessarily complicates your application by injecting a cors requirement and having to manage two security context's. If this isn't an absolute requirement I would serve everything from the same server.

Take a look at this github project for a full integration of angular and spring boot: https://github.com/tschiman/tutorials/tree/master/spring-cloud/spring-cloud-bootstrap/gateway . You can ignore the zuul stuff in there if you don't need that. It has no impact on the support for the angular ui.

If you want to enable csrf, and you should, look at this commit: be4b206478c136d24ea1bf8b6f93da0f3d86a6c3

If reading that isn't making sense these are the steps you need to follow:

  1. Move your angular source code into your spring boot app
  2. Modify you angular-cli.json file to have an output directory in your resources static or resources public folder: "outDir":"../../resources/static/home" This sets the output directory for when you run ng build from the command line using the angular cli.
  3. Modify your login's default success url to redirect to /home/index.html .formLogin().defaultSuccessUrl("/home/index.html", true) this will force a successful login to always redirect to your angular app.
  4. If you want to automate building your angular app into maven then add this plugin to your POM file:

      <plugin> <artifactId>maven-antrun-plugin</artifactId> <executions> <execution> <phase>generate-resources</phase> <configuration> <tasks> <exec executable="cmd" osfamily="windows" dir="${project.basedir}/src/main/angular/ui"> <arg value="/c"/> <arg value="ng"/> <arg value="build"/> </exec> <exec executable="/bin/sh" osfamily="mac" dir="${project.basedir}/src/main/angular/ui"> <arg value="-c"/> <arg value="ng build"/> </exec> </tasks> </configuration> <goals> <goal>run</goal> </goals> </execution> </executions> </plugin> 

If you have additional questions I am happy to respond further if this was not enough for you to get it working.

Did you try to explicitly permit OPTIONS requests? Like this:

protected void configure(HttpSecurity http) throws Exception {
   ...
   http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll();
   ...
}

First of all - this is definitely a CORS configuration issue because you are running backend and frontend on different addresses.

You can twick backend to support CORS requests but this is a dirty solution, espesialy since you probably do not want CORS in production version of you app. Much cleaner approach would be to configure either frontend or backend to server as a proxy to the other.

I prefer to configure frontend to proxy requests to backend. This way it will have no effect on production version of the app.

Next part assumes you are using angular-cli (as you should with angular2 app).

They have a section in their docs specifically for proxy configuration: https://www.npmjs.com/package/angular-cli#proxy-to-backend .

I have all my REST services inside /api context, so my proxy.conf.json looks like this:

{
  "/api": {
    "target": "http://localhost:8083",
    "secure": false
  },
  "/login": {
    "target": "http://localhost:8083",
    "secure": false
  },
  "/logout": {
    "target": "http://localhost:8083",
    "secure": false
  }
}

Where http://localhost:8083 is my backend. You can then run your app with:

ng serve --proxy-config proxy.conf.json

and use only your frontend address. No CORS configuration needed.

PS If you dont use angular-cli Im sure you can use this approach with whatever tool you use to serve frontent. Before switching to angular-cli I used this with lite-server as well.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM