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