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 授权码模式使用
-
启动oauth2-server服务
-
在浏览器访问该地址进行登录授权:http://localhost:9401/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all&state=normal
-
输入账号密码进行登录操作
-
登录之后进行授权操作
-
浏览器会带着授权码跳转到指定的路径
-
使用授权码请求该地址获取访问令牌:http://localhost:9401/oauth/token
-
使用Basic认证通过client_id和client_secret构造一个Authorization头信息
-
在body中添加以下参数信息,通过POST请求获取访问令牌
-
在请求头中添加访问令牌,访问需要登录认证的接口进行测试,http://localhost:9401/user/getCurrentUser
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当中
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发现已经解析到了对应的内容
13.2.3 增强JWT中存储的内容
-
添加一个类继承TokenEnhancer实现一个JWT内容增强器(13.2.2中已经提及)
-
创建一个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
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
-
第二次通过refresh_key获取新的令牌,访问http://localhost:9401/oauth/token
-
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/发现服务已存在
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,通过服务管理中心监测
-
通过nacos-ribbon-service的接口实际调用nacos-user-service的服务观察负载均衡,调用接口:http://localhost:8308/user/1
可以看出同一服务名下的两个服务交替调用
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
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组成不同的配置文件
默认情况:NameSpace=public,Group=DEFAULT_GROUP,默认Cluster为DEFAULT
比如说现在有三个环境,开发、测试和生产环境;可以创建三个NameSpace分别代表三种环境
Group可以把不同的微服务划分到同一个分组中
Service就是微服务,一个Service可以包含多个Cluster(集群),Cluster是对指定微服务的一个虚拟划分
配置不同的Group_Id
在配置中心配置不同分组的配置文件之后,只需要在微服务的bootstrap.yml中指定group即可
配置不同的NameSpace
配置中心新建命名空间时会生成随机的命名空间ID
在bootstrap.yml配置文件中指定命名空间
13.4 Nacos集群部署
Nacos默认自带嵌入式数据库derby,但如果是集群模式就不能使用自带数据库,不然就会导致每个节点一个数据库,数据不统一,因此需要使用外部mysql数据库
13.4.1 目标架构
13.4.2 从derby数据库转向mysql
-
在nacos-server/conf找到nacos-mysql.sql脚本文件,创建nacos_config数据库,并在数据库中执行脚本
-
在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
当在配置中心新建一个配置文件时
mysql数据库就会更新该配置信息
13.4.3 Nacos集群部署
14 Sentinel(熔断与限流)
14.1 概述
随着微服务的流行,服务之间的稳定性变得越来越重要,Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
Sentinel特性
- 丰富的应用场景:承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀,可以实时熔断下游不可用应用
- 完备的实时监控:同时提供实时的监控功能,可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况
- 广泛的开源生态:提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合
- 完善的 SPI 扩展点:提供简单易用、完善的 SPI 扩展点,您可以通过实现扩展点,快速的定制逻辑
14.2 安装Sentinel控制台
Sentinel控制台是一个轻量级的控制台应用,它可用于实时查看单机资源监控及集群资源汇总,并提供了一系列的规则管理功能,如流控规则、降级规则、热点规则等
-
官网下载Sentinel控制台,地址:github.com/alibaba/Sen…
-
终端输入命令运行控制台
java -jar sentinel-dashboard-1.8.0.jar
-
Sentinel控制台默认运行在8080端口上,登录账号密码均为
sentinel
,通过如下地址可以进行访问:http://localhost:8080 -
Sentinel控制台可以查看单台机器的实时监控数据
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页面
14.3.2 演示Sentinel限流功能
流量控制规则
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值
-
当超过每秒钟1次访问接口之后,发现返回了自定义的限流处理信息
接口:http://localhost:8401/rateLimit/byResource
根据URL限流
-
在Sentinel控制台配置流控规则,使用访问的URL
当超过每秒钟1次访问接口之后,发现返回了自定义的限流处理信息
接口:http://localhost:8401/rateLimit/byUrl
关联
当与一个接口A关联的接口B达到了域值,就对A进行限流;比如说支付模块达到了域值,就对订单模块进行限流,防止新的订单产生
-
在Sentinel控制台配置流控规则,
-
当byResource资源达到域值之后,byUrl资源返回了自定义的限流处理信息
14.3.3 演示Sentinel降级功能
降级规则
Sentinel的断路器没有半开状态(半开状态系统自动去检测请求是否有异常,没有异常就关闭断路器恢复使用,有异常就继续打开断路器不可用)
-
RT(平均响应时间,秒级)
平均响应时间超出域值且在时间窗口内通估计的请求>=5两个条件同时满足后出发降级
窗口期过后关闭断路器(就是窗口期类的请求平均响应时间小于域值)
RT最大为4900(更大的需要通过-Dcsp.sentinel.static.max.rt=xxxx才能生效)
-
异常比例
QPS》=5且异常比例(秒级统计)超过域值后出发降级;时间窗口结束后关闭降级
-
异常数
异常数(分钟统计)超过阈值后出发降级;时间窗口结束后关闭降级
配置RT
降级规则当一秒钟请求数量超过5个,最大的响应时间超过200毫秒时就发生服务熔断,熔断时间为1秒钟
配置异常比例
降级规则为一秒钟内请求的数据超过5个,且异常请求的比例超过50%就发生服务熔断,熔断时间为1秒钟
配置异常数
降级规则为一秒钟内请求的数据超过5个,且异常请求的数量超过3个就发生服务熔断,熔断时间为1秒钟
14.3.4 Sentinel热点限
热点
经常访问的数据,大多数时候希望统计某个热点数据中访问频次最高的Top K数据,并对其访问进行限制
- 商品ID为参数,统计一段时间内最长购买的商品ID进行限制
- 用户ID为参数,统计一段时间内频繁访问的客户ID进行限制
配置普通热点限流
限流规则请求的资源且添加了第0个索引(类似于www.baidu.com/query,query就是第0个参数),在一秒钟之内请求的次数超过1次,则会进行限流
注意:如果没有配置blockHandler方法,就会返回error_page页面
@SentinelResource(value = "byUrl",blockHandler = "handleException")
配置特别限流规则(配置参数例外项)
当第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
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将规则推送至客户端并直接更新到内存中。一旦我们重启应用,规则将消失
原理
- 首先直接在配置中心创建规则,配置中心将规则推送到客户端
- 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中添加配置
-
配置信息
[ { "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控制台已经有了对应限流规则
个人公众号目前正初步建设中,如果喜欢可以关注我的公众号,谢谢!