REST端点身份验证的Spring Security意外行为?

时间:2016-12-22 20:49:58

标签: spring rest spring-mvc spring-boot spring-security

我们正在寻找的方案如下:

  1. 客户端使用REST连接到REST登录URL
  2. Spring微服务(使用Spring Security)应返回200 OK和登录令牌
  3. 客户端保留令牌
  4. 客户端使用相同的令牌调用其他REST端点。
  5. 但是,我发现客户端正在获取302Location标头以及令牌。所以它确实进行了身份验证,但使用了不需要的HTTP响应状态代码和标头。

    Spring Security配置如下所示:

    @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();
    ...
    }
    

    我尝试添加拦截器和过滤器,但无法查看在Spring端设置和添加302和位置的位置。 但是,Location标头确实显示在客户端收到的响应标头中(以及其他Spring Security标头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
    

    任何建议如何使其按预期工作(&#34; 200 OK&#34;,没有位置标题和令牌)?

    注意:使用Spring Boot,Spring Security,无UI,只需调用REST端点的客户端代码。

6 个答案:

答案 0 :(得分:1)

如果您需要休息api,则不得使用http.formLogin()。它按照here所述生成基于表单的登录。

相反,您可以使用此配置

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);

创建一个类AuthTokenFilter,它扩展Spring UsernamePasswordAuthenticationFilter并覆盖doFilter方法,该方法检查每个请求中的身份验证令牌并相应地设置SecurityContextHolder

@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);
}

然后创建一个AuthenticationController,用/login url映射,检查凭据,然后返回令牌。

/*
* 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

要了解loadUserByUsernameUserDetailsServiceUserDetails,请参阅Spring security docs     }

为了更好地理解,请仔细阅读以上链接和后续章节。

答案 1 :(得分:1)

http.formLogin()

专为基于表单的登录而设计。因此,如果您尝试在未经过身份验证的情况下访问受保护资源,则可以预期响应中的302状态和位置标头。

根据您的要求/方案,

  
      
  1. 客户端使用REST连接到REST登录URL
  2.   

您是否考虑过使用HTTP Basic进行身份验证?

http.httpBasic()

使用HTTP Basic,您可以使用用户名/密码填充Authorization标头,BasicAuthenticationFilter将负责验证凭据并相应地填充SecurityContext。

我在客户端使用Angular,在后端使用Spring Boot-Spring Security,有working example

如果查看security-service.js,您会看到一个名为securityService的工厂,它提供login()功能。此函数根据HTTP Basic格式使用填充了用户名/密码的/principal标头调用Authorization端点,例如:

Authorization : Basic base64Encoded(username:passsword)

BasicAuthenticationFilter将通过提取凭据并最终验证用户并使用经过身份验证的委托人填充SecurityContext来处理此请求。身份验证成功后,请求将继续到目标端点/principal,该端点映射到SecurityController.currentPrincipal,它只返回经过身份验证的主体的json表示。

您的剩余要求:

  
      
  1. Spring微服务(使用Spring Security)应返回200 OK和登录令牌
  2.   
  3. 客户端保留令牌
  4.   
  5. 客户端使用相同的令牌调用其他REST端点。
  6.   

您可以生成安全/登录令牌并返回该令牌而不是用户信息。但是,如果您在不同的微服务中部署了许多需要通过安全令牌保护的REST端点,我强烈建议您查看Spring Security OAuth。构建自己的STS(安全令牌服务)可能变得非常复杂,因此不推荐使用。

答案 2 :(得分:0)

这是一个302响应,告诉浏览器重定向到您的登录页面。你期望发生什么? 302响应必须具有Location标头。

答案 3 :(得分:0)

您可以实现自定义AuthenticationSuccessHandler并覆盖方法“onAuthenticationSuccess”,以根据需要更改响应状态。

示例:

@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);
}

答案 4 :(得分:0)

您需要覆盖默认的注销成功处理程序,以避免重定向。在spring boot2中,您可以执行以下操作:

....logout().logoutSuccessHandler((httpServletRequest,httpServletResponse,authentication)->{
                //do nothing not to redirect
        })

有关更多详细信息:请检查this

答案 5 :(得分:-1)

您可以使用headers().defaultsDisabled()然后将该方法链接以添加所需的特定标题。