在 Spring 授权服务器(0.2.3+)中支持并发全栈 MVC(会话)身份验证以及无状态 JWT 身份验证
Supporting concurrent full-stack MVC (session) authentication as well as stateless JWT authentication in Spring Authorization Server (0.2.3+)
在创建Authorization server时,我的AS比较简单。它支持:
- 用户注册 (WebMVC)
- 表单登录(WebMVC)
- 忘记密码(WebMVC)
- Managing RegisteredClient (WebMVC) - 一个可以管理他们的 API 客户端的地方,这些客户端交换访问令牌以访问其他资源服务器。
我还有一些 API @RestController 端点;但是,我观察到我无法向他们发出经过 JWT 身份验证的请求,因为身份验证过程对这些请求不起作用,因为我正在获取基于会话的 .formlogin() 样式显示的登录页面内容,并以 200 呈现好的,而不是我期望的——401 或 403 或 200 好的,但有 RESTful application/json 结构化答案。
如何同时支持基于会话的 WebMVC 流程以及依赖 JWT 身份验证的 REST 控制器端点?
import org.junit.jupiter.api.Order;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DefaultSecurityConfiguration {
public static final int FILTER_PRECEDENCE = -5;
@Bean
@Order(FILTER_PRECEDENCE)
SecurityFilterChain defaultSecurityFilterChain(final HttpSecurity http) throws Exception {
return http.authorizeRequests(authorizeRequests ->
authorizeRequests
.mvcMatchers("/favicon.ico", "/favicon-16x16.png", "/favicon-32x32.png", "/mstile-150x150.png", "/apple-touch-icon.png", "/", "/assets/**", "/login/**", "/webjars/**", "/register", "/register-form", "/actuator/health", "/reset-password", "/reset-password-2", "/error")
.permitAll()
).formLogin(oauth2 ->
oauth2
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=1")
.permitAll()
)
.build();
}
}
这是我的 AuthorizationServerConfiguration 的过滤器链配置:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(final HttpSecurity http) throws Exception {
final OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
final RequestMatcher endpointsMatcher = authorizationServerConfigurer
.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}
并且,对于傻笑,这是我希望工作的示例 REST 端点:
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/oauth/account")
public class AccountController {
public static final String ACCOUNT_ID_IS_REQUIRED = "Account Id is required.";
private final UserDetailRepository userDetailRepository;
private final AccountService accountService;
/**
* Return List of all account and clients of logged in user
*
* @param authentication
* @return
*/
@GetMapping
public List<AccountResponseDTO> findAllUserAccounts(final Authentication authentication) {
final User user = this.userDetailRepository.findByUsername(authentication.getName()).get();
return this.accountService.findAccountsByUserId(user.getId());
}
除了简单之外,我还很好奇为什么在 example for a custom-consent server 中两个不同的安全过滤器中重复声明了 .formLogin()。我一直在玩这个,我对安全过滤器覆盖有点难过。
如果我在 .formLogin() 中有差异,我会观察到默认值或间歇性故障。如果我在更高值的@Order 上删除 .formLogin,则登录不起作用,但我得到了一个工作的 REST 端点。如果我在两个地方都有 .formLogin() ,我就有了一个很好的 WebMVC 方面;然而,我的 REST 控制器身份验证仅返回 JWT 的承载值而不是身份验证主体。这里发生了奇怪的事情——任何帮助将不胜感激!
考虑配置 3 个安全过滤器链。第一个用于身份验证,第二个用于具有承载令牌的调用,最后一个用于所有其他 MVC 请求:
@Bean
@Order(1)
@SuppressWarnings("unused")
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
return http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer)
.oidc(oidc -> oidc
.clientRegistrationEndpoint(Customizer.withDefaults())
)
.and()
.formLogin(Customizer.withDefaults()).build();
}
@Bean
@Order(2)
@SuppressWarnings("unused")
public SecurityFilterChain resourceServerOauthFilterChain(HttpSecurity http) throws Exception {
http
.requestMatcher(request -> {
String headerValue = request.getHeader("Authorization");
return headerValue != null && headerValue.startsWith("Bearer");
})
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer().jwt(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(3)
@SuppressWarnings("unused")
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests(authorizeRequests ->
authorizeRequests
.mvcMatchers("/favicon.ico", "/favicon-16x16.png", "/favicon-32x32.png", "/mstile-150x150.png", "/apple-touch-icon.png", "/", "/assets/**", "/login/**", "/webjars/**", "/register", "/register-form", "/actuator/health", "/reset-password", "/reset-password-2", "/error")
.permitAll().anyRequest().authenticated()
)
.formLogin(oauth2 ->
oauth2
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=1")
.permitAll()
)
.build();
}
关于你的第二个问题,我认为每个过滤器链都有自己的过滤器集。由于登录也是作为过滤器实现的,因此应该将其添加到相应的过滤器链中。
更新:似乎您对订单注释的导入不正确:
导入 org.junit.jupiter.api.Order;
在创建Authorization server时,我的AS比较简单。它支持:
- 用户注册 (WebMVC)
- 表单登录(WebMVC)
- 忘记密码(WebMVC)
- Managing RegisteredClient (WebMVC) - 一个可以管理他们的 API 客户端的地方,这些客户端交换访问令牌以访问其他资源服务器。
我还有一些 API @RestController 端点;但是,我观察到我无法向他们发出经过 JWT 身份验证的请求,因为身份验证过程对这些请求不起作用,因为我正在获取基于会话的 .formlogin() 样式显示的登录页面内容,并以 200 呈现好的,而不是我期望的——401 或 403 或 200 好的,但有 RESTful application/json 结构化答案。
如何同时支持基于会话的 WebMVC 流程以及依赖 JWT 身份验证的 REST 控制器端点?
import org.junit.jupiter.api.Order;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class DefaultSecurityConfiguration {
public static final int FILTER_PRECEDENCE = -5;
@Bean
@Order(FILTER_PRECEDENCE)
SecurityFilterChain defaultSecurityFilterChain(final HttpSecurity http) throws Exception {
return http.authorizeRequests(authorizeRequests ->
authorizeRequests
.mvcMatchers("/favicon.ico", "/favicon-16x16.png", "/favicon-32x32.png", "/mstile-150x150.png", "/apple-touch-icon.png", "/", "/assets/**", "/login/**", "/webjars/**", "/register", "/register-form", "/actuator/health", "/reset-password", "/reset-password-2", "/error")
.permitAll()
).formLogin(oauth2 ->
oauth2
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=1")
.permitAll()
)
.build();
}
}
这是我的 AuthorizationServerConfiguration 的过滤器链配置:
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authServerSecurityFilterChain(final HttpSecurity http) throws Exception {
final OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
final RequestMatcher endpointsMatcher = authorizationServerConfigurer
.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.apply(authorizationServerConfigurer);
return http.formLogin(Customizer.withDefaults()).build();
}
并且,对于傻笑,这是我希望工作的示例 REST 端点:
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/oauth/account")
public class AccountController {
public static final String ACCOUNT_ID_IS_REQUIRED = "Account Id is required.";
private final UserDetailRepository userDetailRepository;
private final AccountService accountService;
/**
* Return List of all account and clients of logged in user
*
* @param authentication
* @return
*/
@GetMapping
public List<AccountResponseDTO> findAllUserAccounts(final Authentication authentication) {
final User user = this.userDetailRepository.findByUsername(authentication.getName()).get();
return this.accountService.findAccountsByUserId(user.getId());
}
除了简单之外,我还很好奇为什么在 example for a custom-consent server 中两个不同的安全过滤器中重复声明了 .formLogin()。我一直在玩这个,我对安全过滤器覆盖有点难过。
如果我在 .formLogin() 中有差异,我会观察到默认值或间歇性故障。如果我在更高值的@Order 上删除 .formLogin,则登录不起作用,但我得到了一个工作的 REST 端点。如果我在两个地方都有 .formLogin() ,我就有了一个很好的 WebMVC 方面;然而,我的 REST 控制器身份验证仅返回 JWT 的承载值而不是身份验证主体。这里发生了奇怪的事情——任何帮助将不胜感激!
考虑配置 3 个安全过滤器链。第一个用于身份验证,第二个用于具有承载令牌的调用,最后一个用于所有其他 MVC 请求:
@Bean
@Order(1)
@SuppressWarnings("unused")
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
return http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer)
.oidc(oidc -> oidc
.clientRegistrationEndpoint(Customizer.withDefaults())
)
.and()
.formLogin(Customizer.withDefaults()).build();
}
@Bean
@Order(2)
@SuppressWarnings("unused")
public SecurityFilterChain resourceServerOauthFilterChain(HttpSecurity http) throws Exception {
http
.requestMatcher(request -> {
String headerValue = request.getHeader("Authorization");
return headerValue != null && headerValue.startsWith("Bearer");
})
.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.oauth2ResourceServer().jwt(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(3)
@SuppressWarnings("unused")
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
return http.authorizeRequests(authorizeRequests ->
authorizeRequests
.mvcMatchers("/favicon.ico", "/favicon-16x16.png", "/favicon-32x32.png", "/mstile-150x150.png", "/apple-touch-icon.png", "/", "/assets/**", "/login/**", "/webjars/**", "/register", "/register-form", "/actuator/health", "/reset-password", "/reset-password-2", "/error")
.permitAll().anyRequest().authenticated()
)
.formLogin(oauth2 ->
oauth2
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=1")
.permitAll()
)
.build();
}
关于你的第二个问题,我认为每个过滤器链都有自己的过滤器集。由于登录也是作为过滤器实现的,因此应该将其添加到相应的过滤器链中。
更新:似乎您对订单注释的导入不正确: 导入 org.junit.jupiter.api.Order;