SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
提前声明:Spring Cloud Gateway 底层使用了高性能的通信框架Netty。
- | Gateway | Zuul 1.x | Zuul 2.x |
---|---|---|---|
靠谱性 | 官方支持 | 曾经靠谱过 | 专业放鸽子 |
性能 | Netty | 同步阻塞, 性能慢 | Netty |
RPS | >32000 | 20000左右 | 25000左右 |
Spring Cloud | 已整合 | 已整合 | 暂无整合计划 |
- | Gateway | Zuul 1.x | Zuul 2.x |
---|---|---|---|
长连接 | 支持 | 长不支持 | 支持 |
编程体验 | 比较难 | 比较简单 | 比较难 |
调式&链路追踪 | 比较难 | 无压力 | 比较难 |
- | Springcloud | SpringBoot |
---|---|---|
- | Greenwich.SR1 | 2.1.5.RELEASE |
D:.
├─auth-service
│ └─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─example
│ │ │ └─springcloud
│ │ │ ├─controller
│ │ │ └─service
│ │ └─resources
│ └─test
│ └─java
├─auth-service-api
│ └─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─example
│ │ │ └─springcloud
│ │ │ ├─entity
│ │ │ └─service
│ │ └─resources
│ └─test
│ └─java
└─gateway-sample
└─src
├─main
│ ├─java
│ │ └─com
│ │ └─example
│ │ └─springcloud
│ │ ├─config
│ │ └─filter
│ └─resources
└─test
└─java
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-demo-decartifactId>
<groupId>com.examplegroupId>
<version>1.0.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>auth-service-apiartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
dependencies>
project>
com.example.springcloud.entity.Account
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {
private String username;
private String token;
private String refreshToken;
}
com.example.springcloud.entity.AuthResponse
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private Account account;
private Long code;
}
com.example.springcloud.entity.AuthResponseCode
public class AuthResponseCode {
public static final Long SUCCESS = 1L;
public static final Long INCORRECT_PWD = 1000L;
public static final Long USER_NOT_FOUND = 1001L;
}
com.example.springcloud.service.AuthService
@FeignClient("auth-service")
public interface AuthService {
/**
* 创建token
* @param username str
* @param password str
* @return AuthResponse
*/
@PostMapping("/login")
@ResponseBody
public AuthResponse login(@RequestParam("username") String username,
@RequestParam("password") String password);
/**
* 验证token
* @param token str
* @param username name
* @return AuthResponse
*/
@GetMapping("/verify")
public AuthResponse verify(@RequestParam("token") String token,
@RequestParam("username") String username);
/**
* 刷新token
* @param refresh str
* @return AuthResponse
*/
@PostMapping("/refresh")
@ResponseBody
public AuthResponse refresh(@RequestParam("refresh") String refresh);
}
com.example.springcloud.controller.DemoController
@Slf4j
@RestController
public class DemoController {
@Autowired
private JwtService jwtService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 生成token
* @param username str
* @param password str
* @return AuthResponse
*/
@PostMapping("/login")
@ResponseBody
public AuthResponse login(@RequestParam String username,
@RequestParam String password) {
Account account = Account.builder()
.username(username)
.build();
String token = jwtService.token(account);
account.setToken(token);
account.setRefreshToken(UUID.randomUUID().toString());
redisTemplate.opsForValue().set(account.getRefreshToken(), account);
return AuthResponse.builder()
.account(account)
.code(AuthResponseCode.SUCCESS)
.build();
}
/**
* 刷新token
* @param refreshToken str
* @return AuthResponse
*/
@PostMapping("/refresh")
@ResponseBody
public AuthResponse refresh(@RequestParam String refreshToken) {
Account account = (Account) redisTemplate.opsForValue().get(refreshToken);
if (account == null) {
return AuthResponse.builder()
.code(AuthResponseCode.USER_NOT_FOUND)
.build();
}
String jwt = jwtService.token(account);
account.setToken(jwt);
account.setRefreshToken(UUID.randomUUID().toString());
redisTemplate.delete(refreshToken);
redisTemplate.opsForValue().set(account.getRefreshToken(), account);
return AuthResponse.builder()
.account(account)
.code(AuthResponseCode.SUCCESS)
.build();
}
@GetMapping("/verify")
public AuthResponse verify(@RequestParam String token,
@RequestParam String username) {
boolean success = jwtService.verify(token, username);
return AuthResponse.builder()
.code(success ? AuthResponseCode.SUCCESS : AuthResponseCode.USER_NOT_FOUND)
.build();
}
}
application.yml
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20000/eureka/
info:
app:
description: test
name: auth-service
logging:
file: ${spring.application.name}.log
management:
endpoint:
env:
enabled: false
health:
show-details: always
endpoints:
web:
exposure:
include: '*'
server:
port: 65100
spring:
application:
name: auth-service
redis:
database: 5
host: 192.168.8.100
port: 6379
com.example.springcloud.AuthApplication
@SpringBootApplication
@EnableEurekaClient
public class AuthApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(AuthApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
}
}
com.example.springcloud.service.JwtService
@Slf4j
@Service
public class JwtService {
private static final String KEY = "changIt";
private static final String ISSUER = "eddie";
private static final String USER_NAME = "username";
private static final long TOKEN_EXP_TIME = 60 * 1000; // 60秒
/**
* 生成token
* @param account 账户
* @return str
*/
public String token(Account account) {
Date now = new Date();
// 算法
Algorithm algorithm = Algorithm.HMAC256(KEY);
String token = JWT.create()
.withIssuer(ISSUER) // 发行者, 这个在生产环境也是需要加密的
.withIssuedAt(now) // 当前时间
.withExpiresAt(new Date(now.getTime() + TOKEN_EXP_TIME))// 过期时间
.withClaim(USER_NAME, account.getUsername())
// .withClaim("ROLE","") // 企业级别应用基本都会传入 ROLE 权限
.sign(algorithm);
log.info("jwt generated user={}, token={}", account.getUsername(), token);
return token;
}
/**
* 校验token
* @param token str
* @param username str
* @return b
*/
public boolean verify(String token, String username) {
log.info("verifying jwt - username={}", username);
try {
Algorithm algorithm = Algorithm.HMAC256(KEY);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUER)
.withClaim(USER_NAME, username)
.build();
verifier.verify(token);
return true;
} catch (Exception e) {
log.error("auth failed", e);
return false;
}
}
}
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-demo-decartifactId>
<groupId>com.examplegroupId>
<version>1.0.0-SNAPSHOTversion>
<relativePath>../../pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<packaging>jarpackaging>
<artifactId>gateway-sampleartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
dependency>
<dependency>
<groupId>${project.groupId}groupId>
<artifactId>auth-service-apiartifactId>
<version>${project.version}version>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.5version>
dependency>
dependencies>
project>
application.yml
spring:
application:
name: gateway-sample
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes: # 断言配置
- id: qq
uri: https://www.qq.com
predicates:
- Path=/qq/**
filters:
- StripPrefix=1
- id: feignclient
uri: lb://FEIGN-CLIENT
predicates:
- Path=/yml/**
filters:
- StripPrefix=1
server:
port: 65000
eureka:
client:
serviceUrl:
defaultZone: http://localhost:20000/eureka/
management:
endpoint:
env:
enabled: false
health:
show-details: always
endpoints:
web:
exposure:
include: '*'
ignore:
jwt:
skip-auth-urls:
- /sayHi2
com.example.springcloud.GatewayApplication
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
com.example.springcloud.AuthFilter
@Data
@Slf4j
@Component("authFilter")
@ConfigurationProperties("ignore.jwt")
public class AuthFilter implements GatewayFilter, Ordered {
private static final String AUTH = "Authorization";
private static final String USERNAME = "jwt-user-name";
private String[] skipAuthUrls;
@Autowired
private AuthService authService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Auth start");
String url = exchange.getRequest().getURI().getPath();
System.out.println(url);
if (null != skipAuthUrls && Arrays.asList(skipAuthUrls).contains(url)) {
log.info("ignore jwt auth");
return chain.filter(exchange);
}
ServerHttpRequest request = exchange.getRequest();
HttpHeaders header = request.getHeaders();
String token = header.getFirst(AUTH);
String username = header.getFirst(USERNAME);
ServerHttpResponse response = exchange.getResponse();
if (StringUtils.isBlank(token)) {
log.error("token not found");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
AuthResponse resp = authService.verify(token, username);
if (resp.getCode() != 1L) {
log.error("invalid token");
response.setStatusCode(HttpStatus.FORBIDDEN);
return response.setComplete();
}
ServerHttpRequest.Builder mutate = request.mutate();
assert username != null;
mutate.header("jwt-user-name", username);
ServerHttpRequest buildReuqest = mutate.build();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("jwt-username",username);
return chain.filter(exchange.mutate()
.request(buildReuqest)
.response(response)
.build());
}
@Override
public int getOrder() {
return 0;
}
}
com.example.springcloud.filter.TimerFilter
@Slf4j
@Component
public class TimerFilter implements GatewayFilter, Ordered {
//public class TimerFilter implements GlobalFilter, Ordered { // GlobalFilter 全局过滤器
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
StopWatch timer = new StopWatch();
timer.start(exchange.getRequest().getURI().getRawPath());
// exchange.getAttributes().put("requestTimeBegain", System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
timer.stop();
log.info(timer.prettyPrint());
})
);
}
@Override
public int getOrder() {
return 0;
}
}
com.example.springcloud.config.GatewayConfiguration
@Configuration
public class GatewayConfiguration {
@Autowired
private TimerFilter timerFilter;
@Autowired
private AuthFilter authFilter;
@Bean
@Order
public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/java/**")
.and().method(HttpMethod.GET)
.and().header("name")
.filters(f -> f.stripPrefix(1)
.addResponseHeader("java-param", "gateway-config")
.filter(timerFilter)
.filter(authFilter)
)
.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();
}
}
按顺序启动服务
- EurekaServerApplication :20000/
- FeignClientApplication :40002/
- FeignClientApplication :40004/
- AuthApplication :65100/
- GatewayApplication :65000/
基于 auth-service 服务请求
- 生成Token
- POST http://localhost:65100/login
- Body -> x-www-form-unlencoded -> username=me / password=123456
- 校验Token
- GET http://localhost:65100/verify?username=${生成token时候的用户名}&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJlZGRpZSIsImV4cCI6MTYxMjE5MDY3MSwiaWF0IjoxNjEyMTkwNjExLCJ1c2VybmFtZSI6Im1lIn0.oDWI17FUGhNKZpQOhFHR2UqG2XgecfpySVAxMiUz6oc
- 刷新token
- POST http://localhost:65100/refresh
- Body -> x-www-form-unlencoded -> refreshToken=${生成token时候的refreshToken}
基于 gateway-sample 服务请求
- 拦截请求
- http://localhost:65000/java/sayHi
- Headers
- name:eddie
- Authorization:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJlZGRpZSIsImV4cCI6MTYxMjM0Mjc3NCwiaWF0IjoxNjEyMzQyNzE0LCJ1c2VybmFtZSI6Im1lIn0.RKtuUTcKBMBEzJM0GFZYbaSBuUc7VbiHrAIa8uqRqfI
- jwt-user-name:me
- 放行请求
- http://localhost:65000/java/sayHi2
- Headers
- name:eddie
TIPS: 必需添加 Headers:name=${value}, 不然会报404. 除非 route 不指定添加头部信息
The complete sample project for this tutorial can be downloaded code.