如何使用 JDBC 实现水平扩展 spring-boot oauth2 服务器

Kai*_*kun 4 horizontal-scrolling spring-boot kubernetes oauth2-server

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

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

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

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

如果我有多个 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());
    }

}
Run Code Online (Sandbox Code Playgroud)

主要班级

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


    public static void main(String[] args) {

        SpringApplication.run(AuthorizationServerApplication.class, args);

    }

}
Run Code Online (Sandbox Code Playgroud)

网络安全配置

@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());
    }


}
Run Code Online (Sandbox Code Playgroud)

客户端 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);
    }

}
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

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

Kai*_*kun 5

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

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

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

2 - 客户端服务器,如果用户尚未经过身份验证,则向浏览器发送带有 302 http 代码的响应,以将用户重定向到 oauth2-服务器,重定向的值由安全值组成 。 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
Run Code Online (Sandbox Code Playgroud)

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

4 - oauth2-server 将 302 http 代码发送到浏览器,其中包含 oauth2-server 的登录 url,h*tps://oauth2.mydomain.com/oauth2-server/login

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

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

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

location: h*tps://www.mydomain.com/login?code=gnpZ0r&state=bSWtGx
Run Code Online (Sandbox Code Playgroud)

8 - 客户端服务器联系 oauth2 服务器并从代码中获取令牌以及对其进行身份验证的状态。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"
}
Run Code Online (Sandbox Code Playgroud)

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

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

感谢克里斯蒂安·阿尔塔米拉诺·阿亚拉的帮助。

  • 谢谢,最终比我想象的更容易修复。但我需要更好地理解整个过程。我希望它能帮助别人。 (2认同)