Spring Security unexpected behavior for REST endpo

2019-07-22 17:34发布

The scenario we are looking for is as follows:

  1. client connects with REST to a REST login url
  2. Spring microservice (using Spring Security) should return 200 OK and a login token
  3. the client keeps the token
  4. the client calls other REST endpoints using the same token.

However, I see that the client is getting 302 and a Location header, together with the token. So it does authenticate, but with un-desired HTTP response status code and header.

The Spring Security configuration looks like this:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .disable()  // Refactor login form
               // See https://jira.springsource.org/browse/SPR-11496
            .headers()
                .addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
                .and()
            .formLogin()
                .loginPage("/signin")
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/signout")
                .permitAll()
                .and()
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated();
...
}

I tried adding interceptors and filters but can't see where 302 and Location being set and added in Spring side. However, the Location header does show in the response headers received at the client side (together with the rest of the Spring Security headers LINK):

Server=Apache-Coyote/1.1
X-Content-Type-Options=nosniff
X-XSS-Protection=1; mode=block
Cache-Control=no-cache, no-store, max-age=0, must-revalidate
Pragma=no-cache
Expires=0
X-Frame-Options=DENY, SAMEORIGIN
Set-Cookie=JSESSIONID=D1C1F1CE1FF4E1B3DDF6FA302D48A905; Path=/; HttpOnly
Location=http://ec2-35-166-130-246.us-west-2.compute.amazonaws.com:8108/ <---- ouch
Content-Length=0
Date=Thu, 22 Dec 2016 20:15:20 GMT

Any suggestion how to make it work as expected ("200 OK", no Location header and the token)?

NOTE: using Spring Boot, Spring Security, no UI, just client code calling REST endpoints.

5条回答
兄弟一词,经得起流年.
2楼-- · 2019-07-22 17:46

It's a 302 response telling the browser to redirect to your login page. What do you expect to happen? 302 response must have a Location header.

查看更多
趁早两清
3楼-- · 2019-07-22 17:46

You can use headers().defaultsDisabled() and then chain that method to add the specific headers you want.

查看更多
祖国的老花朵
4楼-- · 2019-07-22 17:51

You can implement your custom AuthenticationSuccessHandler and override method "onAuthenticationSuccess" to change the response status as per your need.

Example:

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
        Authentication authentication) throws IOException, ServletException {
    ObjectMapper mapper = new ObjectMapper();
    Map<String, String> tokenMap = new HashMap<String, String>();
    tokenMap.put("token", accessToken.getToken());
    tokenMap.put("refreshToken", refreshToken.getToken());
    response.setStatus(HttpStatus.OK.value());
    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
    mapper.writeValue(response.getWriter(), tokenMap);
}
查看更多
Lonely孤独者°
5楼-- · 2019-07-22 17:58

http.formLogin()

is designed for form-based login. So the 302 status and Location header in the response is expected if you attempt to access a protected resource without being authenticated.

Based on your requirement/scenario,

  1. client connects with REST to a REST login url

have you considered using HTTP Basic for authentication?

http.httpBasic()

Using HTTP Basic, you can populate the Authorization header with the username/password and the BasicAuthenticationFilter will take care of authenticating the credentials and populating the SecurityContext accordingly.

I have a working example of this using Angular on the client-side and Spring Boot-Spring Security on back-end.

If you look at security-service.js, you will see a factory named securityService which provides a login() function. This function calls the /principal endpoint with the Authorization header populated with the username/password as per HTTP Basic format, for example:

Authorization : Basic base64Encoded(username:passsword)

The BasicAuthenticationFilter will process this request by extracting the credentials and ultimately authenticating the user and populating the SecurityContext with the authenticated principal. After authentication is successful, the request will proceed to the destined endpoint /principal which is mapped to SecurityController.currentPrincipal which simply returns a json representation of the authenticated principal.

For your remaining requirements:

  1. Spring microservice (using Spring Security) should return 200 OK and a login token
  2. the client keeps the token
  3. the client calls other REST endpoints using the same token.

You can generate a security/login token and return that instead of the user info. However, I would highly recommend looking at Spring Security OAuth if you have a number of REST endpoints deployed across different Microservices that need to be protected via a security token. Building out your own STS (Security Token Service) can become very involved and complicated so not recommended.

查看更多
【Aperson】
6楼-- · 2019-07-22 18:03

If you need a rest api, you must not use http.formLogin(). It generates form based login as described here.

Instead you can have this configuration

httpSecurity
                .csrf()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(authenticationEntryPoint)
                .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                    .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                    .antMatchers("/login").permitAll()
                    .anyRequest().authenticated()
                .and()
                .logout()
                    .disable()
                .addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);

Create a class, AuthTokenFilter which extends Spring UsernamePasswordAuthenticationFilter and override doFilter method, which checks for an authentication token in every request and sets SecurityContextHolder accordingly.

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse) response;
        resp.setHeader("Access-Control-Allow-Origin", "*");
        resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
        resp.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, " + tokenHeader);

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String authToken = httpRequest.getHeader(tokenHeader);
        String username = this.tokenUtils.getUsernameFromToken(authToken); // Create some token utility class to manage tokens

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(-------------);
            // Create an authnetication as above and set SecurityContextHolder
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
}

Then create an AuthenticationController, mapped with /login url, which checks credentials, and returns token.

/*
* Perform the authentication. This will call Spring UserDetailsService's loadUserByUsername implicitly
* BadCredentialsException is thrown if username and password mismatch
*/
Authentication authentication = this.authenticationManager.authenticate(
     new UsernamePasswordAuthenticationToken(
            authenticationRequest.getUsername(),
            authenticationRequest.getPassword()
     )
);
SecurityContextHolder.getContext().setAuthentication(authentication);        
UserDetailsImp userDetails = (UserDetailsImp) authentication.getPrincipal();
// Generate token using some Token Utils class methods, using this principal

To understand loadUserByUsername , UserDetailsService and UserDetails, please refer Spring security docs }

For better understanding, please thoroughly read above link and subsequent chapters.

查看更多
登录 后发表回答