SpringCloud组件(3)-简单解析及测试

138 阅读12分钟

12 Spring Cloud Security:OAuth2

12.1 概述

12.1.1 简介

  • Spring Cloud Security 为构建安全的SpringBoot应用提供了一系列解决方案,结合Oauth2可以实现单点登录、令牌中继、令牌交换等功能
  • OAuth 2.0是用于授权的行业标准协议,OAuth 2.0为简化客户端开发提供了特定的授权流,包括Web应用、桌面应用、移动端应用等

12.1.2 具体内容

12.2 OAuth2的使用

12.1 创建模块作为认证服务器(oauth2-server)

  • 依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
  • 配置文件

    server:
      port: 9401
    spring:
      application:
        name: oauth2-service
    
  • 创建User实现UserDetails接口用于存储用户信息(ORM)

    public class User implements UserDetails {
        private String username;
        private String password;
        private List<GrantedAuthority> authorities;
    
        public User(String username, String password, List<GrantedAuthority> authorities) {
            this.username = username;
            this.password = password;
            this.authorities = 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;
        }
    }
    
  • 注册PasswordEncoder和AuthenticationManager组件到容器

    @Configuration
    public class SecureConfig extends WebSecurityConfigurerAdapter {
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
        
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    
  • 添加UserService实现UserDetailsService接口,用于加载用户信息

    @Service
    public class UserService implements UserDetailsService {
    
        private List<User> userList;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @PostConstruct
        public void initData() {
            String password = passwordEncoder.encode("123456");
            userList = new ArrayList<>();
            userList.add(new User("macro", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin")));
            userList.add(new User("andy", password, AuthorityUtils.commaSeparatedStringToAuthorityList("client")));
            userList.add(new User("mark", password, AuthorityUtils.commaSeparatedStringToAuthorityList("client")));
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            List<User> findUserList = userList.stream().filter(user -> user.getUsername().equals(username)).collect(Collectors.toList());
            if (!CollectionUtils.isEmpty(findUserList)) {
                return findUserList.get(0);
            } else {
                throw new UsernameNotFoundException("用户名或密码错误");
            }
        }
    }
    
  • 添加认证服务器配置,添加@EnableAuthorizationServer注解开启认证服务器

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserService userService;
    
        /**
         * 使用密码模式需要配置
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userService);
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("admin") // 配置client_id
                    .secret("123456") // 配置client_secret
                    .accessTokenValiditySeconds(3600) // 配置token有效期
                    .refreshTokenValiditySeconds(86400) // 设置刷新token有效期
                    .redirectUris("http://www.baidu.com") // 配置登录成功跳转页面
                    .scopes("all") // 设置申请的权限范围
                    .authorizedGrantTypes("authorization_code","password"); // 配置grant_type,表示授权类型
        }
    }
    
  • 配置资源服务器,添加@EnableResourceServer注解开启资源服务器

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .requestMatchers()
                    .antMatchers("/user/**"); // 配置需要保护的资源路径
        }
    }
    
  • 添加SpringSecurity配置,允许认证相关路径的访问及表单登录

    • PasswordEncoder和AuthenticationManager已经提前注册到容器中,主要添加的是认证配置
    @Configuration
    @EnableWebSecurity
    public class SecureConfig extends WebSecurityConfigurerAdapter {
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf()
                    .disable()
                    .authorizeRequests()
                    .antMatchers("/oauth/**", "/login/**", "/logout/**")
                    .permitAll()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .formLogin()
                    .permitAll();
        }
    }
    
  • 添加需要登录的接口用于测试

    @RestController
    @RequestMapping("/user")
    public class UserController {
        @GetMapping("/getCurrentUser")
        public Object getCurrentUser(Authentication authentication) {
            return authentication.getPrincipal();
        }
    }
    

12.2.2 授权码模式使用

13 Spring Cloud Security:Oauth2结合JWT使用

13.1 概述

13.1.1 Security结合Oauth2

  • Spring Cloud Security 为构建安全的SpringBoot应用提供了一系列解决方案,结合Oauth2还可以实现更多功能,比如使用JWT令牌存储信息,刷新令牌功能

13.1.2 JWT

13.2 整合JWT

  • 之前是把令牌存储在内存中的,这样如果部署多个服务,就会导致无法使用令牌的问题。 Spring Cloud Security中有两种存储令牌的方式可用于解决该问题,一种是使用Redis来存储,另一种是使用JWT来存储

13.2.1 使用Redis存储令牌

  • 添加依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  • 配置文件添加redis配置

    server:
      port: 9401
    spring:
      application:
        name: oauth2-service
      redis:
        host: 123.57.66.144
        port: 6379
        password: nidi1995230
    
  • 添加redis存储令牌的配置类

    @Configuration
    public class ReisTokenStoreConfig {
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
    
        @Bean
        public TokenStore redisTokenStore() {
            return new RedisTokenStore(redisConnectionFactory);
        }
    }
    
  • 在认证服务器配置中指定令牌的存储策略为Redis

    • 首先在类中注入了redisTokenStore
    • 之后在endpoints中指定了token存储策略为redis存储
    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserService userService;
    
        @Autowired
        @Qualifier("redisTokenStore")
        private TokenStore tokenStore;
    
        /**
         * 使用密码模式需要配置
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userService)
                    .tokenStore(tokenStore); // 指定token存储策略
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("admin") // 配置client_id
                    .secret(passwordEncoder.encode("123456")) // 配置client_secret
                    .accessTokenValiditySeconds(3600) // 配置token有效期
                    .refreshTokenValiditySeconds(86400) // 设置刷新token有效期
                    .redirectUris("http://www.baidu.com") // 配置登录成功跳转页面
                    .scopes("all") // 设置申请的权限范围
                    .authorizedGrantTypes("authorization_code","password"); // 配置grant_type,表示授权类型
        }
    }
    
  • 使用授权码方式测试,发现已经存储到redis当中

    image-20201126224341906

13.2.2 使用JWT存储令牌

  • 配置JWT内容增强器

    • 可以看出是给token增加了额外的信息
    public class JwtTokenEnhancer implements TokenEnhancer {
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            Map<String,Object> info = new HashMap<>();
            info.put("enhance","enhance info");
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
            return accessToken;
        }
    }
    
  • 添加使用JWT存储令牌的配置类

    • 可以看出先获取JwtAccessTokenConverter并设置秘钥,之后通过JwtAccessTokenConverter生成JwtTokenStore,进而向容器中注入了TokenEnhancer
    @Configuration
    public class JwtTokenStoreConfig {
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
            accessTokenConverter.setSigningKey("test_key"); //配置JWT使用的秘钥
            return accessTokenConverter;
        }
        
        @Bean
        public TokenStore jwtTokenStore() {
            return new JwtTokenStore(jwtAccessTokenConverter());
        }
        
     	@Bean()
        public JwtTokenEnhancer jwtTokenEnhancer() {
            return new JwtTokenEnhancer();
        }
    }
    
  • 在认证服务器配置中指定令牌的存储策略为JWT

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserService userService;
    
        @Autowired
        @Qualifier("jwtTokenStore")
        private TokenStore tokenStore;
    
        @Autowired
        private JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Autowired
        private JwtTokenEnhancer jwtTokenEnhancer;
    
        /**
         * 使用密码模式需要配置
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userService)
                    .tokenStore(tokenStore) // 指定token存储策略
                    .accessTokenConverter(jwtAccessTokenConverter);
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("admin") // 配置client_id
                    .secret(passwordEncoder.encode("123456")) // 配置client_secret
                    .accessTokenValiditySeconds(3600) // 配置token有效期
                    .refreshTokenValiditySeconds(86400) // 设置刷新token有效期
                    .redirectUris("http://www.baidu.com") // 配置登录成功跳转页面
                    .scopes("all") // 设置申请的权限范围
                    .authorizedGrantTypes("authorization_code","password"); // 配置grant_type,表示授权类型
        }
    }
    
  • 使用授权码方式测试

  • jwt.io/网站解析access_token发现已经解析到了对应的内容

    image-20201126231159161

13.2.3 增强JWT中存储的内容

  • 添加一个类继承TokenEnhancer实现一个JWT内容增强器(13.2.2中已经提及)

    image-20201126231534533

  • 创建一个JwtTokenEnhancer实例(13.2.2中已经提及)

  • 在认证服务器中配置JWT内容增强器

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserService userService;
    
        @Autowired
        @Qualifier("jwtTokenStore")
        private TokenStore tokenStore;
    
        @Autowired
        private JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Autowired
        private JwtTokenEnhancer jwtTokenEnhancer;
    
        /**
         * 使用密码模式需要配置
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
            List<TokenEnhancer> delegates = new ArrayList<>();
            delegates.add(jwtTokenEnhancer); //配置JWT的内容增强器
            delegates.add(jwtAccessTokenConverter);
            enhancerChain.setTokenEnhancers(delegates);
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userService)
                    .tokenStore(tokenStore) //配置令牌存储策略
                    .accessTokenConverter(jwtAccessTokenConverter)
                    .tokenEnhancer(enhancerChain);
        }
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("admin") // 配置client_id
                    .secret(passwordEncoder.encode("123456")) // 配置client_secret
                    .accessTokenValiditySeconds(3600) // 配置token有效期
                    .refreshTokenValiditySeconds(86400) // 设置刷新token有效期
                    .redirectUris("http://www.baidu.com") // 配置登录成功跳转页面
                    .scopes("all") // 设置申请的权限范围
                    .authorizedGrantTypes("authorization_code","password"); // 配置grant_type,表示授权类型
        }
    }
    
  • 运行项目后使用密码模式来获取令牌,之后对令牌进行解析,发现已经包含扩展的内容

    {
      "user_name": "macro",
      "scope": [
        "all"
      ],
      "exp": 1572683821,
      "authorities": [
        "admin"
      ],
      "jti": "1ed1b0d8-f4ea-45a7-8375-211001a51a9e",
      "client_id": "admin",
      "enhance": "enhance info"
    }
    

12.2.4 Java中解析JWT中的内容

  • 如果需要获取JWT中的信息,可以使用一个叫jjwt的工具包

  • 添加依赖

    • 注意:hutool工具包的使用
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-core -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-core</artifactId>
        <version>5.5.1</version>
    </dependency>
    
  • 修改UserController类,使用jjwt工具类来解析Authorization头中存储的JWT内容

    @RestController
    @RequestMapping("/user")
    public class UserController {
        @GetMapping("/getCurrentUser")
        public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
            String authorization = request.getHeader("Authorization");
            String token = StrUtil.subAfter(authorization, "bearer ", false);
            return Jwts.parser()
                    .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))
                    .parseClaimsJws(token)
                    .getBody();
        }
    }
    
  • 将令牌放入Authorization头中,访问如下地址获取信息:http://localhost:9401/user/getCurrentUser

    img

12.2.5 刷新令牌

  • 在Spring Cloud Security 中使用oauth2时,如果令牌失效了,可以使用刷新令牌通过refresh_token的授权模式再次获取access_token

  • 只需修改认证服务器的配置,添加refresh_token的授权模式即可

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    .withClient("admin")
                    .secret(passwordEncoder.encode("admin123456"))
                    .accessTokenValiditySeconds(3600)
                    .refreshTokenValiditySeconds(864000)
                    .redirectUris("http://www.baidu.com")
                    .autoApprove(true) //自动授权配置
                    .scopes("all")
                    .authorizedGrantTypes("authorization_code","password","refresh_token"); //添加授权模式
        }
    }
    
  • 使用刷新令牌模式来获取新的令牌,访问如下地址:http://localhost:9401/oauth/token

    • 第一次访问时不仅获得了access_token,同时也获得了refresh_token

      image-20201127010918557

    • 第二次通过refresh_key获取新的令牌,访问http://localhost:9401/oauth/token

      img

13 Nacos(注册中心、配置中心)

13.1 概述

Nacos致力于发现、配置和管理微服务,Nacos提供了一组简单易用的特性集,快读实现动态服务发现、服务配置、服务元数据及流量管理

  • Nacos特性
    • 服务发现和服务健康监测:支持基于DNS和基于RPC的服务发现,支持对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求
    • 动态配置服务:动态配置服务可以让您以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置
    • 动态 DNS 服务:动态 DNS 服务支持权重路由,让您更容易地实现中间层负载均衡、更灵活的路由策略、流量控制以及数据中心内网的简单DNS解析服务
    • 服务及其元数据管理:支持从微服务平台建设的视角管理数据中心的所有服务及元数据

13.2 使用Nacos作为注册中心

13.2.1 创建应用注册到Nacos(nacos-user-service)

  • 依赖

    如果使用Spring Cloud Alibaba的组件都需要在pom.xml中添加该配置

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    

    添加Nacos相关依赖

    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
  • 配置文件

    • spring.cloud.nacos.discovery.server-addr代表配置nacos的注册中心地址
    server:
      port: 8206
    spring:
      application:
        name: nacos-user-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #配置Nacos地址
    management:
      endpoints:
        web:
          exposure:
            include: '*'
    
  • 添加作为Nacos客户端服务被发现功能

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

    启动nacos-user-service服务之后,访问http://localhost:8848/nacos/发现服务已存在

    image-20201130104347109

13.2.2 创建模块演示Nacos负载均衡能力(nacos-user-service)

  • 依赖

    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                <version>2.2.6.RELEASE</version>
            </dependency>
    
  • 配置文件

    • service-url.nacos-user-service用来通过@Value注解调用user服务
    server:
      port: 8308
    spring:
      application:
        name: nacos-ribbon-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    service-url:
      nacos-user-service: http://nacos-user-service
    
  • 添加作为Nacos客户端服务被发现功能

    @SpringBootApplication
    @EnableDiscoveryClient
    public class NacosRibbonServiceApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(NacosRibbonServiceApplication.class, args);
        }
    
    }
    
  • 运行两个nacos-user-service(以不同配置文件启动)和一个nacos-ribbon-service,通过服务管理中心监测

    image-20201130110611366

  • 通过nacos-ribbon-service的接口实际调用nacos-user-service的服务观察负载均衡,调用接口:http://localhost:8308/user/1

    image-20201130111007990

    可以看出同一服务名下的两个服务交替调用

13.3 使用Nacos作为配置中心

13.3.1 创建模块演示配置管理(nacos-config-client)

  • 依赖

    <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            </dependency>
    
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
  • 配置文件

    添加配置文件application.yml,启用的是dev环境的配置

    spring:
      profiles:
        active: dev
    

    添加配置文件bootstrap.yml,主要是对Nacos的作为配置中心的功能进行配置

    • Nacos同config配置中心一样,需要先从云端配置中心拉取配置,故bootstrap优先级高于application

    • spring.cloud.nacos.config.server-addr表示配置中心的地址,而file-extension代表配置中心文件的类型,可以是properties或者yaml

    server:
      port: 9101
    spring:
      application:
        name: nacos-config-client
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #Nacos地址
          config:
            server-addr: localhost:8848 #Nacos地址
            file-extension: yaml #这里我们获取的yaml格式的配置
    
  • 创建ConfigClientController,从Nacos配置中心中获取配置信息

    • config.info代表从配置中心中获取对应的yaml配置文件(配置文件中的config.info)
    • @RefreshScope:SpringCloud原生注解实现配置自动刷新
    @RestController
    @RefreshScope
    public class ConfigClientController {
    
        @Value("${config.info}")
        private String configInfo;
    
        @GetMapping("/configInfo")
        public String getConfigInfo() {
            return configInfo;
        }
    }
    

13.3.2 在Nacos中添加配置

  • Nacos中的dataid组成格式和SpringBoot配置文件的属性关系

    ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
    

    比如说我们现在要获取应用名称为nacos-config-client的应用在dev环境下的yaml配置

    nacos-config-client-dev.yaml
    
  • 在nacos-config-client-dev.yaml添加配置

    config:
      info: "config info for dev"
    
  • 启动nacos-config-client,调用接口查看配置信息:http://localhost:9101/configInfo

    image-20201130114010921

13.3.3 Nacos的动态刷新配置

只要修改下Nacos中的配置信息,再次调用查看配置的接口,就会发现配置已经刷新,Nacos和Consul一样都支持动态刷新配置;在Nacos页面上修改配置并发布后,应用会刷新配置并打印如下信息

2019-11-06 14:50:49.460  INFO 12372 --- [-localhost_8848] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$ec395f8e] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-11-06 14:50:49.608  INFO 12372 --- [-localhost_8848] c.a.c.n.c.NacosPropertySourceBuilder     : Loading nacos data, dataId: 'nacos-config-client-dev.yaml', group: 'DEFAULT_GROUP'
2019-11-06 14:50:49.609  INFO 12372 --- [-localhost_8848] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='NACOS', propertySources=[NacosPropertySource {name='nacos-config-client-dev.yaml'}, NacosPropertySource {name='nacos-config-client.yaml'}]}
2019-11-06 14:50:49.610  INFO 12372 --- [-localhost_8848] o.s.boot.SpringApplication               : The following profiles are active: dev
2019-11-06 14:50:49.620  INFO 12372 --- [-localhost_8848] o.s.boot.SpringApplication               : Started application in 0.328 seconds (JVM running for 172.085)
2019-11-06 14:50:49.638  INFO 12372 --- [-localhost_8848] o.s.c.e.event.RefreshEventListener       : Refresh keys changed: [config.info]

13.3.4 Nacos的分类配置

一个大型分布式微服务系统有很多个微服务子模块,而每个微服务又有相应的开发环境、测试环境和正式环境...,Nacos通过命名空间、分组名称和Dataid组成不同的配置文件

image-20201130205410790

默认情况:NameSpace=public,Group=DEFAULT_GROUP,默认Cluster为DEFAULT

比如说现在有三个环境,开发、测试和生产环境;可以创建三个NameSpace分别代表三种环境

Group可以把不同的微服务划分到同一个分组中

Service就是微服务,一个Service可以包含多个Cluster(集群),Cluster是对指定微服务的一个虚拟划分

配置不同的Group_Id

在配置中心配置不同分组的配置文件之后,只需要在微服务的bootstrap.yml中指定group即可

image-20201130211226947

配置不同的NameSpace

配置中心新建命名空间时会生成随机的命名空间ID

image-20201130211621997

在bootstrap.yml配置文件中指定命名空间

image-20201130211746245

13.4 Nacos集群部署

Nacos默认自带嵌入式数据库derby,但如果是集群模式就不能使用自带数据库,不然就会导致每个节点一个数据库,数据不统一,因此需要使用外部mysql数据库

13.4.1 目标架构

image-20201130213152078

13.4.2 从derby数据库转向mysql

  • 在nacos-server/conf找到nacos-mysql.sql脚本文件,创建nacos_config数据库,并在数据库中执行脚本

    image-20201130213550381

    image-20201130213842589

  • 在nacos-server-1.1.4\nacos\conf目录下找到application.properties,在文件最后追加msyql数据源配置

    spring.datasource.platform=mysql
    
    db.num=1
    db.url.0=jdbc:mysql://123.57.66.144:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
    db.user=root
    db.password=youdontknow
    
  • 重启Nacos

    当在配置中心新建一个配置文件时

    image-20201130214854606

    mysql数据库就会更新该配置信息

    image-20201130214927773

13.4.3 Nacos集群部署

14 Sentinel(熔断与限流)

14.1 概述

随着微服务的流行,服务之间的稳定性变得越来越重要,Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性

Sentinel特性

  • 丰富的应用场景:承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀,可以实时熔断下游不可用应用
  • 完备的实时监控:同时提供实时的监控功能,可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况
  • 广泛的开源生态:提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合
  • 完善的 SPI 扩展点:提供简单易用、完善的 SPI 扩展点,您可以通过实现扩展点,快速的定制逻辑

image-20201130114910079

14.2 安装Sentinel控制台

Sentinel控制台是一个轻量级的控制台应用,它可用于实时查看单机资源监控及集群资源汇总,并提供了一系列的规则管理功能,如流控规则、降级规则、热点规则等

  • 官网下载Sentinel控制台,地址:github.com/alibaba/Sen…

  • 终端输入命令运行控制台

    java -jar sentinel-dashboard-1.8.0.jar
    
  • Sentinel控制台默认运行在8080端口上,登录账号密码均为sentinel,通过如下地址可以进行访问:http://localhost:8080

    image-20201130121656512

  • Sentinel控制台可以查看单台机器的实时监控数据

    image-20201130221225530

14.3 演示Sentinel熔断和限流功能

14.3.1 创建模块演示注册到Sentinel(sentinel-service)

  • 添加依赖,使用nacos作为注册中心

    <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-datasource-nacos</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency><dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba.csp</groupId>
                <artifactId>sentinel-datasource-nacos</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>4.6.3</version>
            </dependency>
        </dependencies>
    
  • 配置文件,主要配置Nacos和Sentinel控制台的地址

    • spring.cloud.sentinel.transport.dashboard配置dashborad地址,port配置sentinel的端口号
    server:
      port: 8401
    spring:
      application:
        name: sentinel-service
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848 #配置Nacos地址
        sentinel:
          transport:
            dashboard: localhost:8080 #配置sentinel dashboard地址
            port: 8719 #默认8719,假如被占用了会自动从8719开始依次+1扫描。直至找到未被占用的端口
    service-url:
    	user-service: http://nacos-user-service       
    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  • 测试

    当启动sentinel-service服务时,发现Sentinel Dashboard并没有出现对sentinel-service服务的监控

    原因是Sentinel采用懒加载的方式,只有当该服务被调用时才会加载到Dashboard页面

    image-20201130130422586

14.3.2 演示Sentinel限流功能

流量控制规则

image-20201130221312641

Sentinel Starter 默认为所有的 HTTP 服务提供了限流埋点,同样可以通过使用@SentinelResource来自定义一些限流行为

  • 配置Ribbon负载均衡

    package org.jiang.config;
    @Configuration
    public class RibbonConfig {
    
        @Bean
        @SentinelRestTemplate
        public RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    
  • 超过限流出现异常时使用自定义异常处理类CustomBlockHandler

    package org.jiang.handler;
    public class CustomBlockHandler {
    
        public CommonResult handleException(BlockException exception){
            return new CommonResult("自定义限流信息",200);
        }
    }
    
  • 创建RateLimitController测试熔断限流功能

    @RestController
    @RequestMapping("/rateLimit")
    public class RateLimitController {
    
        /**
         * 按资源名称限流,需要指定限流处理逻辑
         */
        @GetMapping("/byResource")
        @SentinelResource(value = "byResource",blockHandler = "handleException")
        public CommonResult byResource() {
            return new CommonResult("按资源名称限流", 200);
        }
    
        /**
         * 按URL限流,有默认的限流处理逻辑
         */
        @GetMapping("/byUrl")
        @SentinelResource(value = "byUrl",blockHandler = "handleException")
        public CommonResult byUrl() {
            return new CommonResult("按url限流", 200);
        }
    
        /**
         * 自定义通用的限流处理逻辑
         */
        @GetMapping("/customBlockHandler")
        @SentinelResource(value = "customBlockHandler", blockHandler = "handleException",blockHandlerClass = CustomBlockHandler.class)
        public CommonResult blockHandler() {
            return new CommonResult("限流成功", 200);
        }
    
        public CommonResult handleException(BlockException exception){
            return new CommonResult(exception.getClass().getCanonicalName(),200);
        }
    
    }
    
按照资源名称限流

根据@SentinelResource注解中定义的value(资源名称)来进行限流操作,并指定限流处理逻辑规则

  • 在Sentinel控制台配置流控规则,根据@SentinelResource注解的value值

    img

  • 当超过每秒钟1次访问接口之后,发现返回了自定义的限流处理信息

    接口:http://localhost:8401/rateLimit/byResource

    image-20201130222740441

根据URL限流
  • 在Sentinel控制台配置流控规则,使用访问的URL

    img

当超过每秒钟1次访问接口之后,发现返回了自定义的限流处理信息

接口:http://localhost:8401/rateLimit/byUrl

image-20201130223910879

关联

当与一个接口A关联的接口B达到了域值,就对A进行限流;比如说支付模块达到了域值,就对订单模块进行限流,防止新的订单产生

  • 在Sentinel控制台配置流控规则,

    image-20201130223428807

  • 当byResource资源达到域值之后,byUrl资源返回了自定义的限流处理信息

    image-20201130223918172

14.3.3 演示Sentinel降级功能

降级规则

Sentinel的断路器没有半开状态(半开状态系统自动去检测请求是否有异常,没有异常就关闭断路器恢复使用,有异常就继续打开断路器不可用)

image-20201130224636924

  • RT(平均响应时间,秒级)

    平均响应时间超出域值且在时间窗口内通估计的请求>=5两个条件同时满足后出发降级

    窗口期过后关闭断路器(就是窗口期类的请求平均响应时间小于域值)

    RT最大为4900(更大的需要通过-Dcsp.sentinel.static.max.rt=xxxx才能生效)

  • 异常比例

    QPS》=5且异常比例(秒级统计)超过域值后出发降级;时间窗口结束后关闭降级

  • 异常数

    异常数(分钟统计)超过阈值后出发降级;时间窗口结束后关闭降级

配置RT

image-20201130230306269

降级规则当一秒钟请求数量超过5个,最大的响应时间超过200毫秒时就发生服务熔断,熔断时间为1秒钟

配置异常比例

image-20201130230929603

降级规则为一秒钟内请求的数据超过5个,且异常请求的比例超过50%就发生服务熔断,熔断时间为1秒钟

配置异常数

image-20201130231236061

降级规则为一秒钟内请求的数据超过5个,且异常请求的数量超过3个就发生服务熔断,熔断时间为1秒钟

14.3.4 Sentinel热点限

热点

经常访问的数据,大多数时候希望统计某个热点数据中访问频次最高的Top K数据,并对其访问进行限制

  • 商品ID为参数,统计一段时间内最长购买的商品ID进行限制
  • 用户ID为参数,统计一段时间内频繁访问的客户ID进行限制
配置普通热点限流

image-20201130232501861

限流规则请求的资源且添加了第0个索引(类似于www.baidu.com/query,query就是第0个参数),在一秒钟之内请求的次数超过1次,则会进行限流

注意:如果没有配置blockHandler方法,就会返回error_page页面

@SentinelResource(value = "byUrl",blockHandler = "handleException")
配置特别限流规则(配置参数例外项)

image-20201130233203224

当第0个参数的值为5时限流规则就发生了改变,只有一秒钟之内请求的次数超过了200次才会进行限流

14.3.5 自定义限流处理逻辑

通过@SentinelResource可以指定自定义通用的限流处理逻辑

  • 创建CustomBlockHandler类用于自定义限流处理逻辑
public class CustomBlockHandler {

    public CommonResult handleException(BlockException exception){
        return new CommonResult("自定义限流信息",200);
    }
}
  • 在RateLimitController中使用自定义限流处理逻辑

    在@SentinelResource注解中首先指定了资源名为customBlockHandler,进而指定了限流处理的类为CustomBlockHandler,限流处理方法为handleException

    @RestController
    @RequestMapping("/rateLimit")
    public class RateLimitController {
    
        /**
         * 自定义通用的限流处理逻辑
         */
        @GetMapping("/customBlockHandler")
        @SentinelResource(value = "customBlockHandler", blockHandler = "handleException",blockHandlerClass = CustomBlockHandler.class)
        public CommonResult blockHandler() {
            return new CommonResult("限流成功", 200);
        }
    
    }
    

14.3.6 Sentinel熔断功能

Sentinel支持对服务间调用进行保护,对故障应用进行熔断操作

通过RestTemplate来调用nacos-user-service服务所提供的接口进行演示
  • 使用@SentinelRestTemplate包装RestTemplate实例

    @Configuration
    public class RibbonConfig {
    
        @Bean
        @SentinelRestTemplate
        public RestTemplate restTemplate(){
            return new RestTemplate();
        }
    }
    
  • 定义CircleBreakerController类,定义对nacos-user-service提供接口的调用

    /**
     * 熔断功能
     * Created by macro on 2019/11/7.
     */
    @RestController
    @RequestMapping("/breaker")
    public class CircleBreakerController {
    
        private Logger LOGGER = LoggerFactory.getLogger(CircleBreakerController.class);
        @Autowired
        private RestTemplate restTemplate;
        @Value("${service-url.user-service}")
        private String userServiceUrl;
    
        @RequestMapping("/fallback/{id}")
        @SentinelResource(value = "fallback",fallback = "handleFallback")
        public CommonResult fallback(@PathVariable Long id) {
            return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
        }
    
        @RequestMapping("/fallbackException/{id}")
        @SentinelResource(value = "fallbackException",fallback = "handleFallback2", exceptionsToIgnore = {NullPointerException.class})
        public CommonResult fallbackException(@PathVariable Long id) {
            if (id == 1) {
                throw new IndexOutOfBoundsException();
            } else if (id == 2) {
                throw new NullPointerException();
            }
            return restTemplate.getForObject(userServiceUrl + "/user/{1}", CommonResult.class, id);
        }
    
        public CommonResult handleFallback(Long id) {
            User defaultUser = new User(-1L, "defaultUser", "123456");
            return new CommonResult<>(defaultUser,"服务降级返回",200);
        }
    
        public CommonResult handleFallback2(@PathVariable Long id, Throwable e) {
            LOGGER.error("handleFallback2 id:{},throwable class:{}", id, e.getClass());
            User defaultUser = new User(-2L, "defaultUser2", "123456");
            return new CommonResult<>(defaultUser,"服务降级返回",200);
        }
    }
    
  • 启动nacos-user-service和sentinel-service服务

  • 由于我们并没有在nacos-user-service中定义id为4的用户,所有访问如下接口会返回服务降级 请求地址:http://localhost:8401/breaker/fallback/4

    {
    	"data": {
    		"id": -1,
    		"username": "defaultUser",
    		"password": "123456"
    	},
    	"message": "服务降级返回",
    	"code": 200
    }
    
  • 当使用了exceptionsToIgnore参数忽略了NullPointerException,所以我们访问接口报空指针时不会发生服务降级:http://localhost:8401/breaker/fallbackException/2

    img

14.3.7 @SentinelResource中的fallback和brokerHandler对比

fallback

当程序运行时出现了运行时异常,如果指定了fallback对应的兜底方法,就会按照兜底方法返回数据;如果没有配置就返回错误页面

fallback和brokerHandler同时存在

当程序运行时出现运行时异常,而且没有达到sentinel服务端配置的降级要求,就会按照fallback方法执行;一旦达到降级要求,就按照brokerHandler指定的方法执行

14.3.8 Sentinel和Feign结合

  • 在sentinel-service服务上添加依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
  • 修改配置文件打开sentinel对feign的支持

    feign:
      sentinel:
        enabled: true #打开sentinel对feign的支持
    
  • 在应用启动类上添加@EnableFeignClients启动Feign的功能

  • 创建一个UserService接口,用于定义对nacos-user-service服务的调用

    @FeignClient(value = "nacos-user-service",fallback = UserFallbackService.class)
    public interface UserService {
        @PostMapping("/user/create")
        CommonResult create(@RequestBody User user);
    
        @GetMapping("/user/{id}")
        CommonResult<User> getUser(@PathVariable Long id);
    
        @GetMapping("/user/getByUsername")
        CommonResult<User> getByUsername(@RequestParam String username);
    
        @PostMapping("/user/update")
        CommonResult update(@RequestBody User user);
    
        @PostMapping("/user/delete/{id}")
        CommonResult delete(@PathVariable Long id);
    }
    
  • 创建UserFallbackService类实现UserService接口,用于处理服务降级逻辑

    注意:@Component注解一定要加上,将组建注册到容器中

    @Component
    public class UserFallbackService implements UserService {
        @Override
        public CommonResult create(User user) {
            User defaultUser = new User(-1L, "defaultUser", "123456");
            return new CommonResult<>(defaultUser,"服务降级返回",200);
        }
    
        @Override
        public CommonResult<User> getUser(Long id) {
            User defaultUser = new User(-1L, "defaultUser", "123456");
            return new CommonResult<>(defaultUser,"服务降级返回",200);
        }
    
        @Override
        public CommonResult<User> getByUsername(String username) {
            User defaultUser = new User(-1L, "defaultUser", "123456");
            return new CommonResult<>(defaultUser,"服务降级返回",200);
        }
    
        @Override
        public CommonResult update(User user) {
            return new CommonResult("调用失败,服务被降级",500);
        }
    
        @Override
        public CommonResult delete(Long id) {
            return new CommonResult("调用失败,服务被降级",500);
        }
    }
    
  • 在UserFeignController中使用UserService通过Feign调用nacos-user-service服务中的接口

    @RestController
    @RequestMapping("/user")
    public class UserFeignController {
        @Autowired
        private UserService userService;
    
        @GetMapping("/{id}")
        public CommonResult getUser(@PathVariable Long id) {
            return userService.getUser(id);
        }
    
        @GetMapping("/getByUsername")
        public CommonResult getByUsername(@RequestParam String username) {
            return userService.getByUsername(username);
        }
    
        @PostMapping("/create")
        public CommonResult create(@RequestBody User user) {
            return userService.create(user);
        }
    
        @PostMapping("/update")
        public CommonResult update(@RequestBody User user) {
            return userService.update(user);
        }
    
        @PostMapping("/delete/{id}")
        public CommonResult delete(@PathVariable Long id) {
            return userService.delete(id);
        }
    }
    
  • 调用http://localhost:8401/user/4接口会发生服务降级,返回服务降级处理信息

    {
    	"data": {
    		"id": -1,
    		"username": "defaultUser",
    		"password": "123456"
    	},
    	"message": "服务降级返回",
    	"code": 200
    }
    

14.3.9 使用Nacos持久化存储Sentinel配置规则

默认情况下,当我们在Sentinel控制台中配置规则时,控制台推送规则方式是通过API将规则推送至客户端并直接更新到内存中。一旦我们重启应用,规则将消失

原理

img

  • 首先直接在配置中心创建规则,配置中心将规则推送到客户端
  • Sentinel控制台也从配置中心去获取配置信息
持久化功能演示(在sentinel-service服务演示)
  • 添加依赖

    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    
  • 修改配置文件,添加Nacos数据源配置

    ds1代表datasource1,server-addr就是nacos服务器地址,dataID、groupId和data-type组合起来就对应着一个配置文件,rule-tpype代表的是规则

    spring:
      cloud:
        sentinel:
          datasource:
            ds1:
              nacos:
                server-addr: localhost:8848
                dataId: ${spring.application.name}-sentinel
                groupId: DEFAULT_GROUP
                data-type: json
                rule-type: flow
    
  • Nacos中添加配置

    img

  • 配置信息

    [
        {
            "resource": "/rateLimit/byUrl",
            "limitApp": "default",
            "grade": 1,
            "count": 1,
            "strategy": 0,
            "controlBehavior": 0,
            "clusterMode": false
        }
    ]
    
    • resource:资源名称

    • limitApp:来源应用

    • grade:阈值类型,0表示线程数,1表示QPS

    • count:单机阈值

    • strategy:流控模式,0表示直接,1表示关联,2表示链路

    • controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待

    • clusterMode:是否集群

  • 再次登录sentinel控制台已经有了对应限流规则

    img


个人公众号目前正初步建设中,如果喜欢可以关注我的公众号,谢谢!

二维码