Spring Security 6请求匹配器和包含ID的URLs

huangapple go评论54阅读模式
英文:

Spring Security 6 Request Matchers and URLs containing IDs

问题

I'm trying to upgrade to spring boot 3 and spring security 6.
我正在尝试升级到Spring Boot 3和Spring Security 6。

I'm running into an issue where previously, mvcMatchers would match against these URLs:
我遇到了一个问题,之前mvcMatchers会匹配这些URL:

As part of the move to spring security 6, mvcMatchers are now replaced with requestMatchers - I thought I could drop in the new matches and things would keep working, but now the listed URLs before only allow some of the requests. Here are some examples:
作为升级到Spring Security 6的一部分,mvcMatchers现在被requestMatchers替代了 - 我以为我可以使用新的匹配规则,一切都会继续工作,但现在列出的URL只允许某些请求。以下是一些示例:

.mvcMatchers(assessmentsEndpoints).authenticated() use to match
.mvcMatchers(assessmentsEndpoints).authenticated()以前可以匹配

"/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/customer-rating".
"/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/customer-rating"

.requestMatchers(assessmentsEndpoints).authenticated() does not match so the API returns a 403.
.requestMatchers(assessmentsEndpoints).authenticated()不匹配,因此API返回403。

This makes me think that requestMatcher is not a drop in replacement for mvcMatcher, but I'm not sure how I should be structuring this to make requestMatcher allow these requests.
这让我觉得requestMatcher不是mvcMatcher的简单替代品,但我不确定应该如何构建它以使requestMatcher允许这些请求。

I have many requests with path parameters like "/assessment/{assessmentId}/value-rating*". How should I structure my String[] endpoints to allow such URLs?
我有许多带有路径参数的请求,例如"/assessment/{assessmentId}/value-rating*"。我应该如何构建我的String[]端点以允许这些URL?

For reference, here is the full SecurityConfig class that contains the relevant code.
供参考,这是包含相关代码的完整的SecurityConfig类。

英文:

I'm trying to upgrade to spring boot 3 and spring security 6.

I'm running into an issue where previously, mvcMatchers would match against these URLs:

        String[] companiesEndpoints = {"/companies", "/companies/*"};
        String[] ideationEndpoint = {"/ideation", "/ideation/*"};
        String[] assessmentsEndpoints = {"/assessment", "/assessment/*", "/assessment/*/value-rating", "/assessment/*/viability-rating", "/assessment/*/customer-rating"};
        String[] teamsEndpoints = {"/teams", "/teams/*"};
        String[] userEndpoints = {"/users", "/users/*"};
        String[] projectEndpoints = {"/project", "/project/*", "/project-and-assessment"};
        String[] workspaceEndpoints = {"/workspace", "/workspace/*"};
        String[] tagEndpoints = {"/tag", "/tag/*"};

As part of the move to spring security 6, mvcMatchers are now replaced with requestMatchers - I thought I could drop in the new matches and things would keep working, but now the listed URLs before only allow some of the requests. Here are some examples:

.mvcMatchers(assessmentsEndpoints).authenticated() use to match
"/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/customer-rating".
.requestMatchers(assessmentsEndpoints).authenticated() does not match so the API returns a 403.

This makes me think that requestMatcher is not a drop in replacement for mvcMatcher, but I'm not sure how I should be structuring this to make requestMatcher allow these requests.

I have many requests with path parameters like "/assessment/{assessmentId}/value-rating*". How should I structure my String[] endpoints to allow such URLs?

For reference, here is the full SecurityConfig class that contains the relevant code.

@Configuration
public class SecurityConfig {

    @Value(value = "${auth0.audience}")
    private String apiAudience;
    @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
    private String issuer;

    @Bean
    ForwardedHeaderFilter forwardedHeaderFilter() {
        return new ForwardedHeaderFilter();
    }

    @Bean
    JwtDecoder jwtDecoder() {
        NimbusJwtDecoder jwtDecoder = JwtDecoders.fromOidcIssuerLocation(issuer);

        OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator(apiAudience);
        OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuer);
        OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

        jwtDecoder.setJwtValidator(withAudience);

        return jwtDecoder;
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList(
                "http://localhost:4200"));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE"));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(Arrays.asList(
                "x-requested-with",
                "content-type",
                "Accept",
                "Authorization",
                "sentry-trace",
                "baggage"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);

        return source;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        String[] companiesEndpoints = {"/companies", "/companies/*"};
        String[] ideationEndpoint = {"/ideation", "/ideation/*"};
        String[] assessmentsEndpoints = {"/assessment", "/assessment/*", "/assessment/*/value-rating", "/assessment/*/viability-rating", "/assessment/*/customer-rating"};
        String[] teamsEndpoints = {"/teams", "/teams/*"};
        String[] userEndpoints = {"/users", "/users/*"};
        String[] projectEndpoints = {"/project", "/project/*", "/project-and-assessment"};
        String[] workspaceEndpoints = {"/workspace", "/workspace/*"};
        String[] tagEndpoints = {"/tag", "/tag/*"};


        http.authorizeHttpRequests((authorize) -> {
            try {
                authorize
                                .requestMatchers(companiesEndpoints).authenticated()
                                .requestMatchers(ideationEndpoint).authenticated()
                                .requestMatchers(assessmentsEndpoints).authenticated()
                                .requestMatchers(teamsEndpoints).authenticated()
                                .requestMatchers(userEndpoints).authenticated()
                                .requestMatchers(projectEndpoints).authenticated()
                                .requestMatchers(workspaceEndpoints).authenticated()
                                .requestMatchers(tagEndpoints).authenticated()
                                .requestMatchers(EndpointRequest.to("info")).hasAuthority("SCOPE_read:status")
                                .requestMatchers(EndpointRequest.to("health")).permitAll()
                                .and()
                                .oauth2ResourceServer((oauth2ResourceServer) ->
                                        oauth2ResourceServer.jwt(jwt -> jwt.decoder(jwtDecoder())));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        // Disable X-Frames on same origin to enable access to H2 in memory db console
        // https://stackoverflow.com/questions/26220083/h2-database-console-spring-boot-load-denied-by-x-frame-options
        http.headers().frameOptions().sameOrigin();

        return http.build();
    }
}

答案1

得分: 1

添加了如dur建议的调试日志后,问题浮出水面。
其实并没有问题,Spring实际上正在授权这些请求。

用Postman测试API,所有的响应都是200,所以 requestMatchers 做得很好。

然而,集成测试失败了!一番迅速的调查和调试发现了Spring Security 6中我不知道的一个变化 - 在控制器测试中需要添加csrf保护。

以下是一个我用于诊断和调试问题的失败单元测试:

    @Test
    @Transactional
    void updateValueRating() throws Exception {
        ValueRating newRating = ValueRatingBuilder.aValueRating()
                .withEarnableRevenue(50)
                .withRevenueAtRisk(50)
                .build();

        mockMvc.perform(put("/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/value-rating")
                        .content(mapper.writeValueAsString(newRating))
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.earnableRevenue", is(50)))
                .andExpect(jsonPath("$.revenueAtRisk", is(50)))
                .andExpect(jsonPath("$.overallValueRating", is(1.5)));
    }

添加静态打印调用 .andDo(print())) 后,Spring Security 的 CSRF 过滤器显示了一条消息:找到无效的CSRF令牌,然后在单元测试中回应了403,而使用HTTP探测则正常。

快速搜索得到 https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/csrf.html - 将.with(csrf()) 添加到测试中,问题就解决了。

mockMvc.perform(put("/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/value-rating")
                        .content(mapper.writeValueAsString(newRating))
                        .contentType(MediaType.APPLICATION_JSON)
                        .with(csrf()))

我没有意识到这在Spring Security 5和6之间有所改变,所以它让我措手不及。如果有人遇到类似的问题,我在这里发布这个答案。

英文:

Adding debug logging as dur suggested revealed the issue.
There was no issue, spring was actually authorising these requests.

Poking the API with postman responded with 200s across the board, so the requestMatchers did just fine.

Integration tests failed though! Some quick investigation and debugging revealed a change in Spring Security 6 that I wasn't aware of - the need to add csrf protection to controller tests.

Here is a failing unit test that I was using to diagnose and debug the issue:

    @Test
    @Transactional
    void updateValueRating() throws Exception {
        ValueRating newRating = ValueRatingBuilder.aValueRating()
                .withEarnableRevenue(50)
                .withRevenueAtRisk(50)
                .build();

        mockMvc.perform(put("/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/value-rating")
                        .content(mapper.writeValueAsString(newRating))
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.earnableRevenue", is(50)))
                .andExpect(jsonPath("$.revenueAtRisk", is(50)))
                .andExpect(jsonPath("$.overallValueRating", is(1.5)));
    }

Adding the static print call .andDo(print())) revealed a message from Spring Security's CSRF filter: Invalid CSRF token found, which then responded with a 403 in the unit test, where poking with HTTP worked fine.

A quick google yielded https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/csrf.html - adding .with(csrf()) to the test like so, fixed the issue.

mockMvc.perform(put("/assessment/2900b695-d344-4bec-b25d-524f6b22a93a/value-rating")
                        .content(mapper.writeValueAsString(newRating))
                        .contentType(MediaType.APPLICATION_JSON)
                        .with(csrf()))

I didn't realise that this changed between Spring Security 5 and 6 so it caught be by surprise. In case anyone hits a similar issue I'm posting this answer.

huangapple
  • 本文由 发表于 2023年2月16日 08:02:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/75466565.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定