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

Posted

技术标签:

【中文标题】REST端点身份验证的Spring Security意外行为?【英文标题】:Spring Security unexpected behavior for REST endpoints authentication? 【发布时间】:2017-05-08 13:41:19 【问题描述】:

我们要找的场景如下:

    客户端通过 REST 连接到 REST 登录 url Spring 微服务(使用 Spring Security)应该返回 200 OK 和一个登录令牌 客户端保留令牌 客户端使用相同的令牌调用其他 REST 端点。

但是,我看到客户端正在获取 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 的位置。 但是,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

任何建议如何使其按预期工作(“200 OK”,没有 Location 标头和令牌)?

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

【问题讨论】:

我们要做的是在应用程序使用 POST 和凭据登录时拥有一个 url,并接收一个登录令牌。我不清楚为什么应该有 302。我可能遗漏了一些东西...... //客户端调用:new RestTemplate().execute(SIGIN_URL, HttpMethod.POST, new RequestCallback() ..., new ResponseExtractor... public Object extractData(... ) //状态码现在是 302 headersToUpdate.add("Cookie", response.getHeaders().getFirst("Set-Cookie")); return null; (没有 UI 和按钮) 它正在重定向到一个新的 url 【参考方案1】:

您可以使用headers().defaultsDisabled(),然后链接该方法以添加您想要的特定标头。

【讨论】:

【参考方案2】:

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

【讨论】:

客户端连接到 /signin 这是登录 url。所以我实际上希望看到 200。我错过了什么吗?你能分享更多信息吗? 也许我会改写 - 为了获得 200 状态代码而没有 Location 标头,我应该更改什么? @Roy 你不能。 /signin url 是用于浏览器的,还是 API 客户端获取某种令牌的登录端点?如果是要获取token,那么这个token在客户端获取后应该如何使用呢?【参考方案3】:

如果你需要一个rest 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

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

【讨论】:

@Ramanujan,我有第一个工作基线。我会阅读您发送的链接,查看代码并更新。谢谢! 仍在为冗长且不那么清晰的文档而苦苦挣扎。同时,您能否就如何处理注销(例如,authToken 无效,不使用重定向等)添加建议?谢谢!【参考方案4】:

您可以实现自定义 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);

【讨论】:

【参考方案5】:

http.formLogin()

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

根据您的要求/场景,

    客户端通过 REST 连接到 REST 登录 url

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

http.httpBasic()

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

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

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

Authorization : Basic base64Encoded(username:passsword)

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

对于您的剩余要求:

    Spring 微服务(使用 Spring Security)应该返回 200 OK 和一个登录令牌 客户端保留令牌 客户端使用相同的令牌调用其他 REST 端点。

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

【讨论】:

【参考方案6】:

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

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

更多详情:请查看this。

【讨论】:

以上是关于REST端点身份验证的Spring Security意外行为?的主要内容,如果未能解决你的问题,请参考以下文章

Spring-Boot REST 服务基本 http 身份验证排除一个端点

在 Spring 安全性中使用 rest 服务进行身份验证和授权 [关闭]

具有基本身份验证和 OAuth 顺序问题的 Spring Boot 安全性

简单的 REST 端点身份验证

如何从 WCS 向 Magnolia Rest 端点进行身份验证

Spring安全缓存基本身份验证?不验证后续请求