Spring Auth Server不会通过传递的Bearer令牌将HTTP请求授权给受保护的端点

tv6aics1  于 2023-06-22  发布在  Spring
关注(0)|答案(2)|浏览(196)

关键点

我正在Sping Boot 3.1.0中开发一个应用,使用Spring Authorization Server实现一个OAuth 2.1服务器,用于使用PKCE的Auth Code Flow。
OAuth工作得很好,但是当我继续处理服务API部分并保护它时,我的应用程序拒绝授权传入的HTTP请求到API端点,并在头中传递Bearer令牌。

主要问题

如何在此单模块Web服务器中使用Bearer令牌身份验证来保护我的REST API端点?
这可能吗?我该怎么办?

测试用例

Spring应用程序日志:

# I try to exchange OAuth code for an access token
23:16:44.991 [nio-8080-exec-4] o.s.security.web.FilterChainProxy        : Securing POST /oauth2/token
23:16:45.059 [nio-8080-exec-4] o.s.a.w.OAuth2ClientAuthenticationFilter : Set SecurityContextHolder authentication to OAuth2ClientAuthenticationToken

# And then I try to use this token for default OAuth provided endpoints (result: 200 OK)
23:17:16.212 [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Securing GET /userinfo
23:17:16.217 [nio-8080-exec-6] o.s.web.client.RestTemplate              : HTTP POST http://localhost:8080/oauth2/introspect
23:17:16.221 [nio-8080-exec-6] o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
23:17:16.222 [nio-8080-exec-6] o.s.web.client.RestTemplate              : Writing [{token=[2Jd2M-Pq3Cx8We9gKVpfosvAnNGjprCJoyA6-gHOH3t2_cbpVaGsmGkgJ1n9wzam_kvvL4cthCUwSCNRWrfm_uGZJUtFWJjL_jaaKla0p37MDwkPbrGGhoJOGLeGDSrC]}] with org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter

# Also for opaque token introspection (result: 200 OK)
23:17:16.227 [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Securing POST /oauth2/introspect
23:17:16.294 [nio-8080-exec-5] o.s.a.w.OAuth2ClientAuthenticationFilter : Set SecurityContextHolder authentication to OAuth2ClientAuthenticationToken
23:17:16.307 [nio-8080-exec-6] o.s.web.client.RestTemplate              : Response 200 OK
23:17:16.308 [nio-8080-exec-6] o.s.web.client.RestTemplate              : Reading to [java.util.Map<java.lang.String, java.lang.Object>]
23:17:16.320 [nio-8080-exec-6] .s.r.a.OpaqueTokenAuthenticationProvider : Authenticated token
23:17:16.320 [nio-8080-exec-6] .s.r.w.a.BearerTokenAuthenticationFilter : Set SecurityContextHolder to BearerTokenAuthentication [Principal=org.springframework.security.oauth2.server.resource.introspection.OAuth2IntrospectionAuthenticatedPrincipal@49bc387c, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[SCOPE_openid, SCOPE_user:read, SCOPE_user:write]]

# After that I try to send request to my REST API controller (result: 401 + redirect)
23:17:43.504 [nio-8080-exec-8] o.s.security.web.FilterChainProxy        : Securing GET /api/user/get
23:17:43.504 [nio-8080-exec-8] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
23:17:43.504 [nio-8080-exec-8] o.s.s.w.session.SessionManagementFilter  : Request requested invalid session id 9FC445DE02E5AA86CF6C7D898290112F
23:17:43.505 [nio-8080-exec-8] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to team.flow.server.api.controller.UserApiController#getUser(Long, Authentication)
23:17:43.505 [nio-8080-exec-8] o.s.s.w.s.HttpSessionRequestCache        : Saved request http://localhost:8080/api/user/get?continue to session
23:17:43.505 [nio-8080-exec-8] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/auth/sign-in
23:17:43.512 [nio-8080-exec-9] o.s.security.web.FilterChainProxy        : Securing GET /auth/sign-in
23:17:43.512 [nio-8080-exec-9] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
23:17:43.512 [nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to team.flow.server.auth.controller.AuthFrontController#handleDefaultRequest()
23:17:43.512 [nio-8080-exec-9] o.s.security.web.FilterChainProxy        : Secured GET /auth/sign-in
23:17:43.512 [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet        : GET "/auth/sign-in", parameters={}
23:17:43.512 [nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to team.flow.server.auth.controller.AuthFrontController#handleDefaultRequest()
23:17:43.513 [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet        : Completed 200 OK

最后一个请求的Postman控制台屏幕截图:

源代码

安全配置:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private static final List<String> FLOW_OAUTH2_SCOPES = List.of(
            "openid",
            "user:read", "user:write"
    );

    private final UserRepository userRepository;
    private final FlowAuthenticationHandler authenticationHandler;

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());

        return http
                .exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/auth"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        ))
                .oauth2ResourceServer((resourceServer) -> resourceServer
                        .opaqueToken(Customizer.withDefaults()))
                .build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/error", "/flow-web/**", "/favicon.ico").permitAll()
                        .requestMatchers("/auth/complete").authenticated()
                        .requestMatchers("/auth/**", "/logout").permitAll()
                        .requestMatchers("/oauth2/code").permitAll()
                        .requestMatchers(HttpMethod.GET, "/api/user/**").hasAuthority("SCOPE_user:read")
                        .requestMatchers(HttpMethod.POST, "/api/user/**").hasAuthority("SCOPE_user:write")
                        .anyRequest().authenticated())
                .sessionManagement(sessionManagement -> sessionManagement
                        .maximumSessions(1))
                .formLogin(formLogin -> formLogin
                        .loginPage("/auth/sign-in")
                        .loginProcessingUrl("/auth/sign-in")
                        .successHandler(authenticationHandler)
                        .failureHandler(authenticationHandler)
                        .usernameParameter("email")
                        .passwordParameter("password"))
                .logout(logout -> logout
                        .deleteCookies("JSESSIONID")
                        .logoutUrl("/logout")
                        .logoutSuccessUrl("/auth"))
                .build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new FlowUserDetailsService(userRepository);
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient flowClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("")
                .clientName("")
                .clientSecret("")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://localhost:8080/oauth2/code")
                .postLogoutRedirectUri("http://localhost:8080/auth")
                .scopes(scopes -> scopes.addAll(FLOW_OAUTH2_SCOPES))
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(false)
                        .requireProofKey(true)
                        .build())
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.REFERENCE) // yes, I use opaque tokens here
                        .authorizationCodeTimeToLive(Duration.ofSeconds(30))
                        .accessTokenTimeToLive(Duration.ofDays(3))
                        .refreshTokenTimeToLive(Duration.ofDays(14))
                        .reuseRefreshTokens(false)
                        .build())
                .build();

        return new InMemoryRegisteredClientRepository(flowClient);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityContextRepository securityContextRepository() {
        return new HttpSessionSecurityContextRepository();
    }

REST API控制器:

@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserApiController {

    private final UserApiService userApiService;

    @GetMapping("/get")
    public ResponseEntity<?> getUser(@RequestParam(name = "id", required = false) Long userId) throws ApiException {
        if (userId < 1) {
            return ResponseEntity.badRequest().build();
        }

        User user = userApiService.getUser(userId);
        return ResponseEntity.ok(UserModel.constructFrom(user));
    }

    @GetMapping("/meta/get")
    public ResponseEntity<?> getUserMeta(@RequestParam(name = "id", required = false) Long userId) throws ApiException {
        if (userId < 1) {
            return ResponseEntity.badRequest().build();
        }

        UserMeta userMeta = userApiService.getUserMeta(userId);
        return ResponseEntity.ok(UserMetaModel.constructFrom(userMeta));
    }

}

应用程序配置:

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://127.0.0.1/${DATABASE}
    username: ${USERNAME}
    password: ${PASSWORD}

  jpa:
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect

    hibernate:
      ddl-auto: update

    open-in-view: false

  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: http://localhost:8080/oauth2/introspect
          client-id: my_client_id
          client-secret: my_client_secret

logging:
  level:
    root: INFO
    '[org.springframework.web]': DEBUG
    '[org.springframework.security]': DEBUG
    '[org.springframework.security.oauth2]': DEBUG
    org.springframework.security.web.FilterChainProxy: DEBUG

server:
  servlet:
    session:
      cookie:
        same-site: lax

  error:
    whitelabel:
      enabled: false
    path: /error

maven项目配置的一部分:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.1.0</version>
        <relativePath/>
    </parent>

    <dependencies>
        <!-- Spring Boot Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <!-- Thymeleaf Extras: Spring Security 5 -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
            <version>3.1.1.RELEASE</version>
            <scope>compile</scope>
        </dependency>

        <!-- PostgreSQL JDBC Driver -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
n7taea2i

n7taea2i1#

如何通过承载令牌认证保护我的REST API端点?

关于resource server configuration

在已经包含客户端conf的应用中如何操作?

授权服务器端点和暴露Thymeleaf页面的端点都需要OAuth2客户端配置,其中请求使用会话进行保护(并需要CSRF保护)。
要使用访问令牌保护REST API端点(并且不使用会话、CSRF保护、登录或注销),请定义专用于资源服务器端点的第三个安全过滤器链。
要保持您的defaultSecurityFilterChain与OAuth2客户端配置为默认值,请将其顺序更改为@Order(3),并插入一个resourceServerFilterChain@Order(2)和一个安全匹配器,如http.securityMatcher("/api/**")*,以便它只匹配REST API路由,并让defaultSecurityFilterChain处理所有未被任何安全过滤链拦截的请求。
您可能会参考my tutorials来获取资源服务器配置和同时具有OAuth2客户端和OAuth2资源服务器配置的应用程序(但没有提到Spring的授权服务器,因为我使用的是其他解决方案作为OpenID Provider)。

im9ewurl

im9ewurl2#

您应该注意到,在PKCE方法中,没有使用客户端机密,因为PKCE的目的是出于安全问题的考虑,不在前端应用程序中存储任何客户端机密。通过使用PKCE,每次客户端向授权服务器发送请求以获得授权码时,生成新的哈希码来验证客户端,而不是使用客户端秘密。你可以在互联网上搜索challenge-code和code-verifier来获得更多关于hash码的信息,hash码是用来验证客户端而不是client-secret的。

相关问题