当前位置: 首页 > 知识库问答 >
问题:

带Eureka和Docker Swarm的Spring开机管理

鲜于裕
2023-03-14

编辑/解决方案:

我明白了,部分归功于@anemyte的评论。尽管eureka.hostname属性不是问题所在(尽管它确实需要更正),但仔细观察后,我发现了问题的真正原因:正在使用的网络接口、端口转发和(糟糕的)运气。

不幸的是,客户机服务选择用它们的桥接接口IP向Eureka注册作为广告IP而不是内部swarm IP--可能是因为在这种情况下inetaddress.getLocalHost()(Spring Cloud在内部使用)将返回该IP。

这导致我错误地认为Spring Boot Admin可以访问这些服务--就像我在外部可以访问一样,而实际上它不能访问,因为广告的是错误的IP。使用curl来验证这一点只会加剧混乱,因为我使用覆盖IP来检查服务是否可以通信,而这不是在Eureka注册的服务。

该问题的(临时)解决方案是将spring.cloud.inetutils.preferred-networks设置设置为10.0,这是内部群网络(此处提供文档)的默认IP地址池(更具体地说:10.0.0.0/8)。还有一种黑名单方法使用spring.cloud.inetutils.liveled-networks,但是我不喜欢使用它。

在这种情况下,客户机应用程序向Eureka公布了它们的实际群覆盖IP,SBA能够到达它们。

我确实觉得有点奇怪,我没有从SBA获得任何错误消息,并将在他们的跟踪器上打开一个问题。也许我只是做错了什么。

(原题如下)

    null
    null

但是,我在Docker群中复制这个设置时遇到了问题。

我有两个Docker服务,比如eureka-serverclient-api-这两个服务都是使用相同的网络创建的,容器可以通过这个网络(例如curl)相互连接。eureka-server正确启动,client-api立即向Eureka注册。

尝试正确导航到eureka_url/Admin会显示Keycloak登录页面,并在成功登录后重定向回Spring Boot Admin UI。然而,没有申请注册,我不知道为什么。

编辑:

我不太确定哪些设置可能与此问题相关,但以下是我的一些配置文件(作为代码段,因为它们不是那么小,我希望可以):

application.yaml

---
eureka:
  hostname: localhost
  port: 8761
  client:
    register-with-eureka: false
    # Registry must be fetched so that Spring Boot Admin knows that there are registered applications
    fetch-registry: true 
    serviceUrl:
      defaultZone: http://${eureka.hostname}:${eureka.port}/eureka/
  instance:
    lease-renewal-interval-in-seconds: 10
    lease-expiration-duration-in-seconds: 30
  environment: eureka-test-${user.name}
  server:
    enable-self-preservation: false # Intentionally disabled for non-production

spring:
  application:
    name: eureka-server
  boot:
    admin:
      client:
        prefer-ip: true
      # Since we are running in Eureka, "/" is already the root path for Eureka itself
      # Register SBA under the "/admin" path
      context-path: /admin
  cloud:
    config:
      enabled: false
  main:
    allow-bean-definition-overriding: true


keycloak:
  realm: ${realm}
  auth-server-url: ${auth_url}
  # Client ID
  resource: spring-boot-admin-automated
  # Client secret used for service account grant
  credentials:
    secret: ${client_secret}
  ssl-required: external
  autodetect-bearer-only: true
  use-resource-role-mappings: false
  token-minimum-time-to-live: 90
  principal-attribute: preferred_username
// Versioning / Spring parents poms
apply from: new File(project(':buildscripts').projectDir, '/dm-versions.gradle')

configurations {
    all*.exclude module: 'spring-boot-starter-tomcat'
}

ext {
    springBootAdminVersion = '2.3.1'
    keycloakVersion = '11.0.2'
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
    implementation "de.codecentric:spring-boot-admin-starter-server:${springBootAdminVersion}"
    implementation 'org.keycloak:keycloak-spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    compile "org.keycloak:keycloak-admin-client:${keycloakVersion}"

    testCompileOnly 'org.projectlombok:lombok'
}

dependencyManagement {
    imports {
        mavenBom "org.keycloak.bom:keycloak-adapter-bom:${keycloakVersion}"
    }
}

实际应用程序代码:

package com.app.eureka;

import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableAdminServer
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {

public static void main(String[] args) {
    SpringApplication.run(EurekaServer.class, args);
}

}

Keycloak配置:

package com.app.eureka.keycloak.config;

import de.codecentric.boot.admin.server.web.client.HttpHeadersProvider;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.OAuth2Constants;
import org.keycloak.adapters.springboot.KeycloakSpringBootProperties;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.http.HttpHeaders;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.security.Principal;
import java.util.Objects;

@KeycloakConfiguration
@EnableConfigurationProperties(KeycloakSpringBootProperties.class)
class KeycloakConfig extends KeycloakWebSecurityConfigurerAdapter {

private static final String X_API_KEY = System.getProperty("sba_api_key");

@Value("${keycloak.token-minimum-time-to-live:60}")
private int tokenMinimumTimeToLive;

/**
 * {@link HttpHeadersProvider} used to populate the {@link HttpHeaders} for
 * accessing the state of the disovered clients.
 *
 * @param keycloak
 * @return
 */
@Bean
public HttpHeadersProvider keycloakBearerAuthHeaderProvider(final Keycloak keycloak) {
    return provider -> {
        String accessToken = keycloak.tokenManager().getAccessTokenString();
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Api-Key", X_API_KEY);
        headers.add("X-Authorization-Token", "keycloak-bearer " + accessToken);
        return headers;
    };
}

/**
 * The Keycloak Admin client that provides the service-account Access-Token
 *
 * @param props
 * @return keycloakClient the prepared admin client
 */
@Bean
public Keycloak keycloak(KeycloakSpringBootProperties props) {
    final String secretString = "secret";
    Keycloak keycloakAdminClient = KeycloakBuilder.builder()
            .serverUrl(props.getAuthServerUrl())
            .realm(props.getRealm())
            .grantType(OAuth2Constants.CLIENT_CREDENTIALS)
            .clientId(props.getResource())
            .clientSecret((String) props.getCredentials().get(secretString))
            .build();

    keycloakAdminClient.tokenManager().setMinTokenValidity(tokenMinimumTimeToLive);
    return keycloakAdminClient;
}

/**
 * Put the SBA UI behind a Keycloak-secured login page.
 * 
 * @param http
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    super.configure(http);
    http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/**/*.css", "/admin/img/**", "/admin/third-party/**").permitAll()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().permitAll();
}

@Autowired
public void configureGlobal(final AuthenticationManagerBuilder auth) {
    SimpleAuthorityMapper grantedAuthorityMapper = new SimpleAuthorityMapper();
    grantedAuthorityMapper.setPrefix("ROLE_");
    grantedAuthorityMapper.setConvertToUpperCase(true);

    KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
    keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(grantedAuthorityMapper);
    auth.authenticationProvider(keycloakAuthenticationProvider);
}

@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
    return new RegisterSessionAuthenticationStrategy(buildSessionRegistry());
}

@Bean
protected SessionRegistry buildSessionRegistry() {
    return new SessionRegistryImpl();
}

/**
 * Allows to inject requests scoped wrapper for {@link KeycloakSecurityContext}.
 * <p>
 * Returns the {@link KeycloakSecurityContext} from the Spring
 * {@link ServletRequestAttributes}'s {@link Principal}.
 * <p>
 * The principal must support retrieval of the KeycloakSecurityContext, so at
 * this point, only {@link KeycloakPrincipal} values and
 * {@link KeycloakAuthenticationToken} are supported.
 *
 * @return the current <code>KeycloakSecurityContext</code>
 */
@Bean
@Scope(scopeName = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public KeycloakSecurityContext provideKeycloakSecurityContext() {

    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    Principal principal = Objects.requireNonNull(attributes).getRequest().getUserPrincipal();
    if (principal == null) {
        return null;
    }

    if (principal instanceof KeycloakAuthenticationToken) {
        principal = (Principal) ((KeycloakAuthenticationToken) principal).getPrincipal();
    }

    if (principal instanceof KeycloakPrincipal<?>) {
        return ((KeycloakPrincipal<?>) principal).getKeycloakSecurityContext();
    }

    return null;
}

}

KeyCloakConfigurationResolver

package com.app.eureka.keycloak.config;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class KeycloakConfigurationResolver {

/**
 * Load Keycloak configuration from application.properties or application.yml
 *
 * @return
 */
@Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
    return new KeycloakSpringBootConfigResolver();
}

}
package com.app.eureka.keycloak.config;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.HttpServletRequest;

@Controller
class LogoutController {

/**
 * Logs the current user out, preventing access to the SBA UI
 * @param request
 * @return
 * @throws Exception
 */
@PostMapping("/admin/logout")
public String logout(final HttpServletRequest request) throws Exception {
    request.logout();
    return "redirect:/admin";
}
}

不幸的是,我没有docker-compose.yaml,因为我们的部署主要是通过Ansible完成的,匿名这些脚本相当困难。

这些服务最终按以下方式创建(使用Docker service Create):(其中一些网络可能与此无关,因为这是在我的个人节点上运行的本地群,请注意swarm网络)

dev@ws:~$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
3ba4a65c319f        bridge              bridge              local
21065811cbff        docker_gwbridge     bridge              local
ti1ksbdxlouo        services            overlay             swarm
c59778b105b5        host                host                local
379lzdi0ljp4        ingress             overlay             swarm
dd92d2f75a31        none                null                local

eureka-serverdockerfile:

FROM registry/image:latest
MAINTAINER "dev@com.app"
COPY eureka-server.jar /home/myuser/eureka-server.jar
USER myuser
WORKDIR /home/myuser
CMD /usr/bin/java -jar \
    -Xmx523351K -Xss1M -XX:ReservedCodeCacheSize=240M \
    -XX:MaxMetaspaceSize=115625K \
    -Djava.security.egd=file:/dev/urandom eureka-server.jar \
    --server.port=8761; sh
dev@ws:~$ docker service create --name eureka-server -p 8080:8761 --replicas 1 --network services --hostname eureka-server --limit-cpu 1 --limit-memory 768m eureka-server

然后按以下方式启动客户端应用程序:

DockerFile

FROM registry/image:latest
MAINTAINER "dev@com.app"
COPY client-api.jar /home/myuser/client-api.jar
USER myuser
WORKDIR /home/myuser
CMD /usr/bin/java -jar \
    -Xmx523351K -Xss1M -XX:ReservedCodeCacheSize=240M \
    -XX:MaxMetaspaceSize=115625K \
    -Djava.security.egd=file:/dev/urandom -Deureka.instance.hostname=client-api client-api.jar \
    --eureka.zone=http://eureka-server:8761/eureka --server.port=0; sh

然后创建为群服务,如下所示:

dev@ws:~$ docker service create --name client-api --replicas 1 --network services --hostname client-api --limit-cpu 1 --limit-memory 768m client-api
eureka:
  name: ${spring.application.name}
  instance:
    leaseRenewalIntervalInSeconds: 10
    instanceId: ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instanceId:${random.int}}
    preferIpAddress: true
  client:
    registryFetchIntervalSeconds: 5

共有1个答案

祁杰
2023-03-14

我没有发现您提供的配置有任何问题。我看到的唯一薄弱线索是eureka.hostname=localhost来自application.ymllocalhost和环回IP是Swarm最好避免的两件事。我想你应该检查一下它是否与网络有关。

 类似资料:
  • 我试图将spring boot admin接口添加到kubernetes集群中部署的一些微服务中。spring boot admin应用程序具有以下配置: kubernetes集群有一个入口,用作api网关: 我如何解决这个问题并配置一个传统路径来为来自正确URL的请求中的静态内容提供服务? 提前致谢

  • 使用Spring Boot V1.5.x,可以将单个服务double用作Spring Boot Admin服务器和Eureka Discovery服务器。对于Spring Boot2.x,Spring Boot Admin Server似乎使用Spring reactive API(webflux,Netty Server等),而Netflix Eureka Discovery Server仍然使

  • 我正试图使用SpringCloudGateway构建一个带有负载平衡器的网关,但当我点击gateway时,它给了我404,而不是将我路由到eureka中的注册服务 我不想使用自动发现功能,所以我使用手动路由。 我的代码上传到这里:https://github.com/gry77/poc-spring-cloud-gateway 我怎样才能修好它?

  • 我想使用Feign Client、Ribbon和Eureka实现一个弹性的微服务架构,所以我遇到了一个问题。当一个微服务目标关闭时,我希望重定向到另一个微服务实例,而不让用户看到它。例如,我有4个微服务B实例和一个实例A:

  • iam使用spring boot gateway与eureka服务器,但当我试图从gateway访问某个api时,它不使用gateway路由的路径,而是使用服务名 但如果我将“book”替换为“books”(服务名),它就会起作用

  • null 这个设置工作良好,但麻烦开始时,我开始玩的缩放服务。当我放大时,Zuul会在30秒后拿起新的服务器。我没意见。但是,当我取下服务容器时,对Zuul的调用可能会失败(HTTP错误代码200!)因为Zuul仍然认为服务器在活动池中。 我使用的是Spring Boot 1.3.6和Spring Cloud 1.1.2 我的问题: null