Spring 使用 Security OAuth2 启动 - 如何通过 Web 登录表单使用资源服务器?
Spring Boot with Security OAuth2 - how to use resource server with web login form?
我有 Spring Boot (1.2.1.RELEASE) 应用程序服务 OAuth2 (2.0.6.RELEASE) 一个应用程序实例中的授权和资源服务器。它使用自定义 UserDetailsService
实现,利用 MongoTemplate
搜索 MongoDB 中的用户。在 /oauth/token
上使用 grant_type=password
进行身份验证就像一个魅力,以及在调用特定资源时使用 Authorization: Bearer {token}
header 进行授权。
现在我想向服务器添加简单的 OAuth 确认对话框,这样我就可以进行身份验证和授权,例如Swagger UI 在 api-docs 中调用受保护的资源。这是我到目前为止所做的:
@Configuration
@SessionAttributes("authorizationRequest")
class OAuth2ServerConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/oauth/confirm_access").setViewName("authorize");
}
@Configuration
@Order(2)
protected static class LoginConfig extends WebSecurityConfigurerAdapter implements ApplicationEventPublisherAware {
@Autowired
UserDetailsService userDetailsService
@Autowired
PasswordEncoder passwordEncoder
ApplicationEventPublisher applicationEventPublisher
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider()
provider.passwordEncoder = passwordEncoder
provider.userDetailsService = userDetailsService
return provider
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(authenticationManagerBean())
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//return super.authenticationManagerBean()
ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
return providerManager
}
@Bean
public PasswordEncoder passwordEncoder() {
new BCryptPasswordEncoder(5)
}
}
@Configuration
@EnableResourceServer
protected static class ResourceServer extends ResourceServerConfigurerAdapter {
@Value('${oauth.resourceId}')
private String resourceId
@Autowired
@Qualifier('authenticationManagerBean')
private AuthenticationManager authenticationManager
@Override
public void configure(HttpSecurity http) throws Exception {
http.setSharedObject(AuthenticationManager.class, authenticationManager)
http.csrf().disable()
http.httpBasic().disable()
http.formLogin().loginPage("/login").permitAll()
//http.authenticationProvider(daoAuthenticationProvider())
http.anonymous().and()
.authorizeRequests()
.antMatchers('/login/**').permitAll()
.antMatchers('/uaa/register/**').permitAll()
.antMatchers('/uaa/activate/**').permitAll()
.antMatchers('/uaa/password/**').permitAll()
.antMatchers('/uaa/account/**').hasAuthority('ADMIN')
.antMatchers('/api-docs/**').permitAll()
.antMatchers('/admin/**').hasAuthority('SUPERADMIN')
.anyRequest().authenticated()
//http.sessionManagement().sessionCreationPolicy(STATELESS)
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceId)
resources.authenticationManager(authenticationManager)
}
}
@Configuration
@EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Value('${oauth.clientId}')
private String clientId
@Value('${oauth.secret:}')
private String secret
@Value('${oauth.resourceId}')
private String resourceId
@Autowired
@Qualifier('authenticationManagerBean')
private AuthenticationManager authenticationManager
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
return new JwtAccessTokenConverter();
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.checkTokenAccess("permitAll()")
oauthServer.allowFormAuthenticationForClients()
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter())
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(secret)
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("USER", "ADMIN")
.scopes("read", "write", "trust")
.resourceIds(resourceId)
}
}
}
主要问题是我无法同时创建(header 中的 Web 登录表单和 OAuth2 授权令牌)运行。如果 ResourceServer
获得更高优先级,则 OAuth2 令牌授权有效,但我无法使用 Web 表单登录。另一方面,如果我将更高的优先级设置为 LoginConfig
class,则 OAuth2 令牌授权将停止工作。
案例研究:登录表单有效,OAuth2 令牌授权无效
我发现在那种情况下问题是由 non-registered OAuth2AuthenticationProcessingFilter
引起的。我尝试在 ResourceServer.configure(HttpSecurity http)
方法中手动注册它,但它不起作用 - 我可以在 FilterChain 列表中看到过滤器,但它没有被触发。这不是修复它的好方法,因为在 ResourceServer 初始化期间还有很多其他魔术,所以我转向第二种情况。
案例研究:登录表单不起作用,OAuth2 令牌授权有效
在这种情况下,主要问题是默认情况下 UsernamePasswordAuthenticationFilter
无法找到正确配置的 AuthenticationProvider
实例(在 ProviderManager
中)。当我尝试通过以下方式手动添加它时:
http.authenticationProvide(daoAuthenticationProvider())
它得到一个,但在这种情况下没有 AuthenticationEventPublisher
定义,并且成功的身份验证无法发布到其他组件。事实上,在下一次迭代中,它会被 AnonymousAuthenticationToken
取代。这就是为什么我尝试手动定义 AuthenticationManager
实例,其中包含 DaoAuthenticationProvider
:
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//return super.authenticationManagerBean()
ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
return providerManager
}
我认为它会起作用,但是向注册过滤器提供 AuthenticationManager
实例存在不同的问题。事实证明,每个过滤器都有 authenticationManager
使用 sharedObjects
组件手动注入:
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
这里的问题是不能保证您有一个正确的实例集,因为有一个简单的 HashMap (check it on GitHub) 用于存储特定的共享 object 并且它可以任意更改时间。我尝试将其设置为:
http.setSharedObject(AuthenticationManager.class, authenticationManager)
但在我到达它正在阅读的地方之前,它已经被默认实现所取代。我用调试器检查了它,看起来每个新过滤器都有一个新的身份验证管理器实例。
我的问题是:我做得对吗?如何将资源服务器集成在一个应用程序中并使用登录表单(OAuth2 对话框)工作来设置授权服务器?也许可以用一种不同的、更简单的方式来完成。如果有任何帮助,我将不胜感激。
我认为您不应该尝试在您的 ResourceServerConfigurerAdapter
中设置表单登录或 http basic,如果您已经在其他 WebSecurityConfigurerAdapter
中设置它们,当然也不应该(您这样做是因为默认情况下它们是打开的)。它可能有效,但 OAuth2 保护资源和 UI 的身份验证和访问决策非常不同,我建议您将它们分开(因为它们在 github 的所有示例中)。如果您接受建议并继续使用您已经定义的组件,那么正确执行此操作的关键是要知道过滤器链是按顺序尝试的,并且第一个匹配的将获胜,因此只有其中一个会作用于任何给定的要求。您必须将请求匹配器放在两个链中(或至少是顺序最低的那个),并确保它们不重叠。
如果您使用配置了不同安全性的不同端点怎么办?
对于上面的示例,/uaa/** 的所有内容都使用 WebSecurityConfigurerAdapter 进行保护,而 /api-docs/** 使用 ResourceServerConfigurerAdapter 进行保护。
那样的话,过滤链还会冲突吗?
这里是问题的解决方案。看看这个示例 Groovy
class:
@Configuration
@EnableResourceServer
class ResourceServer extends ResourceServerConfigurerAdapter {
@Value('${oauth.resourceId}')
private String resourceId
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
http.httpBasic().disable()
http.requestMatchers().antMatchers('/admin/**', '/uaa/**')
.and().authorizeRequests()
.antMatchers('/uaa/authenticated/**').authenticated()
.antMatchers('/uaa/register/**').permitAll()
.antMatchers('/uaa/activate/**').permitAll()
.antMatchers('/uaa/password/**').permitAll()
.antMatchers('/uaa/auth/**').permitAll()
.antMatchers('/uaa/account/**').hasAuthority('ADMIN')
.antMatchers('/admin/**').hasAuthority('ADMIN')
.anyRequest().authenticated()
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceId);
}
}
基本上,要运行 OAuth2.0 身份验证与 Web 表单身份验证并行,您必须将
http.requestMatchers().antMatchers('/path/1/**', '/path/2/**')
配置 class。我之前的配置遗漏了这个重要的部分,所以只有OAuth2.0参与了认证过程。
我有 Spring Boot (1.2.1.RELEASE) 应用程序服务 OAuth2 (2.0.6.RELEASE) 一个应用程序实例中的授权和资源服务器。它使用自定义 UserDetailsService
实现,利用 MongoTemplate
搜索 MongoDB 中的用户。在 /oauth/token
上使用 grant_type=password
进行身份验证就像一个魅力,以及在调用特定资源时使用 Authorization: Bearer {token}
header 进行授权。
现在我想向服务器添加简单的 OAuth 确认对话框,这样我就可以进行身份验证和授权,例如Swagger UI 在 api-docs 中调用受保护的资源。这是我到目前为止所做的:
@Configuration
@SessionAttributes("authorizationRequest")
class OAuth2ServerConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/oauth/confirm_access").setViewName("authorize");
}
@Configuration
@Order(2)
protected static class LoginConfig extends WebSecurityConfigurerAdapter implements ApplicationEventPublisherAware {
@Autowired
UserDetailsService userDetailsService
@Autowired
PasswordEncoder passwordEncoder
ApplicationEventPublisher applicationEventPublisher
@Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider()
provider.passwordEncoder = passwordEncoder
provider.userDetailsService = userDetailsService
return provider
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.parentAuthenticationManager(authenticationManagerBean())
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//return super.authenticationManagerBean()
ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
return providerManager
}
@Bean
public PasswordEncoder passwordEncoder() {
new BCryptPasswordEncoder(5)
}
}
@Configuration
@EnableResourceServer
protected static class ResourceServer extends ResourceServerConfigurerAdapter {
@Value('${oauth.resourceId}')
private String resourceId
@Autowired
@Qualifier('authenticationManagerBean')
private AuthenticationManager authenticationManager
@Override
public void configure(HttpSecurity http) throws Exception {
http.setSharedObject(AuthenticationManager.class, authenticationManager)
http.csrf().disable()
http.httpBasic().disable()
http.formLogin().loginPage("/login").permitAll()
//http.authenticationProvider(daoAuthenticationProvider())
http.anonymous().and()
.authorizeRequests()
.antMatchers('/login/**').permitAll()
.antMatchers('/uaa/register/**').permitAll()
.antMatchers('/uaa/activate/**').permitAll()
.antMatchers('/uaa/password/**').permitAll()
.antMatchers('/uaa/account/**').hasAuthority('ADMIN')
.antMatchers('/api-docs/**').permitAll()
.antMatchers('/admin/**').hasAuthority('SUPERADMIN')
.anyRequest().authenticated()
//http.sessionManagement().sessionCreationPolicy(STATELESS)
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceId)
resources.authenticationManager(authenticationManager)
}
}
@Configuration
@EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Value('${oauth.clientId}')
private String clientId
@Value('${oauth.secret:}')
private String secret
@Value('${oauth.resourceId}')
private String resourceId
@Autowired
@Qualifier('authenticationManagerBean')
private AuthenticationManager authenticationManager
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
return new JwtAccessTokenConverter();
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.checkTokenAccess("permitAll()")
oauthServer.allowFormAuthenticationForClients()
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter())
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(secret)
.authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
.authorities("USER", "ADMIN")
.scopes("read", "write", "trust")
.resourceIds(resourceId)
}
}
}
主要问题是我无法同时创建(header 中的 Web 登录表单和 OAuth2 授权令牌)运行。如果 ResourceServer
获得更高优先级,则 OAuth2 令牌授权有效,但我无法使用 Web 表单登录。另一方面,如果我将更高的优先级设置为 LoginConfig
class,则 OAuth2 令牌授权将停止工作。
案例研究:登录表单有效,OAuth2 令牌授权无效
我发现在那种情况下问题是由 non-registered OAuth2AuthenticationProcessingFilter
引起的。我尝试在 ResourceServer.configure(HttpSecurity http)
方法中手动注册它,但它不起作用 - 我可以在 FilterChain 列表中看到过滤器,但它没有被触发。这不是修复它的好方法,因为在 ResourceServer 初始化期间还有很多其他魔术,所以我转向第二种情况。
案例研究:登录表单不起作用,OAuth2 令牌授权有效
在这种情况下,主要问题是默认情况下 UsernamePasswordAuthenticationFilter
无法找到正确配置的 AuthenticationProvider
实例(在 ProviderManager
中)。当我尝试通过以下方式手动添加它时:
http.authenticationProvide(daoAuthenticationProvider())
它得到一个,但在这种情况下没有 AuthenticationEventPublisher
定义,并且成功的身份验证无法发布到其他组件。事实上,在下一次迭代中,它会被 AnonymousAuthenticationToken
取代。这就是为什么我尝试手动定义 AuthenticationManager
实例,其中包含 DaoAuthenticationProvider
:
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
//return super.authenticationManagerBean()
ProviderManager providerManager = new ProviderManager([daoAuthenticationProvider()], super.authenticationManagerBean())
providerManager.setAuthenticationEventPublisher(new DefaultAuthenticationEventPublisher(applicationEventPublisher))
return providerManager
}
我认为它会起作用,但是向注册过滤器提供 AuthenticationManager
实例存在不同的问题。事实证明,每个过滤器都有 authenticationManager
使用 sharedObjects
组件手动注入:
authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
这里的问题是不能保证您有一个正确的实例集,因为有一个简单的 HashMap (check it on GitHub) 用于存储特定的共享 object 并且它可以任意更改时间。我尝试将其设置为:
http.setSharedObject(AuthenticationManager.class, authenticationManager)
但在我到达它正在阅读的地方之前,它已经被默认实现所取代。我用调试器检查了它,看起来每个新过滤器都有一个新的身份验证管理器实例。
我的问题是:我做得对吗?如何将资源服务器集成在一个应用程序中并使用登录表单(OAuth2 对话框)工作来设置授权服务器?也许可以用一种不同的、更简单的方式来完成。如果有任何帮助,我将不胜感激。
我认为您不应该尝试在您的 ResourceServerConfigurerAdapter
中设置表单登录或 http basic,如果您已经在其他 WebSecurityConfigurerAdapter
中设置它们,当然也不应该(您这样做是因为默认情况下它们是打开的)。它可能有效,但 OAuth2 保护资源和 UI 的身份验证和访问决策非常不同,我建议您将它们分开(因为它们在 github 的所有示例中)。如果您接受建议并继续使用您已经定义的组件,那么正确执行此操作的关键是要知道过滤器链是按顺序尝试的,并且第一个匹配的将获胜,因此只有其中一个会作用于任何给定的要求。您必须将请求匹配器放在两个链中(或至少是顺序最低的那个),并确保它们不重叠。
如果您使用配置了不同安全性的不同端点怎么办?
对于上面的示例,/uaa/** 的所有内容都使用 WebSecurityConfigurerAdapter 进行保护,而 /api-docs/** 使用 ResourceServerConfigurerAdapter 进行保护。
那样的话,过滤链还会冲突吗?
这里是问题的解决方案。看看这个示例 Groovy
class:
@Configuration
@EnableResourceServer
class ResourceServer extends ResourceServerConfigurerAdapter {
@Value('${oauth.resourceId}')
private String resourceId
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
http.httpBasic().disable()
http.requestMatchers().antMatchers('/admin/**', '/uaa/**')
.and().authorizeRequests()
.antMatchers('/uaa/authenticated/**').authenticated()
.antMatchers('/uaa/register/**').permitAll()
.antMatchers('/uaa/activate/**').permitAll()
.antMatchers('/uaa/password/**').permitAll()
.antMatchers('/uaa/auth/**').permitAll()
.antMatchers('/uaa/account/**').hasAuthority('ADMIN')
.antMatchers('/admin/**').hasAuthority('ADMIN')
.anyRequest().authenticated()
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(resourceId);
}
}
基本上,要运行 OAuth2.0 身份验证与 Web 表单身份验证并行,您必须将
http.requestMatchers().antMatchers('/path/1/**', '/path/2/**')
配置 class。我之前的配置遗漏了这个重要的部分,所以只有OAuth2.0参与了认证过程。