Spring Cloud

Published: by Creative Commons Licence

  • Tags:

Eureka

Eureka是Spring Cloud中的服务注册发现中心。

单机部署

下面是Eureka项目的简单搭建过程。

创建一个新的项目,名字叫做eureka-sample。在maven文件中加入:

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

之后在application.yml文件中写入必要的配置信息:

server:
  port: 8761
eureka:
  client:
    registerWithEureka: false #单机模式下不允许自己注册自己
    fetchRegistry: false #单机模式下不允许从注册中心拉注册信息

spring:
  application:
    name: eureka-sample

之后创建一个EurekaServerApplication文件作为Spring应用的入口。

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
	public static void main(String[] args) {
		SpringApplication.run(EurekaServerApplication.class, args);
	}
}

启动项目后,通过浏览器访问localhost:8761

Gateway

Eureka是Spring Cloud中的网关。

单机部署

下面是Gateway的简单部署过程。

首先加入maven依赖:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

之后修改application.yml文件:

server:
  port: 10000

spring:
  application:
    name: gateway-sample
  cloud:
    gateway:
      routes:
      discovery:
        locator:
          enabled: true # 对于注册到注册中心的服务S,可以通过/S/X访问S的服务的路径/X
          lowerCaseServiceId: true # 这里允许我们以驼峰法来替代S

eureka:
  client:
    healthcheck:
      enabled: true
    service-url:
      defaultZone: http://peer1:8761/eureka/

之后创建一个入口类。

@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

为了演示Gateway的用途,我们创建一个简单的Client项目。其maven配置为:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

之后配置文件application.yml内容为:

spring:
  application:
    name: gateway-client

server:
  port: 10001
  
eureka:
  client:
    healthcheck:
      enabled: true
    service-url:
      defaultZone: http://peer1:8761/eureka/

之后创建一个启动类:

@SpringBootApplication
@EnableDiscoveryClient
public class CoreApplication {
    public static void main(String[] args) {
        SpringApplication.run(CoreApplication.class, args);
    }
}

之后我们还需要给gateway-client项目提供一个控制器。

@RequestMapping("/hello")
@RestController
@RefreshScope
public class HelloController {
    @RequestMapping("/date")
    public String getDate() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    }
}

启动gateway-client后,可以通过路径localhost:10001/hello/date来获取当前的时间。但是由于我们使用了网关,因此可以使用localhost:10000/gateway-client/hello/date来实现相同功能。

Config

Config是Spring Cloud中的分布式配置管理中心。

单机部署

先改maven文件。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-stream-kafka</artifactId>
        </dependency>

上面引入了kafka,来实现配置的自动更新。同时还有actuator,用于提供一些和更新配置文件有关的接口。

之后我们修改配置文件application.yml

eureka:
  client:
    healthcheck:
      enabled: true
    service-url:
      defaultZone: http://peer1:8761/eureka/

spring:
  application:
    name: config-sample
  profiles:
    active: native # 配置文件放在本地
  cloud:
    config:
      server:
        native:
          search-locations: /config # 本地配置文件所在的绝对路径
    stream:
      kafka:
        binder:
          zk-nodes: localhost:2181 #ZK
          brokers: localhost:9092 #Kafka
server:
  port: 8861

management:
  endpoints:
    web:
      exposure:
        include: "*" # 这里需要暴露一些端口

创建一个入口类。

@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

之后我们增加一个客户端来演示效果。创建一个config-client项目。

maven文件:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-kafka</artifactId>
        </dependency>

之后是配置文件,由于一些配置项是在Spring容器初始化前加载的,因此我们需要在bootstrap.yml文件中加入:

spring:
  application:
    name: config-client
  cloud:
    config:
      profile: dev #这里我们使用dev profile
      discovery:
        enabled: true
        service-id: config-sample # 配置服务的ID
    stream:
      kafka:
        binder:
          zk-nodes: localhost:2181 #Zk地址
          brokers: localhost:9092 #Kafka地址
    bus:
      refresh:
        enabled: true # 允许刷新
eureka:
  client:
    healthcheck:
      enabled: true
    service-url:
      defaultZone: http://peer1:8761/eureka/

之后的application.yml文件如下:

server:
  port: 10001

management:
  endpoints:
    web:
      exposure:
        include: "*"

照例我们加入一个入口类:

@SpringBootApplication
@EnableDiscoveryClient
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

以及一个Controller。

@RequestMapping("/hello")
@RestController
@RefreshScope
public class HelloController {
    @Value("${greet}")
    private String greet;

    @RequestMapping("/greet")
    public String greet() {
        return greet;
    }
}

注意greet属性我们还没有在任何地方配置。下面我们配置这个在/config目录下创建一个新文件,叫做config-client-dev.yml(格式为服务ID-profile.yml)。

内容如下:

greet: "Hello world"

之后我们先启动config-sample项目,之后启动config-client项目。访问路径localhost:8861/config-client/dev可以看到整个配置文件config-client-dev.yml。访问路径localhost:10001/hello/greet会输出Hello world

接下来我们在不关闭两个服务的情况下修改config-client-dev.yml文件。修改后访问路径localhost:8861/config-client/dev会直接给出最新的信息,但是localhost:10001/hello/greet的结果不会改变,我们必须手工让配置中心通知其它项目。具体就是通过POST方式访问路径localhost:8861/actuator/bus-refresh,之后重新访问localhost:10001/hello/greet可以看到最新的更改。

可以发现Spring在kafka中自动创建了一个名字叫做springCloudBus的topic,里面有一些类型为RefreshRemoteApplicationEvent的消息。

Sleth

单机部署

Zipkin

去官网下载zipkin。

之后我们通过下面命令来启动Zipkin。我这边使用ES作为存储底层,用KAFKA接收消息。

$ java -jar zipkin.jar --STORAGE_TYPE=elasticsearch --ES_HOSTS=localhost:9200 --ES_HTTP_LOGGING=BASIC --KAFKA_BOOTSTRAP_SERVERS=127.0.0.1:9092

项目

创建一个名字叫做sleth-client的项目。

下面是maven文件:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-kafka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

下面是application.yml文件:

server:
  port: 10001

spring:
  application:
    name: sleth-client
  cloud:
    stream:
      kafka:
        binder:
          zk-nodes: localhost:2181
          brokers: localhost:9092
  zipkin:
    sender:
      type: kafka #使用kafka作为发送消息的方式
  sleuth:
    sampler:
      probability: 1.0 # 采用频率,1表示100%

eureka:
  client:
    healthcheck:
      enabled: true
    service-url:
      defaultZone: http://peer1:8761/eureka/

之后创建一个简单的WEB应用:

@SpringBootApplication
@EnableDiscoveryClient
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

创建一个简单的控制器:

@RequestMapping("/hello")
@RestController
@RefreshScope
public class HelloController {

    @RequestMapping("/date")
    public String getDate() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
    }
}

之后先访问路径localhost:10001/hello/date,再去看看zipkin的页面是否出现了变化。

Security

帐号密码登录

下面我们构建一个简单的项目,来演示如何利用Security实现登录操作。

创建一个maven项目,加入下面的依赖:

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

之后我们需要实现UserDetailsServiceUserDetails两个接口。前者向Spring暴露通过用户名查找用户信息的接口,后者则是一般用户信息的接口。

下面是UserDetailsService的实现类,其直接从数据库中查找用户,如果没有找到就抛出UsernameNotFoundException异常。

@Service
public class UserService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        UserExample example = new UserExample();
        example.or().andDeletedIdEqualTo(0).andUsernameEqualTo(s);
        User user = userMapper.selectByExample(example)
                .stream().findFirst().orElseThrow(() -> new UsernameNotFoundException(s));

        return UserDTO.builder().username(user.getUsername())
                .password(user.getPassword())
                .authorities(Collections.singletonList(new SimpleGrantedAuthority("known")))
                .build();
    }
}

下面是UserDetails的实现类:

@Builder
public class UserDTO implements UserDetails {
    private String username;
    private String password;
    private List<? extends GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

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

接下来我们还需要写一些配置类:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout().logoutUrl("/logout").logoutSuccessUrl("/")
                .and().formLogin().permitAll()
                .and().authorizeRequests().antMatchers("/user/test1").authenticated()
                .and().authorizeRequests().antMatchers("/user/test2").permitAll()
                .and().csrf().disable();
    }

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

之后我们增加一个控制器:

@RestController
@RequestMapping("/user")
public class HelloController {
    //通过Principal获得当前的登录用户
    @RequestMapping("/test1")
    public String test1(Principal p) {
        return "love and peace for " + p.getName();
    }

    @RequestMapping("/test2")
    public String test2() {
        return "for freedom";
    }
}

之后调用/user/test2可以发现是正常访问的,但是调用/user/test1会跳转到登录页面。登录完成后,/user/test1也可以正常访问了。

登录完成后,可以发现服务器的响应头中Set-Cookie: JSESSIONID=76B0C326E036B12F2D946F64CE00F1D1; Path=/; HttpOnly字段被设置。即默认的Spring Security会使用Cookie+Session的模式来保存用户的登录信息。

OAuth 2

下面我们演示如何利用Spring Security实现OAuth 2登录。

注意这部分内容是基于帐号密码登录项目继续完善的。

首先在加入下面的maven依赖:

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

之后我们需要增加一个配置类,配置客户端信息:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    //使用redis存储我们分配的token、code等信息
    @Bean
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }

    /**
     * 配置客户端详细信息
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                //客户端ID
                .withClient("zcs")
                .secret(new BCryptPasswordEncoder().encode("zcs"))
                //权限范围
                .scopes("app")
                //授权码模式
                .authorizedGrantTypes("authorization_code")
                //随便写
                .redirectUris("https://www.baidu.com");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(redisTokenStore())
                .authenticationManager(authenticationManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 开启/oauth/token_key验证端口无权限访问
                .tokenKeyAccess("permitAll()")
                // 开启/oauth/check_token验证端口认证权限访问
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }
}

之后还需要增加一个ResourceServerConfigurerAdapter的子类,里面要声明资源服务器的权限。

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/user/**")
                .authorizeRequests()
                .antMatchers("/user/test1").authenticated()
                .antMatchers("/user/test2").permitAll();
    }
}

之后启动服务器后。访问路径/user/test2,是可以正常访问的,但是访问/user/test1则会提示没有权限。

下面我们模拟oauth过程。首先访问/oauth/authorize?client_id=zcs&response_type=code&redirect_uri=https://www.baidu.com,这时候会跳转到登录页面,输入授权用户的帐号密码后。会跳转到https://www.baidu.com/?code=JsVFpm这样一个地址。其中code就是授权用户的授权码。

利用POST请求访问路径/oauth/token?grant_type=authorization_code&redirect_uri=https://www.baidu.com&client_id=zcs&client_secret=zcs&code=sKe4o9。下面是成功响应内容

{
    "access_token": "913cb0d0-67ff-418c-8bef-1cb498f103a8",
    "token_type": "bearer",
    "expires_in": 36517,
    "scope": "app"
}

其中包含了access_token。我们之后可以使用这个token去访问资源服务器中的资源。下面尝试访问原来没有权限访问的/user/test1,结果如下:

$ curl --location --request GET 'localhost:10002/user/test1' \
--header 'Authorization: Bearer 913cb0d0-67ff-418c-8bef-1cb498f103a8'

love and peace for admin

参考资料