【问题标题】:Authentication tests are failing身份验证测试失败
【发布时间】:2022-01-04 11:00:08
【问题描述】:

我正在尝试使用 spring-mvcspring-security 在 Spring Boot REST Web 服务上实现安全性(HTTP Basic Auth + JWT)。

为了实现这一点,我使用了带有两个 AuthenticationManager 的 SecurityContextRepository。

这是我的 SecurityContextRepository:

@Component
public class MySecurityContextRepository implements SecurityContextRepository {

  private final JwtAuthenticationManager jwtAuthenticationManager;
  private final HttpBasicAuthAuthorizationManager httpBasicAuthAuthorizationManager;

  @Autowired
  public MySecurityContextRepository(JwtAuthenticationManager JwtAuthenticationManager,
      HttpBasicAuthAuthorizationManager httpBasicAuthAuthorizationManager) {
    this.jwtAuthenticationManager = jwtAuthenticationManager;
    this.httpBasicAuthAuthorizationManager = httpBasicAuthAuthorizationManager;
  }

  @Override
  public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
    HttpServletRequest request = requestResponseHolder.getRequest();

    String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
    if (authHeader == null) {
      return new SecurityContextImpl(null);
    }
    
    if (authHeader.startsWith("Bearer")) {
      String authToken = authHeader.substring(7);
      Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
      return new SecurityContextImpl(jwtAuthenticationManager.authenticate(auth));
    } else if (authHeader.startsWith("Basic")) {
      String authToken = authHeader.substring(6);
      Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
      return new SecurityContextImpl(httpBasicAuthAuthorizationManager.authenticate(auth));
    } else {
      return null;
    }
  }

  @Override
  public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
    // Nothing to save here
  }

  @Override
  public boolean containsContext(HttpServletRequest request) {
    return request.getHeader(HttpHeaders.AUTHORIZATION) != null
        && (request.getHeader(HttpHeaders.AUTHORIZATION).startsWith("Bearer") ||
        request.getHeader(HttpHeaders.AUTHORIZATION).startsWith("Basic"));
  }
}

这是我的 AuthenticationManager 之一

@Slf4j
@Component
public class JwtAuthenticationManager implements AuthenticationManager {
  private final ObjectMapper mapper;
  private final JwtParser parser;

  public wtAuthenticationManager(ObjectMapper mapper, SecurityProperties security) {
    this.mapper = mapper;
    this.parser = Jwts.parser().setSigningKey(security.getSigningKey());
  }

  @Override
  public Authentication authenticate(Authentication authentication) {

    String authToken = authentication.getCredentials().toString();
    if (!validateToken(authToken)) {
      return null;
    }

    Claims claims = getAllClaimsFromToken(authToken);
    String json = new JSONObject(claims).toString();
    try {
      HttpAuthUser user = mapper.readValue(json, HttpAuthUser.class);
      return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
    } catch (JsonProcessingException ex) {
      LOGGER.warn("Could not parse JWT", ex);
      return null;
    }
  }

  private Claims getAllClaimsFromToken(String token) {
    return parser.parseClaimsJws(token).getBody();
  }

  private Boolean isTokenExpired(String token) {
    try {
      final Date expiration = getExpirationDateFromToken(token);
      return expiration.before(new Date());
    } catch (JwtException jwtException) {
      LOGGER.trace("Expired JWT", jwtException);
      return true;
    }
  }

  private Date getExpirationDateFromToken(String token) {
    return getAllClaimsFromToken(token).getExpiration();
  }

  private Boolean validateToken(String token) {
    return !isTokenExpired(token);
  }
}

这是我的 WebSecurityConfigurerAdapter:

@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(SecurityProperties.class)
public class Security extends WebSecurityConfigurerAdapter {
  private final SecurityProperties securityProperties;
  private final SecurityProblemSupport problemSupport;
  private final MySecurityContextRepository mySecurityContextRepository;

  @Autowired
  public Security(SecurityProperties securityProperties, SecurityProblemSupport problemSupport,
      MySecurityContextRepository mySecurityContextRepository) {
    this.securityProperties = securityProperties;
    this.problemSupport = problemSupport;
    this.mySecurityContextRepository = mySecurityContextRepository;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    /*
     * Roles authorisation is handled in the controllers.
     *
     * Domain object authorisations are handled by @Rbac* annotations
     */
    http.cors().disable()
        .csrf().disable()
        .httpBasic().disable()
        .authorizeRequests()
        .antMatchers("/api/auth/**").permitAll()
        .anyRequest().hasAnyRole("regular", "admin")
        .and()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .securityContext().securityContextRepository(securityContextRepository)
        .and()
        .exceptionHandling()
        .authenticationEntryPoint(
            (request, response, authException) -> {
              response.addHeader(AuthHelper.HEADER_WWW_AUTH, AuthHelper.HEADER_JWT_PREFIX);
              problemSupport.commence(request, response, authException);
            })
        .accessDeniedHandler(problemSupport);
  }
}

当我启动我的网络服务并使用 curl 进行尝试时,一切都按预期工作:经过身份验证的请求就像魅力一样工作,而未经身份验证的请求被拒绝。

但我的问题是当我运行测试时:我的所有请求都被拒绝了。

这是失败的测试之一(与基本身份验证相同的问题):

@SpringBootTest(classes = TestConfig.class)
@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
public class AuthenticationTest {

  @Test
  @WithAnonymousUser
  public void testValidJwtToken() throws Exception {
 
    MvcResult response = this.mvc.perform(post("/api/auth/token")
            .contentType(MediaType.APPLICATION_JSON)
            .content("{\"email\": \"test@test.com\", \"password\": \"super_password\"}"))
        .andExpect(status().isCreated())
        .andReturn();

    HttpAuth auth = mapper.readValue(response.getResponse().getContentAsString(),
        HttpAuth.class);

    String token = auth.getAccessToken();

    this.mvc.perform(get("/api/users").header("Authorization", "Bearer " + token))
        .andExpect(status().isOk());
  }
}

java.lang.AssertionError: Status expected:<200> but was:<401> 失败

在测试的调试输出中,我可以看到一个标题“Authorization: Bearer foo.bar.foobar”,在调试模式下,我通过了我的JwtAuthenticationManager

HttpBasicAuthAuthorizationManager 也会发生同样的事情

我在某处遗漏了什么吗?

【问题讨论】:

  • 请阅读 spring 文档,了解如何正确实现 JWT 身份验证。您已经编写了很多不需要的自定义安全性,以及对 Spring Security 中已经存在的 JWT 的手动解析。 docs.spring.io/spring-security/reference/servlet/oauth2/… 我不知道你为什么选择使用安全库,然后坚持实现已经存在的功能。那么使用图书馆有什么意义呢?自定义安全性也是不好的做法。
  • 我不使用 OAuth 2.0 并且与 Http Basic Auth 有同样的问题。无论如何,我读了这篇文章marcobehler.com/guides/spring-security 试图了解 Spring 安全
  • 您正在使用 oauth,因为您正在阅读授权标头。你知道授权标头是 oauth 的一部分吗?
  • 您发布的文章讨论了使用 formLogin 登录或使用用户名和密码进行基本登录。另一方面,您正在使用授权标头获取令牌,该标头是 oauth 的“资源服务器”文档的一部分。您基本上是在构建一个自定义登录解决方案,将 formLogin 与 oauth2 资源服务器混合在一起。这是不好的做法。构建自定义安全解决方案是不好的做法。这就是存在安全标准的原因。
  • basic auth 使用用户名和密码,formlogin 使用用户名和密码。令牌是 oauth2 标准的一部分,使用令牌对某人进行身份验证是 oauth2 资源服务器标准的一部分。如果您只想使用令牌登录某人,您应该使用我发布的内容而不是构建自定义安全解决方案。

标签: spring spring-boot spring-security


【解决方案1】:

我在周末之前将我们所有的微服务更新到 Spring Boot 2.6.0 时偶然发现了这一点。之前一切都很好。

似乎他们更改了 ErrorPageSecurityFilter 中的某些内容,仅应在发送 ERROR 时调用。 MockMvc 在所有此类调度之间似乎没有区别,因此过滤器会阻止您的请求。

“好消息”:现在您可以在测试中禁用它,MockMvc 应该会再次工作。有关更详细的说明和解决方法,请参阅:https://github.com/spring-projects/spring-boot/issues/28759#issuecomment-975398667

您的请求有可能在非测试环境中有效吗?

【讨论】:

  • 不幸的是,我仍在使用 Srping Boot 2.5.3。但是,确实,我的请求在非测试环境中按预期工作
猜你喜欢
  • 2015-08-18
  • 1970-01-01
  • 2011-09-29
  • 1970-01-01
  • 2014-05-29
  • 2014-07-05
  • 1970-01-01
  • 2014-01-24
相关资源
最近更新 更多