【问题标题】:Does securing a REST application with a JWT and Basic authentication make sense?使用 JWT 和基本身份验证保护 REST 应用程序是否有意义?
【发布时间】:2015-05-09 05:20:55
【问题描述】:

我有一个 Spring REST 应用程序,最初使用基本身份验证进行保护。

然后我添加了一个登录控制器,它创建了一个 JWT JSON Web Token,用于后续请求。

我可以将以下代码移出登录控制器并进入安全过滤器吗?然后我就不再需要登录控制器了。

tokenAuthenticationService.addTokenToResponseHeader(responseHeaders, credentialsResource.getEmail());

或者我可以删除基本身份验证吗?

将基本身份验证与 JWT 混合使用是一个好的设计吗?

虽然一切正常,但我对如何最好地设计这种安全性有点不知所措。

【问题讨论】:

  • 后续请求中token是如何发送到服务器的? (HTTP 标头?Cookie?)。另外,您是否使用 TLS (SSL)?
  • 嗨,Les,很高兴看到你再次出现!是的,令牌作为 X-Auth-Token 标头发送。我也在使用 TLS。使用 JWT 时必须使用 TLS 吗?
  • 嗨斯蒂芬! :) 如果 JWT 代表经过验证的身份,是的,我认为 TLS 是强制性的,否则它(更)容易受到 MITM 攻击。
  • 我尝试回答之前的最后一个背景问题:您的 REST 客户端是 JavaScript(JQuery、Angular 等)还是移动客户端?
  • 好点。我认为在 jwt 设置中不需要基本身份验证..

标签: spring-security basic-authentication jwt


【解决方案1】:

假设所有通信都使用 100% TLS - 无论是在登录期间还是登录后的所有时间 - 通过基本身份验证使用用户名/密码进行身份验证并接收 JWT 作为交换是一个有效的用例。这几乎正是 OAuth 2 的流程之一(“密码授予”)的工作原理。

这个想法是最终用户通过一个端点进行身份验证,例如/login/token 使用您想要的任何机制,并且响应应该包含要在所有后续请求中发回的 JWT。 JWT 应该是具有适当 JWT 过期 (exp) 字段的 JWS(即加密签名的 JWT):这可确保客户端无法操纵 JWT 或使其寿命超过应有的时间。

您也不需要X-Auth-Token 标头:HTTP 身份验证Bearer 方案是为这个确切的用例创建的:基本上任何位于Bearer 方案名称后面的信息都是“承载”信息,应该得到验证。您只需设置 Authorization 标头:

Authorization: Bearer <JWT value here>

但是,话虽如此,如果您的 REST 客户端是“不受信任的”(例如启用 JavaScript 的浏览器),我什至不会这样做:HTTP 响应中可通过 JavaScript 访问的任何值 - 基本上是任何标头值或响应体值 - 可以通过 MITM XSS 攻击被嗅探和拦截。

最好将 JWT 值存储在仅安全、仅 http 的 cookie 中(cookie 配置:setSecure(true)、setHttpOnly(true))。这保证了浏览器将:

  1. 仅通过 TLS 连接传输 cookie,并且,
  2. 永远不要让 cookie 值可用于 JavaScript 代码。

这种方法几乎是您为实现最佳实践安全所需做的一切。最后一件事是确保您对每个 HTTP 请求都有 CSRF 保护,以确保向您的站点发起请求的外部域无法运行。

最简单的方法是使用随机值设置一个仅安全(但不是仅 http)的 cookie,例如一个 UUID。

然后,在对服务器的每个请求中,确保您自己的 JavaScript 代码读取 cookie 值并将其设置在自定义标头中,例如X-CSRF-Token 并在服务器中的每个请求上验证该值。除非外部客户端通过 HTTP 选项请求获得授权,否则外部域客户端无法为对您的域的请求设置自定义标头,因此任何 CSRF 攻击尝试(例如在 IFrame 中,等等)都将失败。

据我们所知,这是当今网络上不受信任的 JavaScript 客户端可用的同类最佳安全性。如果你好奇的话,Stormpath 也在these techniques 上写了一篇文章。 HTH!

【讨论】:

  • 美丽的答案 Les,谢谢。我记得我后来在应用程序开发中添加了登录控制器,当时我了解并在应用程序中实现了 JWT 令牌身份验证。事实上,我不知道如何从 Basic auth Spring Security 过滤器中创建令牌。阅读您的解决方案,我发现这是我可以而且应该这样做的方式,并完全删除登录控制器,因为它变得不必要......
  • 您好 Les,我正准备用使用 Bearer 前缀的标准 Authorization 标头替换 X-Auth-Token 标头。就在那时,我偶然发现了一个答案,指出我不应该使用标准标题,而是使用自定义标题。他的观点是这个标准头应该留给基本身份验证。见stackoverflow.com/questions/12086041/…困惑...
  • @StephaneEybert 我在该线程中添加了答案。 Authorization 标头支持许多方案。你可以使用任何你想要的方案。 Basic 代表一种算法。 Bearer 就是它后面的任何文本(没有算法)。其他方案(如Digest)使用不同的算法。你甚至可以发明自己的方案。关键是标题是相同的,但方案名称及其尾随文本值反映了确切的行为。我扩展了这个here
  • @StephaneEybert 在我上面的回答中,我建议您出于 authentication 原因不应该使用自定义标头 - 只需使用 Authorization 标头(以及您的情况下的 Bearer 方案)。您需要为 CSRF Token 方法使用自定义标头,因为这不是身份验证机制。
  • @StephaneEybert 也可能不清楚,但在您的用例中,我建议您不要使用 Authorization 标头 根本 - 一个安全的,仅限 http cookie 在您的情况下甚至更好。阅读this blog article 了解原因(这在我上面的答案中也有链接)。
【解决方案2】:

这里有一些代码来支持如何在 Spring 中执行此操作的公认答案....只需扩展 UsernamePasswordAuthenticationFilter 并将其添加到 Spring Security...这适用于 HTTP Basic Authentication + Spring Security

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {

        this.authenticationManager = authenticationManager;

    }

    @Override

    public Authentication attemptAuthentication(HttpServletRequest req,

                                                HttpServletResponse res) throws AuthenticationException {

        try {

            ApplicationUser creds = new ObjectMapper()

                    .readValue(req.getInputStream(), ApplicationUser.class);

            return authenticationManager.authenticate(

                    new UsernamePasswordAuthenticationToken(

                            creds.getUsername(),

                            creds.getPassword(),

                            new ArrayList<>())

            );

        } catch (IOException e) {

            throw new RuntimeException(e);

        }

    }

    @Override

    protected void successfulAuthentication(HttpServletRequest req,

                                            HttpServletResponse res,

                                            FilterChain chain,

                                            Authentication auth) throws IOException, ServletException {

        String token = Jwts.builder()

                .setSubject(((User) auth.getPrincipal()).getUsername())

                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))

                .signWith(SignatureAlgorithm.HS512, SECRET)

                .compact();

        res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);

    }

}

使用 JWT 库:

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

spring boot 配置类

package com.vanitysoft.payit.security.web.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

    import com.vanitysoft.payit.util.SecurityConstants;

    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
         @Autowired
           private UserDetailsService userDetailsService;

            @Autowired
            private  BCryptPasswordEncoder bCryptPasswordEncoder;

         @Override
           protected void configure(AuthenticationManagerBuilder auth) throws Exception {
              auth.userDetailsService(userDetailsService)
                      .passwordEncoder(bCryptPasswordEncoder);
           }

         @Override
            protected void configure(HttpSecurity http) throws Exception {
             http.cors().and().csrf().disable()
                    .authorizeRequests()                             
                        .antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll()
                        .antMatchers("/user/**").authenticated()
                        .and()
                        .httpBasic()
                        .and()
                        .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                        .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                        .and()
                        .logout()
                        .permitAll();

            }
    }

【讨论】:

  • 你有把这个过滤器注入到 Spring Security 配置中的代码吗?
  • 有没有办法表示这个过滤器只用于登录请求?
  • 在将过滤器注入安全配置之前,如何确保已创建您注入过滤器的身份验证管理器?
  • 我正在使用 Spring Boot,过滤器的接线由注释自动处理。我会用我在 Config 中使用的代码更新我的答案......查看 SpringBoot 和 Spring Security 教程。
  • 你也在使用 Spring Boot 2 吗?您的 JWTAuthorizationFilter 过滤器是否也扩展了相同的 UsernamePasswordAuthenticationFilter 类?为什么有两个过滤器而不是在同一个过滤器中进行身份验证和授权?你为什么用这个@EnableGlobalMethodSecurity(prePostEnabled = true)?您没有其他过滤器来通过 JWT 令牌进行身份验证?
猜你喜欢
  • 2016-07-30
  • 2018-09-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-24
  • 2019-03-01
  • 2019-10-21
  • 2014-01-26
相关资源
最近更新 更多