Mock Spring的远程JWT服务

时间:2019-01-03 18:35:51

标签: spring spring-boot spring-security

我当前正在使用RemoteTokenServices类:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Value("${auth-server.url}")
    private String authEndpoint;

    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Value("${security.oauth2.client.client-secret}")
    private String clientSecret;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("ms/legacy");
    }

    @Bean
    public ResourceServerTokenServices tokenService() {
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId(clientId);
        tokenServices.setClientSecret(clientSecret);
        tokenServices.setCheckTokenEndpointUrl(authEndpoint + "/uaa/oauth/check_token");
        return tokenServices;
    }
}

我希望能够对所有端点集成测试轻松且正确地进行模拟,知道:

  • JWT在OncePerRequestFilter中进行解码以获取一些关键信息
  • 我对测试身份验证失败不感兴趣(嗯,但这不是我们要在每个端点上执行的操作)

有没有一种标准的方法来

  1. 手动生成JWT令牌吗?
  2. 轻松模拟所有令牌服务访问权限?

预期的结果是,我可以只用几行额外的内容编写一个端点测试,以在请求中设置正确的JWT,并且令牌服务会愚蠢地同意其有效性。

1 个答案:

答案 0 :(得分:0)

鉴于我们根本不想测试安全性,这种情况的最佳解决方案是:

  • 使用标准的Spring测试安全性管理@WithMockUserMockMvc
  • 使ResourceServerConfigurerAdapter适应测试:
  • 创建一个基类,该基类承载除令牌之外的所有配置
  • 为托管令牌特定配置的非测试配置文件(@ActiveProfiles("!test"))创建继承类
  • 为测试配置文件创建继承类,以停用远程令牌检查(security.stateless(false);
  • 使测试类使用test配置文件
  • 在测试中的正确时间注入正确的令牌提取信息

这是在实践中的实现方式:

ResourceServerConfigurerAdapter为基础,因此配置在测试和非测试上下文之间具有主要的共同部分:

public class BaseResourceServerConfiguration extends ResourceServerConfigurerAdapter {

  @Override
  public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    resources.resourceId("ms/legacy");
  }

  @Override
  public void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().permitAll().and().cors().disable().csrf().disable().httpBasic().disable()
        .exceptionHandling()
        .authenticationEntryPoint(
            (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
        .accessDeniedHandler(
            (request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED));
  }

}

它的非测试实现:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Profile("!test")
public class ResourceServerConfiguration extends BaseResourceServerConfiguration {

    @Value("${auth-server.url}")
    private String authEndpoint;

    @Value("${security.oauth2.client.client-id}")
    private String clientId;

    @Value("${security.oauth2.client.client-secret}")
    private String clientSecret;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("ms/legacy");
    }

    @Bean
    public ResourceServerTokenServices tokenService() {
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        tokenServices.setClientId(clientId);
        tokenServices.setClientSecret(clientSecret);
        tokenServices.setCheckTokenEndpointUrl(authEndpoint + "/uaa/oauth/check_token");
        return tokenServices;
    }
}

对于测试:

@Configuration
@EnableResourceServer
@ActiveProfiles("test")
public class TestResourceServerConfigurerAdapter extends BaseResourceServerConfiguration {

  @Override
  public void configure(ResourceServerSecurityConfigurer security) throws Exception {
    super.configure(security);

    // Using OAuth with distant authorization service, stateless implies that the request tokens
    // are verified each time against this service. In test, we don't want that because we need
    // properly isolated tests. Setting this implies that the security is checked only locally
    // and allows us to mock it with @WithMockUser, @AutoConfigureMockMvc and autowired MockMVC
    security.stateless(false);
  }

}

使用测试请求过滤器插入令牌专用信息:

@Component
@ActiveProfiles("test")
public class TestRequestFilter extends OncePerRequestFilter {

  private Optional<InfoConf> nextInfoConf = Optional.empty();

  // Request info is our request-scoped bean that holds JWT info
  @Autowired
  private RequestInfo info;

  @Override
  protected void doFilterInternal(HttpServletRequest httpServletRequest,
      HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    if (nextInfoConf.isPresent()) {
      info.setInfoConf(nextInfoConf.get());
    }
    filterChain.doFilter(httpServletRequest, httpServletResponse);
  }

  public void setNextInfoConf(InfoConf nextInfoConf) {
    this.nextInfoConf = Optional.of(nextInfoConf);
  }

  public void clearNextInfoConf() {
    nextInfoConf = Optional.empty();
  }

}

当然,当没有JWT时,让JWT解析什么也不做。

我们还编写了一个小的实用程序组件来创建要注入的相关信息。

典型的集成测试如下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class TestClass {

    @Autowired
    protected MockMvc mockMvc;

    @Before
    public void before() {
        // Create an user in DB
        // Inject the related information in our filter
    }

    @After
    public void after() {
        // Cleanup both in DB and filter
    }

    @Test
    @WithMockUser
    public void testThing() throws Exception {
        // Use MockMVC
    }
}

另一种解决方案是确实模拟ResourceServerTokenServices,但实际上要构建适当的令牌要痛苦得多,并且使用Spring的标准安全性模拟似乎更合适。