问题描述
我目前有一个后端应用程序,它基于登录名/密码实现了一个非常简单的 Spring 安全性,必须添加到 http 标头中。
我还有一个前端,它使用 OKTA 作为提供程序并使用 JWT 令牌。
我现在想让专用于前端应用程序的端点使用 JWT 令牌系统,而所有其他端点都使用当前的登录/密码系统。
我可以使我的应用程序使用 OKTA 配置或登录名/密码配置工作,但我不能同时使用两者。
查看堆栈溢出的不同消息我已经实现了双重配置,但它始终是第一个应用的。第二个被简单地忽略,并且允许边界的端点而无需任何令牌或登录名/密码
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Configuration
@Order(1)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http
.authorizeRequests().antMatchers("/api/v1/end-point/**").authenticated()
.and().oauth2ResourceServer().jwt();
Okta.configureResourceServer401ResponseBody(http);
}
}
@Configuration
@Order(2)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Value("${http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
@Value("${http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
@Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.antMatcher("/api/v1/tools/**")
.authorizeRequests().anyRequest().authenticated();
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
@Override
@Bean
@Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
在这段代码中,即使我调用 /api/v1/tools 也不会使用配置 2 如果我删除配置 1,则应用配置 2。
你能帮我理解我做错了什么吗?
编辑 1:
在 Eleftheria Stein-Kousathana 的帮助和建议下,我更改了配置(并添加了 Swagger 白名单配置)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs","/swagger-resources/configuration/ui","/swagger-resources","/swagger-resources/configuration/security","/swagger-ui.html","/webjars/**"
};
@Configuration
@Order(1)
public static class SwaggerConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 1");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers(AUTH_WHITELIST))
.authorizeRequests(authz -> {
authz.anyRequest().permitAll();
});
}
}
@Configuration
@Order(2)
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 2");
http.cors();
http.csrf().disable();
http
.requestMatchers(matchers -> matchers.antMatchers("/api/v1/end-point/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated().and().oauth2ResourceServer().jwt();
} catch (Exception e) {
e.printstacktrace();
}
});
Okta.configureResourceServer401ResponseBody(http);
}
}
@Configuration
@Order(3)
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
@Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
@Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("Loading configuration 3");
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(initAuthenticationFilter())
.requestMatchers(matchers -> matchers.antMatchers("/api/**"))
.authorizeRequests(authz -> {
try {
authz.anyRequest().authenticated();
} catch (Exception e) {
e.printstacktrace();
}
});
}
private RequestHeaderAuthenticationFilter initAuthenticationFilter() {
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,apiKeyRequestHeaderName);
requestHeaderAuthenticationFilter.setContinueFilterChainOnUnsuccessfulAuthentication(false);
requestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
return requestHeaderAuthenticationFilter;
}
}
@Override
@Bean
@Primary
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
我觉得我离成功很近了
- 未通过身份验证时可以访问 Swaggers
- 对应于“/api/v1/end-point/**”的路由需要一个 JWT 令牌,否则我会收到 401 错误
- 对应于“/api/**”的路由需要登录名/密码,否则我会收到401错误
每次我在 swagger 下请求页面或调用我的 api 时,我的网络浏览器都会要求我输入登录名/密码。
如果我取消,我仍然可以在 Swagger UI 上导航并调用“/api/v1/end-point/**”。 每个登录名/密码即使在配置 3 中有效也会被拒绝。
如果我不填写登录名/密码并调用“/api/**”的任何路由,我会收到以下错误:
2021-07-23 14:49:16.642 [http-nio-8081-exec-9] INFO c.c.a.a.c.CorrelationIdLoggingAspect - Calling api.controller.endpoint.getActivities executed in 197ms.
2021-07-23 14:49:22.247 [http-nio-8081-exec-1] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [/secret] threw exception [Filter execution threw an exception] with root cause
java.lang.StackOverflowError: null
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(UnkNown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
at jdk.internal.reflect.GeneratedMethodAccessor220.invoke(UnkNown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:205)
at com.sun.proxy.$Proxy236.authenticate(UnkNown Source)
at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:195)
at org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter$AuthenticationManagerDelegator.authenticate(WebSecurityConfigurerAdapter.java:501)
解决方法
如果我正确理解您的程序草图和描述,让我尝试总结一下。您的应用程序寻求支持以下内容:
- 向公众提供 swagger UI 并允许浏览 API 定义。
- 将经过身份验证的 API 端点(以
/api/v1/end-point
为前缀)与来自其他客户端的 Okta 提供的 JWT 一起使用(不是 swagger)。 - 以用户名/密码作为标头,通过 swagger 使用经过身份验证的 API 端点(以
/api
为前缀,但不是/api/v1/end-point
)。
注意:我不会在这里介绍如何将 Okta 配置为提供程序,也不会配置 swagger。如果这些步骤没有正确完成,您可能仍然会遇到问题。
就 Spring Security 而言,我认为您的主要问题是由于您似乎没有为基于标头的配置配置身份验证提供程序。这通常通过 UserDetailsService
完成(请参阅 UserDetailsService 部分):
@Bean
public UserDetailsService userDetailsService() {
// @formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// @formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
这显然不是用于生产的示例。但重要的一点是,您必须为 Spring Security 提供一种方法来确定凭证是否有效。无论是用户的用户名/密码,还是 API 客户端的 appId/apiKey,都会通过 principal
查找 UserDetailsService
(参见 Authentication),然后通过 AuthenticationProvider
来验证凭据{1}}。
遗憾的是,内置 RequestHeaderAuthenticationFilter
建立在不同类型的提供程序之上,该提供程序假定您已通过预身份验证,因此与用户名/密码身份验证不兼容。虽然您可以通过将一种类型的提供程序适应另一种类型来解决此问题,但将 UsernamePasswordAuthenticationFilter
适应您的用例更为直接(至少出于示例目的)。例如:
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
@Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
@Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
@Override
protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException,ServletException {
super.successfulAuthentication(request,response,chain,authResult);
chain.doFilter(request,response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request,authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
如果您有兴趣让这种感觉更加内置,请查看 custom DSLs 上的文档部分。
我还建议您覆盖 configure(WebSecurity web)
中的 WebSecurityConfigurerAdapter
方法以执行您的 permitAll
并将配置压缩为两个,以及消除 /api/**
模式,以便默认情况下,您的整个应用程序都是安全的。这是一个完整的示例(省略任何 Okta 特定的代码),它也演示了 Spring Security lambda DSL 的正确用法:
@Configuration
public class SecurityConfiguration {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs","/swagger-resources/configuration/ui","/swagger-resources","/swagger-resources/configuration/security","/swagger-ui.html","/webjars/**"
};
@Order(1)
@EnableWebSecurity
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// @formatter:on
}
}
@Order(2)
@EnableWebSecurity
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Value("${algo.http.auth-app-id-header-name}")
private String appIdRequestHeaderName;
@Value("${algo.http.auth-api-key-header-name}")
private String apiKeyRequestHeaderName;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.addFilterAt(usernamePasswordAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
// @formatter:on
}
private UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception {
UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter() {
@Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
@Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
@Override
protected void successfulAuthentication(HttpServletRequest request,ServletException {
super.successfulAuthentication(request,authResult);
chain.doFilter(request,response);
}
};
usernamePasswordAuthenticationFilter.setAuthenticationManager(authenticationManager());
usernamePasswordAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernamePasswordAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernamePasswordAuthenticationFilter.setPostOnly(false);
usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler((request,authentication) -> {
// Do nothing
});
return usernamePasswordAuthenticationFilter;
}
@Bean
public UserDetailsService userDetailsService() {
// @formatter:off
UserDetails userDetails = User.builder()
.username("api-client")
.password("{noop}my-api-key")
.roles("USER")
.build();
// @formatter:on
return new InMemoryUserDetailsManager(userDetails);
}
}
}
最后一点: 一个警告是我包括禁用 CSRF,你已经完成了。如果您不打算在具有会话的 Web 浏览器中使用此应用程序,则这只是一个 reasonable thing to do。由于我将两个配置都标记为无状态(您的 Okta+JWT 示例不是),这似乎是合理的。但是,大多数情况下,您确实不想禁用 CSRF 保护,尤其是当原因是“我不知道如何让我的 UI 应用程序在启用 CSRF 的情况下工作。”
,首先非常感谢您的帮助。 我花时间回复是因为我想了解您的回答。
您对我试图实现的草图的描述是正确的。 通过您的配置,我现在无需任何登录名/密码即可访问 Swagger。
第一个配置(OKTA)工作正常,我认为最后一个(登录名/密码)也可以。
当我尝试访问路由时,我现在面临最后一个错误 受登录名和密码保护。 我面临一个问题,Spring 抛出“org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken”异常。
我正在寻求解决这个问题,我认为之后一切都会好起来的。
让我谦虚地指出setter方法:
requestHeaderAuthenticationFilter.setPrincipalRequestHeader(appIdRequestHeaderName);
requestHeaderAuthenticationFilter.setCredentialsRequestHeader(apiKeyRequestHeaderName);
不可访问,我一直通过构造函数设置它们。
RequestHeaderAuthenticationFilter requestHeaderAuthenticationFilter = new RequestHeaderAuthenticationFilter(appIdRequestHeaderName,apiKeyRequestHeaderName)
,
非常感谢您的所有回答。 感谢您的帮助,我们找到了解决方案。
这里是帮助每个需要和我们做同样事情的人的最终代码。
安全配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] AUTH_WHITELIST = {
"/v2/api-docs","/webjars/**"
};
@Order(1)
@Configuration
public static class OauthOktaConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/v1/end-point/**")
.authorizeRequests((authz) -> authz.anyRequest().authenticated())
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
Okta.configureResourceServer401ResponseBody(http);
}
}
@Order(2)
@Configuration
public static class StandardSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Value("${http.app-id-header-name}")
private String appIdRequestHeaderName;
@Value("${http.api-key-header-name}")
private String apiKeyRequestHeaderName;
private final AuthenticationManager authenticationManager;
@Autowired
public StandardSecurityConfigurationAdapter(AuthenticationManager authenticationManager) {
super();
this.authenticationManager = authenticationManager;
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(AUTH_WHITELIST);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterAt(initAuthenticationFilter(),UsernameRequestHeaderAuthenticationFilter.class)
.authorizeRequests((authorizeRequests) ->
authorizeRequests
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(withDefaults())
.csrf(CsrfConfigurer::disable);
}
private UsernameRequestHeaderAuthenticationFilter initAuthenticationFilter() throws Exception {
UsernameRequestHeaderAuthenticationFilter usernameRequestHeaderAuthenticationFilter = new UsernameRequestHeaderAuthenticationFilter();
usernameRequestHeaderAuthenticationFilter.setAuthenticationManager(authenticationManager);
usernameRequestHeaderAuthenticationFilter.setUsernameParameter(appIdRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setPasswordParameter(apiKeyRequestHeaderName);
usernameRequestHeaderAuthenticationFilter.setRequiresAuthenticationRequestMatcher(AnyRequestMatcher.INSTANCE);
usernameRequestHeaderAuthenticationFilter.setPostOnly(false);
usernameRequestHeaderAuthenticationFilter.setAuthenticationSuccessHandler((request,authentication) -> {
// Do nothing
});
return usernameRequestHeaderAuthenticationFilter;
}
}
}
身份验证过滤器:
public class UsernameRequestHeaderAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
protected String obtainUsername(HttpServletRequest request) {
return request.getHeader(getUsernameParameter());
}
@Override
protected String obtainPassword(HttpServletRequest request) {
return request.getHeader(getPasswordParameter());
}
@Override
protected void successfulAuthentication(HttpServletRequest request,ServletException {
super.successfulAuthentication(request,authResult);
chain.doFilter(request,response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,AuthenticationException failed) throws IOException,ServletException {
super.unsuccessfulAuthentication(request,failed);
}
}
我们还调整了 AuthenticationManager 以使用 UserAuthorities
再次感谢大家