3年前 (2021-08-23)  微服务 |   抢沙发  507 
文章评分 0 次,平均分 0.0

在开发微服务体系架构时,Java是一种很好的语言。事实上,我们行业中的一些大牌公司都在使用它。你听说过Netflix、亚马逊或谷歌吗?eBay、Twitter和LinkedIn呢?是的,处理令人难以置信的流量的大公司都在用Java来实现这一点。

在Java中实现微服务体系结构并不适合所有人。因此,通常不需要实现微服务。大多数公司这样做是为了扩大员工规模,而不是系统规模。如果你想扩大你的员工规模,雇佣Java开发人员是最好的方法之一。毕竟,精通Java的开发人员比大多数其他语言的开发人员都多。

Java生态系统有一些成熟的模式用于开发微服务体系结构。如果您熟悉Spring,那么使用SpringBoot和SpringCloud进行开发会让您感到宾至如归。因为这是最快的入门方法之一,我想我会带你快速完成一个教程。

使用Spring CloudSpring Boot创建Java微服务

今天,我想采用一种不同的方法,与您一起逐步介绍一个预构建的示例。希望,这将是一个短一点,更容易理解。

您可以从克隆@oktadeveloper/java微服务示例存储库(https://github.com/oktadeveloper/java-microservices-examples)开始。

git clone https://github.com/oktadeveloper/java-microservices-examples.git
cd java-microservices-examples/spring-boot+cloud

在spring boot+cloud目录中,有三个项目:

  • discovery-service:Netflix Eureka服务器,用于服务发现。
  • car-service:一个简单的汽车服务,它使用SpringDataREST来提供汽车的RESTAPI。
  • api-gateway:一个api网关,它有一个/cool cars端点,与汽车服务对话,过滤掉不酷的汽车(当然,在我看来)。

我使用start.spring.io的RESTAPI和HTTPie创建了所有这些应用程序。

http https://start.spring.io/starter.zip bootVersion==2.2.5.RELEASE javaVersion==11 \
  artifactId==discovery-service name==eureka-service \
  dependencies==cloud-eureka-server baseDir==discovery-service | tar -xzvf -

http https://start.spring.io/starter.zip bootVersion==2.2.5.RELEASE \
  artifactId==car-service name==car-service baseDir==car-service \
  dependencies==actuator,cloud-eureka,data-jpa,h2,data-rest,web,devtools,lombok | tar -xzvf -

http https://start.spring.io/starter.zip bootVersion==2.2.5.RELEASE \
  artifactId==api-gateway name==api-gateway baseDir==api-gateway \
  dependencies==cloud-eureka,cloud-feign,data-rest,web,cloud-hystrix,lombok | tar -xzvf -

使用Netflix Eureka进行Java服务发现

发现服务的配置与大多数Eureka服务器相同。它的主类和属性上有一个@EnableEurekaServer注释,用于设置端口和关闭发现。

server.port=8761
eureka.client.register-with-eureka=false

汽车服务car-service和api网关项目api-gateway以类似的方式配置。两者都定义了一个唯一的名称,并且car服务配置为在端口8090上运行,因此不会与8080冲突。

汽车服务/src/main/resources/application.properties

server.port=8090
spring.application.name=car-service

api-gateway/src/main/resources/application.properties

spring.application.name=api-gateway

两个项目中的主类都用@EnableDiscoveryClient注释。

使用SpringDataREST构建Java微服务

汽车服务提供了一个RESTAPI,可以让您CRUD(创建、读取、更新和删除)汽车。当应用程序使用ApplicationRunner bean加载时,它会创建一组默认的汽车。

car-service/src/main/java/com/example/carservice/CarServiceApplication.java

package com.example.carservice;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.stream.Stream;

@EnableDiscoveryClient
@SpringBootApplication
public class CarServiceApplication {

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

    @Bean
    ApplicationRunner init(CarRepository repository) {
        return args -> {
            Stream.of("Ferrari", "Jaguar", "Porsche", "Lamborghini", "Bugatti",
                    "AMC Gremlin", "Triumph Stag", "Ford Pinto", "Yugo GV").forEach(name -> {
                repository.save(new Car(name));
            });
            repository.findAll().forEach(System.out::println);
        };
    }
}

@Data
@NoArgsConstructor
@Entity
class Car {

    public Car(String name) {
        this.name = name;
    }

    @Id
    @GeneratedValue
    private Long id;

    @NonNull
    private String name;
}

@RepositoryRestResource
interface CarRepository extends JpaRepository<Car, Long> {
}

Spring Cloud+Feign和Hystrix在API网关中

Feign使编写java http客户机变得更容易。SpringCloud使得只需几行代码就可以创建一个外部客户机。Hystrix可以为您的外部客户端添加故障切换功能,使它们更具弹性。

api网关使用FIGN和Hystrix与下游car服务通信,如果不可用,则故障切换到fallback()方法。它还公开了一个/cool-cars端点,用于过滤掉您可能不想拥有的汽车。

api-gateway/src/main/java/com/example/apigateway/ApiGatewayApplication.java

package com.example.apigateway;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.Data;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.hateoas.CollectionModel;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;

@EnableFeignClients
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ApiGatewayApplication {

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

@Data
class Car {
    private String name;
}

@FeignClient("car-service")
interface CarClient {

    @GetMapping("/cars")
    @CrossOrigin
    CollectionModel<Car> readCars();
}

@RestController
class CoolCarController {

    private final CarClient carClient;

    public CoolCarController(CarClient carClient) {
        this.carClient = carClient;
    }

    private Collection<Car> fallback() {
        return new ArrayList<>();
    }

    @GetMapping("/cool-cars")
    @CrossOrigin
    @HystrixCommand(fallbackMethod = "fallback")
    public Collection<Car> goodCars() {
        return carClient.readCars()
                .getContent()
                .stream()
                .filter(this::isCool)
                .collect(Collectors.toList());
    }

    private boolean isCool(Car car) {
        return !car.getName().equals("AMC Gremlin") &&
                !car.getName().equals("Triumph Stag") &&
                !car.getName().equals("Ford Pinto") &&
                !car.getName().equals("Yugo GV");
    }
}

运行Java微服务体系结构

如果使用./mvnw spring boot:run在单独的终端窗口中运行所有这些服务,则可以导航到http://localhost:8761 看看他们已经在Eureka注册了。

基于Spring Boot和Spring Cloud实现Java微服务

如果您从GitHub克隆开始,并导航到http://localhost:8080/cool-cars在你的浏览器中,你将被重定向到Okta。怎么回事?

使用OAuth 2.0和OIDC保护Java微服务

我已经使用OAuth 2.0和OIDC在这个微服务体系结构中配置了安全性。这两者有什么区别?OIDC是OAuth 2.0的一个扩展,提供标识。它还提供了发现功能,因此可以从单个URL(称为颁发者)发现所有不同的OAuth 2.0端点。

我如何为所有这些微服务配置安全性?我很高兴你问我!

我将Okta的Spring Boot starter添加到api-gateway 和 car-service中的pom.xml中:

<dependency>
    <groupId>com.okta.spring</groupId>
    <artifactId>okta-spring-boot-starter</artifactId>
    <version>1.4.0</version>
</dependency>

然后我在Okta中创建了一个新的OIDC应用程序,配置了授权代码流。如果您想看到一切都在运行,您需要完成以下步骤。

打开终端窗口并导航到api网关项目。

在Okta中创建Web应用程序

在开始之前,您需要一个免费的Okta开发者帐户。安装Okta CLI并运行Okta注册以注册新帐户。如果您已经拥有帐户,请运行okta登录。然后,运行okta应用程序创建。选择默认应用程序名称,或根据需要进行更改。选择Web并按Enter键。

选择Okta Spring Boot Starter。接受为您提供的默认重定向URI值。即登录重定向http://localhost:8080/login/oauth2/code/okta 和注销重定向http://localhost:8080.

Okta CLI做什么?

将这些密钥和值复制到汽车服务项目的application.properties文件中。

下面一节中的Java代码已经存在,但我想我应该解释一下,让您知道发生了什么。

为OAuth 2.0登录和资源服务器配置Spring安全性

ApiGatewayApplication.java中,我添加了Spring安全配置,以启用OAuth 2.0登录并启用网关作为资源服务器。

@Configuration
static class OktaOAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .authorizeRequests().anyRequest().authenticated()
                .and()
            .oauth2Login()
                .and()
            .oauth2ResourceServer().jwt();
        // @formatter:on
    }
}

本例中未使用资源服务器配置,但我添加了一个示例,以防您希望将移动应用程序或SPA连接到此网关。如果您使用SPA,还需要添加一个bean来配置CORS。

@Bean
public FilterRegistrationBean<CorsFilter> simpleCorsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.setAllowedOrigins(Collections.singletonList("*"));
    config.setAllowedMethods(Collections.singletonList("*"));
    config.setAllowedHeaders(Collections.singletonList("*"));
    source.registerCorsConfiguration("/**", config);
    FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
    bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return bean;
}

CarServiceApplication.java仅配置为资源服务器,因为不希望直接访问它。

@Configuration
static class OktaOAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http
            .authorizeRequests().anyRequest().authenticated()
                .and()
            .oauth2ResourceServer().jwt();
        // @formatter:on
    }
}

为了使API网关能够访问汽车服务,我在API网关项目中创建了UserFeignClientInterceptor.java

api-gateway/src/main/java/com/example/apigateway/UserFeignClientInterceptor.java

package com.example.apigateway;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.stereotype.Component;

@Component
public class UserFeignClientInterceptor implements RequestInterceptor {
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_TOKEN_TYPE = "Bearer";
    private final OAuth2AuthorizedClientService clientService;

    public UserFeignClientInterceptor(OAuth2AuthorizedClientService clientService) {
        this.clientService = clientService;
    }

    @Override
    public void apply(RequestTemplate template) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
        OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
                oauthToken.getAuthorizedClientRegistrationId(),
                oauthToken.getName());

        OAuth2AccessToken accessToken = client.getAccessToken();
        template.header(AUTHORIZATION_HEADER, String.format("%s %s", BEARER_TOKEN_TYPE, accessToken.getTokenValue()));
    }
}

我在ApiGatewayApplication.java中将其配置为RequestInterceptor

@Bean
public RequestInterceptor getUserFeignClientInterceptor(OAuth2AuthorizedClientService clientService) {
    return new UserFeignClientInterceptor(clientService);
}

并且,我在api-gateway/src/main/resources/application.properties中添加了两个属性,以便使Feign具有Spring安全意识。

feign.hystrix.enabled=true
hystrix.shareSecurityContext=true

在启用安全性的情况下运行的Java微服务

使用./mvnw spring-boot:run运行所有应用程序:在单独的终端窗口中运行,如果愿意,也可以在IDE中运行。

为了简化在IDE中的运行,根目录中有一个聚合器pom.xml。如果您安装了IntellijIDEA的命令行启动器,则只需运行IDEA pom.xml。

导航到http://localhost:8080/cool-cars你将被重定向到Okta登录。

基于Spring Boot和Spring Cloud实现Java微服务

输入您的Okta开发者帐户的用户名和密码,您将看到酷车列表。

基于Spring Boot和Spring Cloud实现Java微服务

如果你做到了这一点,并运行了示例应用程序,恭喜你!

使用Netflix Zuul和Spring Cloud代理路由

在微服务体系结构中,您可能喜欢的另一个便捷功能是Netflix Zuul。Zuul是一种网关服务,提供动态路由、监视、恢复等功能。

为了添加Zuul,我将其作为依赖项添加到api-gateway/pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

然后我将@EnableZuulProxy添加到ApiGatewayApplication类中。

import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringBootApplication
public class ApiGatewayApplication {
    ...
}

为了将访问令牌传递给代理路由,我创建了一个AuthorizationHeaderFilter类,该类扩展了ZumFilter

package com.example.apigateway;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.core.Ordered;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.OAuth2AccessToken;

import java.util.Optional;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

public class AuthorizationHeaderFilter extends ZuulFilter {

    private final OAuth2AuthorizedClientService clientService;

    public AuthorizationHeaderFilter(OAuth2AuthorizedClientService clientService) {
        this.clientService = clientService;
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        Optional<String> authorizationHeader = getAuthorizationHeader();
        authorizationHeader.ifPresent(s -> ctx.addZuulRequestHeader("Authorization", s));
        return null;
    }

    private Optional<String> getAuthorizationHeader() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
        OAuth2AuthorizedClient client = clientService.loadAuthorizedClient(
                oauthToken.getAuthorizedClientRegistrationId(),
                oauthToken.getName());

        OAuth2AccessToken accessToken = client.getAccessToken();

        if (accessToken == null) {
            return Optional.empty();
        } else {
            String tokenType = accessToken.getTokenType().getValue();
            String authorizationHeaderValue = String.format("%s %s", tokenType, accessToken.getTokenValue());
            return Optional.of(authorizationHeaderValue);
        }
    }
}

您可能注意到getAuthorizationHeader()方法中的代码与UserFeignClientInterceptor中的代码非常相似。因为只有几行,所以我选择不将它们移动到实用程序类。伪拦截器用于@FeignClient,而Zuul过滤器用于Zuul代理请求。

为了让SpringBoot和Zuul知道这个过滤器,我在主应用程序类中将它注册为bean。

@Bean
public AuthorizationHeaderFilter authHeaderFilter(OAuth2AuthorizedClientService clientService) {
    return new AuthorizationHeaderFilter(clientService);
}

为了代理从API网关到Car服务的请求,我向api-gateway/src/main/resources/application.properties添加了路由。

zuul.routes.car-service.path=/cars
zuul.routes.car-service.url=http://localhost:8090

zuul.routes.home.path=/home
zuul.routes.home.url=http://localhost:8090

zuul.sensitive-headers=Cookie,Set-Cookie

我为/home路线的汽车服务项目添加了一个HomeController

package com.example.carservice;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
public class HomeController {

    private final static Logger log = LoggerFactory.getLogger(HomeController.class);

    @GetMapping("/home")
    public String howdy(Principal principal) {
        String username = principal.getName();
        JwtAuthenticationToken token = (JwtAuthenticationToken) principal;
        log.info("claims: " + token.getTokenAttributes());
        return "Hello, " + username;
    }
}

确认你的Zuul Routes有效吗

由于这些更改已经存在于克隆的项目中,因此您应该能够查看这些更改http://localhost:8080/carshttp://localhost:8080/home 在浏览器中。
基于Spring Boot和Spring Cloud实现Java微服务

那么Spring Cloud Config呢?

在本例中,您可能注意到的一点是,您必须在每个应用程序中配置OIDC属性。如果你有500个微服务,这可能是一个真正的痛苦。是的,您可以将它们定义为环境变量,这将解决问题。但是,如果您有使用不同OIDC客户端ID的不同微服务堆栈,那么这种方法将很困难。

SpringCloudConfig是一个为分布式系统提供外部化配置的项目。我将在以后的教程中介绍它,而不是将其添加到此示例中。

您可以在GitHub上(https://github.com/oktadeveloper/java-microservices-examples)找到本教程中显示的所有代码。

原文地址:https://developer.okta.com/blog/2019/05/22/java-microservices-spring-boot-spring-cloud#create-java-microservices-with-spring-cloud-and-spring-boot

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2261.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册