架构师学习第22周——Bus,Gateaway
Bus简介
这里我们最重要的作用就是利用Bus总线去同时刷新我们多台服务器的配置文件。
总线式架构的配置中心
Demo使用
Config-bus-server
bootstrap.yml
encrypt:
key: 20051001
application.yml
spring:
application:
name: config-bus-server
rabbitmq:
host: 192.168.31.183
port: 5672
username: admin
password: admin123
cloud:
config:
server:
git:
uri: https://github.com/yangzhao-feng/config-repo/
# 强制拉取资源文件
# search-paths:
# username:
# password:
# 可以配置多个注册中心,根据pattern匹配
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20000/eureka/
server:
port: 60002
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
启动类
package com.imooc.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient
public class ConfigBusServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigBusServerApplication.class,args);
}
}
config-bus-client模块
bootstrap.yml
spring:
application:
name: config-bus-client
rabbitmq:
host: 192.168.31.183
port: 5672
username: admin
password: admin123
cloud:
stream:
default-binder: rabbit
config:
# uri: http://localhost:60000
discovery:
enabled: true
service-id: config-bus-server
profile: prod
label: master
name: config-consumer
server:
port: 61001
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20000/eureka/
##//从github中拉取属性文件,再从本地的属性文件通过@Value(..)拉取到我们的属性中
#myWords: ${words}
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
启动类
package com.imooc.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class ConfigBusClientApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigBusClientApplication.class,args);
}
}
Controller
package com.imooc.springcloud;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Controller {
@Value("${name}")
private String name;
@Value("${food}")
private String food;
//@Value("${myWords}")
@Value("${words}")
private String words;
@GetMapping("/name")
public String getName()
{
return name;
}
@GetMapping("/words")
public String getWords()
{
return words;
}
@GetMapping("/dinner")
public String dinner(){
return "May I have one " + food;
}
}
RefreshController
package com.imooc.springcloud;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/refresh")
@RefreshScope
public class RefreshController {
@Value("${words}")
private String words;
@GetMapping("/words")
public String getWords()
{
return words;
}
//http://localhost:60002/actuator/bus-refresh
}
更改client的端口号作为配置中心的多个子节点使,用postman发送http://localhost:60002/actuator/bus-refresh,刷新后发现多个子节点的属性刷新成功,而且只要我们刷新某个子节点其他的子节点也会一起刷新。
Gateway
Gateway使用Demo
创建gateway-sample模块
application.yml
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20000/eureka/
##//从github中拉取属性文件,再从本地的属性文件通过@Value(..)拉取到我们的属性中
#myWords: ${words}
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
server:
port: 65000
启动类
package com.imooc.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}
启动我们的Eureka服务中心以及两个Feign-Client,端口号位40002和40003,gateway端口号位65000,使用postman访问http://localhost:65000/feign-client/sayHi访问Feign-Client中的接口,第一次访问返回端口40002,第二次返回40003,即默认轮询规则。
添加路由:
用postman发送post请求http://localhost:65000/actuator/gateway/routes/dynamic
最后的dynamic位自定义的id名
请求体
{
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/dynamic/**"
}
}],
"filters": [
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
}],
"uri": "lb://FEIGN-CLIENT",
"order": 0
}
返回201创建成功
http://localhost:65000/actuator/gateway/routes查看,名为dynamic的路由规则已经成功添加。
验证路由规则:
利用postman发送Get请求http://localhost:65000/dynamic/sayHi,成功返回,规则验证成功。
删除路由:
利用postman发送Delete请求http://localhost:65000/actuator/gateway/routes/dynamic,其中最后一个式创建路由规则名,返回200,删除成功。
Path断言
使用yml配置路由规则
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: feignclient
uri: lb://FEIGN-CLIENT
predicates:
- Path=/yml/**
filters:
#localhost:port/yml/......——>localhost:port/.......
- StripPrefix=1
java配置(推荐使用)路由:
package com.imooc.springcloud;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
@Configuration
public class GatewayConfiguration {
@Bean
@Order
public RouteLocator customizedRoutes(RouteLocatorBuilder builder)
{
return builder.routes()
//请求路径满足/java/**
.route(r -> r.path("/java/**")
//请求必须是Get请求
.and().method(HttpMethod.GET)
//请求头里必须有name属性
.and().header("name")
.filters(f -> f.stripPrefix(1)
//Respoonse中添加属性位java-param,值为gateway-config
.addResponseHeader("java-param","gateway-config"))
.uri("lb://FEIGN-CLIENT")
)
.build();
}
}
After断言实现简易秒杀
在Feign-Client中添加一个Controller
package com.imooc.springcloud;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
@Slf4j
@RequestMapping("gateway")
public class GatewayController {
public static final Map<Long,Product> items = new ConcurrentHashMap<>();
//查看或添加商品接口
@ResponseBody
@RequestMapping(value = "details",method = RequestMethod.GET)
public Product get(@RequestParam("pid") Long pid)
{
if(!items.containsKey(pid))
{
Product prod = Product.builder()
.productId(pid)
.description("好吃不贵")
.stock(100L)
.build();
items.putIfAbsent(pid,prod);
}
return items.get(pid);
}
//下单接口
@RequestMapping(value = "placeOrder",method = RequestMethod.POST)
public String buy(@RequestParam("pid") Long pid){
Product prod = items.get(pid);
if(prod == null)
{
return "Product not found";
}else if(prod.getStock() <= 0){
return "Sold out";
}
synchronized (prod)
{
if(prod.getStock() <= 0L)
{
return "Sold out";
}
prod.setStock(prod.getStock() - 1);
}
return "Order Placed";
}
}
Product类
package com.imooc.springcloud;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class Product {
private Long productId;
private String description;
private Long stock;
}
在gateway-config模块中GatewayConfiguration添加路由规则
package com.imooc.springcloud;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import java.time.ZonedDateTime;
@Configuration
public class GatewayConfiguration {
@Bean
@Order
public RouteLocator customizedRoutes(RouteLocatorBuilder builder)
{
return builder.routes()
.route(r -> r.path("/seckill/**")
//当前这个路由规则只在某个时间之后生效
.and().after(ZonedDateTime.now().plusMinutes(1))
//当前这个路由规则只在某个时间之前生效
//.and().before()
//在两个时间点之间
//.and().between()
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-CLIENT")
)
.build();
}
}
再模块启动的一分钟后规则启动,在该时间范围之外的默认返回404。
自定义过滤器
在gateway-config模块中定义一个TimerFilter
这个过滤器的作用就是打印接口的调用时间及相关信息。
package com.imooc.springcloud;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
//GlobalFilter 全局过滤器
public class TimerFilter implements GatewayFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
StopWatch timer = new StopWatch();
timer.start(exchange.getRequest().getURI().getRawPath());
// exchange.getAttributes().put("requestTimeBegin",System.currentTimeMillis());
//Zull - Filter before / after
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
timer.stop();
log.info(timer.prettyPrint());
})
);
}
@Override
public int getOrder() {
return 0;
}
}
GatewayConfiguration
在之前的基础上的filters中添加我们自定义的filter。
package com.imooc.springcloud;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import java.time.ZonedDateTime;
@Configuration
public class GatewayConfiguration {
@Autowired
private TimerFilter timerFilter;
@Bean
@Order
public RouteLocator customizedRoutes(RouteLocatorBuilder builder)
{
return builder.routes()
.route(r -> r.path("/java/**")
//请求必须是Get请求
.and().method(HttpMethod.GET)
//请求头里必须有name属性
.and().header("name")
.filters(f -> f.stripPrefix(1)
//Respoonse中添加属性位java-param,值为gateway-config
.addResponseHeader("java-param","gateway-config")
.filters(timerFilter)
)
.uri("lb://FEIGN-CLIENT")
)
.route(r -> r.path("/seckill/**")
//当前这个路由规则只在某个时间之后生效
.and().after(ZonedDateTime.now().plusMinutes(1))
//当前这个路由规则只在某个时间之前生效
//.and().before()
//在两个时间点之间
//.and().between()
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-CLIENT")
)
.build();
}
}
我们还可以使用全局Filter,只需将过滤器继承GatewayFilter换成GlobalFilter,即可并且不需要再任何一个匹配类中再添加自定义的过滤器。作用是过滤所有的路由,并将执行时间打印出来。
除了执行时间我们还可以做我们想做的其他操作。
package com.imooc.springcloud;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
@Component
//GlobalFilter 全局过滤器
public class TimerFilter implements GlobalFilter , Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
StopWatch timer = new StopWatch();
timer.start(exchange.getRequest().getURI().getRawPath());
// exchange.getAttributes().put("requestTimeBegin",System.currentTimeMillis());
//Zull - Filter before / after
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
timer.stop();
log.info(timer.prettyPrint());
})
);
}
@Override
public int getOrder() {
return 0;
}
}
实现JWT鉴权
这里只贴出狠心代码
负责鉴权的Controller
JwtService
package com.imooc.test;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.imooc.springcloud.Account;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Date;
@Slf4j
@Service
public class JwtService {
//生产环境不能这么用
private static final String KEY = "changeIt";
private static final String ISSUER = "yao";
private static final long TOKEN_EXPTIME_TIME = 60000;
private static final String USER_NAME = "username";
/**
* 生成Token
* @param acct
* @return
*/
public String token(Account acct)
{
Date now = new Date();
//生成算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
//创建token
String token = JWT.create()
.withIssuer(ISSUER)
.withIssuedAt(now)
.withExpiresAt(new Date(now.getTime() + TOKEN_EXPTIME_TIME))
.withClaim(USER_NAME,acct.getUsername())
.sign(algorithm);
log.info("jwt generated user={}",acct.getUsername());
return token;
}
/**
* 校验Token
* @param token
* @param username
* @return
*/
public boolean verify(String token,String username)
{
log.info("verifying jwt - username={}",username);
try {
//生成对应的算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
//放入对应的信息进行鉴权withIssuer(ISSUER),withClaim(USER_NAME,username)与上面对应
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.withClaim(USER_NAME,username)
.build();
//开始鉴权,如果鉴权不成功,将自动报错。
verifier.verify(token);
}catch (Exception e) {
log.error("auth failed",e);
return false;
}
return true;
}
}
声明一个过滤器AuthFilter,并在Configuration类中需要此过滤器的路由添加此路由。
package com.imooc.springcloud;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component("authFilter")
@Slf4j
public class AuthFilter implements GatewayFilter, Ordered {
private static final String AUTH = "Authorization";
private static final String USERNAME = "imooc-user-name";
@Autowired
private AuthService authService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Auth start");
ServerHttpRequest request = exchange.getRequest();
HttpHeaders header = request.getHeaders();
//获取header中的Authorization信息
String token = header.getFirst(AUTH);
//获取header中的imooc-user-name信息
String username = header.getFirst(USERNAME);
ServerHttpResponse response = exchange.getResponse();
//如果Authorization为空则,返回错误
if (StringUtils.isBlank(token)) {
log.error("token not found");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//如果传入的imooc-user-name信息和Authorization信息校验不成功,返回错误。
AuthResponse resp = authService.verify(token, username);
if (resp.getCode() != 1L) {
log.error("invalid token");
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
// TODO 将用户信息存放在请求header中传递给下游业务
ServerHttpRequest.Builder mutate = request.mutate();
mutate.header("imooc-user-name", username);
ServerHttpRequest buildReuqest = mutate.build();
//todo 如果响应中需要放数据,也可以放在response的header中
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("imooc-username",username);
return chain.filter(exchange.mutate()
.request(buildReuqest)
.response(response)
.build());
}
@Override
public int getOrder() {
return 0;
}
}