2

我有一个使用 JDBC 实现的 spring boot oauth2 服务器。它被配置为带有@EnableAuthorizationServer 的授权服务器。

我想水平扩展该应用程序,但它似乎无法正常工作。

只有当我有一个服务器实例(pod)时,我才能连接。

我使用来自另一个客户端服务的 autorisation_code_client 授权来获取令牌。因此,首先客户端服务将用户重定向到 oauth2 服务器表单,然后一旦用户通过身份验证,他应该被重定向到客户端服务,并带有附加到 url 的代码,最后客户端使用该代码请求 oauth2 服务器再次获取令牌。

如果我有多个 oauth2-server 实例,这里用户根本不会被重定向。在一个实例中,它运行良好。

当我实时检查两个实例的日志时,我可以看到身份验证在其中一个上有效。我没有任何特定错误,用户只是没有被重定向。

有没有办法将 oauth2-server 配置为无状态或其他方式来解决该问题?

这是我的配置,AuthorizationServerConfigurerAdapter 实现。

@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {


    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource oauthDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Bean
    public JdbcClientDetailsService clientDetailsSrv() {
        return new JdbcClientDetailsService(oauthDataSource());
    }

    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(oauthDataSource());
    }

    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(oauthDataSource());
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(oauthDataSource());
    }

    @Bean
    public TokenEnhancer tokenEnhancer() {

        return new CustomTokenEnhancer();
    }

    @Bean
    @Primary
    public AuthorizationServerTokenServices tokenServices() {


        DefaultTokenServices tokenServices = new DefaultTokenServices();

        tokenServices.setTokenStore(tokenStore());

        tokenServices.setTokenEnhancer(tokenEnhancer());

        return tokenServices;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.withClientDetails(clientDetailsSrv());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer)  {

        oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints)  {
        endpoints
                .authenticationManager(authenticationManager)
                .approvalStore(approvalStore())
                //.approvalStoreDisabled()
                .authorizationCodeServices(authorizationCodeServices())
                .tokenStore(tokenStore())
                .tokenEnhancer(tokenEnhancer());
    }

}

主要班

@SpringBootApplication
@EnableResourceServer
@EnableAuthorizationServer
@EnableConfigurationProperties
@EnableFeignClients("com.oauth2.proxies")
public class AuthorizationServerApplication {


    public static void main(String[] args) {

        SpringApplication.run(AuthorizationServerApplication.class, args);

    }

}

网络安全配置

@Configuration
@Order(1)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Bean
    @Override
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return new JdbcUserDetails();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { // @formatter:off

        http.requestMatchers()
                .antMatchers("/",
                        "/login",
                        "/login.do",
                        "/registration",
                        "/registration/confirm/**",
                        "/registration/resendToken",
                        "/password/forgot",
                        "/password/change",
                        "/password/change/**",
                        "/oauth/authorize**")
                .and()
                .authorizeRequests()//autorise les requetes
                .antMatchers(
                        "/",
                        "/login",
                        "/login.do",
                        "/registration",
                        "/registration/confirm/**",
                        "/registration/resendToken",
                        "/password/forgot",
                        "/password/change",
                        "/password/change/**")
                .permitAll()
                .and()
                .requiresChannel()
                .anyRequest()
                .requiresSecure()
                .and()
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login.do")
                .usernameParameter("username")
                .passwordParameter("password")
                .and()
                .userDetailsService(userDetailsServiceBean());


    } // @formatter:on


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());
    }


}

客户端 WebSecurityConfigurerAdapter

@EnableOAuth2Sso
@Configuration
public class UiSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http.antMatcher("/**")
                .authorizeRequests()
                .antMatchers(
                        "/",
                        "/index.html",
                        "/login**",
                        "/logout**",
                        //resources
                        "/assets/**",
                        "/static/**",
                        "/*.ico",
                        "/*.js",
                        "/*.json").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().csrfTokenRepository(csrfTokenRepository())
                .and()
                .addFilterAfter(csrfHeaderFilter(), SessionManagementFilter.class);
    }

}

oauth2 配置属性

oauth2-server 是 kubernetes 上的服务名称(负载均衡器),也是它出现两次的服务器路径。

security:
    oauth2:
        client:
            clientId: **********
            clientSecret: *******
            accessTokenUri: https://oauth2-server/oauth2-server/oauth/token
            userAuthorizationUri: https://oauth2.mydomain.com/oauth2-server/oauth/authorize
        resource:
            userInfoUri: https://oauth2-server/oauth2-server/me

这里有一个重要的细节,userAuthorizationUri 的值是从 k8s 集群外部访问 oauth2-server 的地址。如果用户未连接并尝试访问客户端服务的 /login 路径,客户端服务会将该地址发送回带有 302 http 代码的响应中。然后用户被重定向到 oauth2-server 的 /login 路径。
https://oauth2.mydomain.com的目标是一个处理重定向到负载均衡器服务的 Nginx Ingress 控制器。

4

2 回答 2

4

这是解决此问题的方法。这根本不是 Spring 问题,而是 Nginx Ingress 控制器的错误配置。

身份验证过程分几个阶段完成:

1 - 用户单击登录按钮,该按钮以客户端-服务器的 /login 路径为目标

2 - 客户端-服务器,如果用户尚未通过身份验证,则向浏览器发送一个带有 302 http 代码的响应,将用户重定向到 oauth2-server,重定向的值与 安全值组成。 oauth2.client.userAuthorizationUri属性和重定向 url,浏览器将使用它来允许客户端服务器在用户通过身份验证后获取令牌。该网址如下所示:

h*tps://oauth2.mydomain.com/oauth2-server/oauth/authorize?client_id=autorisation_code_client&redirect_uri=h*tps://www.mydomain.com/login&response_type=code&state=bSWtGx

3 - 用户被重定向到上一个 url

4 - oauth2-server 使用 oauth2-server 的登录 url h*tps://oauth2.mydomain.com/oauth2-server/login 向浏览器发送 302 http 代码

5 - 用户提交他的凭据,如果正确,则创建令牌。

6 - 用户被重定向到与第二步相同的地址,并且 oauth-server 将信息添加到 redirect_uri 值

7 - 用户被重定向到客户端服务器。响应的重定向部分如下所示:

location: h*tps://www.mydomain.com/login?code=gnpZ0r&state=bSWtGx

8 - 客户端-服务器联系 oauth2-server 并从代码和验证它的状态中获取令牌。oauth2 服务器的实例是否不同于用户用来验证自己的实例并不重要。这里客户端-服务器使用 security.oauth2.client.accessTokenUri 的值来获取令牌,这是针对 oauth2 服务器 pod 的内部负载均衡服务地址,因此它不会通过任何 Ingress 控制器。

因此,在步骤 3 到 6 中,用户必须通过负载均衡器服务前面的 Ingress 控制器与同一 oauth2-server 实例进行通信。

可以通过使用一些注释配置 Nginx Ingress 控制器来实现:

"annotations": {
  ...
  "nginx.ingress.kubernetes.io/affinity": "cookie",
  "nginx.ingress.kubernetes.io/session-cookie-expires": "172800",
  "nginx.ingress.kubernetes.io/session-cookie-max-age": "172800",
  "nginx.ingress.kubernetes.io/session-cookie-name": "route"
}

这样,我们确保在身份验证过程中用户将被重定向到 oauth2-server 的相同 pod/实例,只要他使用相同的 cookie 进行标识。

亲和会话机制是扩展身份验证服务器和客户端服务器的好方法。一旦用户通过身份验证,他将始终使用相同的客户端实例并保留他的会话信息。

感谢 Christian Altamirano Ayala 的帮助。

于 2019-10-24T15:24:52.830 回答
2

默认情况下,使用内存中的TokenStore

默认的 InMemoryTokenStore 非常适合单个服务器

如果你想要多个豆荚,你可能应该去JdbcTokenStore

JdbcTokenStore 是同一事物的 JDBC 版本,它将令牌数据存储在关系数据库中。如果您可以在服务器之间共享数据库,则使用 JDBC 版本;如果只有一个服务器,则使用同一服务器的扩展实例;如果有多个组件,则使用授权和资源服务器。要使用 JdbcTokenStore,您需要在类路径中添加“spring-jdbc”。

源码Spring Security:OAuth 2 开发者指南

于 2019-10-23T16:21:27.313 回答