springcloud-网关(gateway)
概述
\Spring Cloud Gateway旨在提供一种简单而有效的方式来路由到API,并为其提供跨领域的关注,如:安全、监控/指标和容错
常用术语
- Route(路由): 网关的基本构件。它由一个ID、一个目的地URI、一个谓词(Predicate)集合和一个过滤器(Filter)集合定义。如果集合谓词为真,则路由被匹配。
- Predicate(谓词): 这是一个 Java 8 Function Predicate。输入类型是 [Spring Framework ServerWebExchange]。这让你可以在HTTP请求中的任何内容上进行匹配,比如header或查询参数。
- Filter(过滤器): 这些是 [GatewayFilter] 的实例,已经用特定工厂构建。在这里,你可以在发送下游请求之前或之后修改请求和响应。
网关工作原理
客户端向 Spring Cloud Gateway 发出请求。如果Gateway处理程序映射确定一个请求与路由相匹配,它将被发送到Gateway Web处理程序。这个处理程序通过一个特定于该请求的过滤器链来运行该请求。过滤器被分割的原因是,过滤器可以在代理请求发送之前和之后运行逻辑。所有的
"pre"
(前)过滤器逻辑都被执行。然后发出代理请求。在代理请求发出后,"post"
(后)过滤器逻辑被运行。
业务架构图
开发流程
引入依赖
gateway框架需要引入loadbalancer,否则在访问的时候会出现503异常。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
bootstrap.yml配置
配置nacos
如果不需要nacos的配置,可以不引入nacos的config依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
application:
name: ssc-cloud-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.201.81:7777
namespace: ssc-cloud-id
group: DEV
register-enabled: true
配置gateway
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id : ssc-order-id #路由编号(自定义)
# uri: https://www.bilibili.com/ #跳转地址(网页跳转),lb--负载调整(微服务)
uri: lb://ssc-cloud-older
predicates:
- Path=/ssc-older/** #url 的映射名称(让客户看到的)
filters:
- StripPrefix=1
- id: ssc-paycenter-id #路由编号(自定义)
uri: lb://ssc-cloud-paycenter
predicates:
- Path=/ssc-pay/**
filters:
- StripPrefix=1
路由配置规则
序号 | 名称 | 解释 |
---|---|---|
1 | id | ⾃定义的路由 ID,保持唯⼀ |
2 | uri | 目标服务地址:以lb:// 开头则表示要去向的微服务名称 |
3 | predicates | 路由条件 |
4 | Filter(过滤器) | 过滤器是路由转发请求时所经过的过滤逻辑,可⽤于修改请求、响应内容。 |
predicates断言条件
-
Path
实例: - Path=/ssc-older/
routes: - id : ssc-older-id uri: lb://ssc-cloud-older predicates: - Path=/ssc-older/**
解释: 当请求的路径为ssc-older开头的时,转发到ssc-cloud-older微服服务器上,ssc-cloud-older是navcos中微服务注册的名称。
-
Before
- Before=2017-01-20T17:42:47.789-07:00[Asia/Shanghai]
在某个时间之前的请求才会被转发到 http://localhost:9001 服务器上
-
After
- After=2017-01-20T17:42:47.789- 07:00[Asia/Shanghai]
在某个时间之后的请求才会被转发
-
Between
- Between=2017-01-20T17:42:47.789-07:00[Asia/Shanghai],
2017-01- 21T17:42:47.789-07:00[Asia/Shanghai]
在某个时间段之间的才会被转发
-
Cookie
- Cookie=sesionId,ssc-test
名为ss-old的表单或者满⾜正则old的表单才会被匹配到 进⾏请求转发
routes: - id : ssc-older-id uri: lb://ssc-cloud-older predicates: - Cookie=sesionId,ssc-test
-
Header
- Header=X-Request-Id
携带参数X-Request-Id的请求头才会匹配
-
Host
- Host=www.ssc.older.com
当主机名为www.ssc.older.com的时候直接转发到指定服务器。
-
Method
- Method=GET
只有GET⽅法才会匹配转发请求,还可以限定POST、PUT等。
routes: - id : ssc-older-id uri: lb://ssc-cloud-older predicates: - Method=GET,POST
过滤器规则
过滤规则 | 实例 | 说明 |
---|---|---|
PrefixPath | - PrefixPath=/app | 在请求路径前加上app |
PrefixPath | - PrefixPath=/app | 在请求路径前加上app |
SetPath | SetPath=/app/{path} | 通过模板设置路径,转发的规则时会在路径前增加 app,{path}表示原请求路径 |
RedirectTo | 重定向 | |
RemoveRequestHea | 去掉某个请求头信息\1. PrefixPath | |
StripPrefix | Path=/ssc-pay/** | 去掉ssc-pay才是真实路径 |
网关的案例
使用网关进行用户鉴权过滤。
鉴权微服务
api模块
api模块为外界提供接口(openfeign调用),为网关调用需要基于reactor开发。
pom
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.wnhz.ssc.cloud.authority</groupId>
<artifactId>ssc-cloud-authority</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.wnhz.ssc.cloud.authority.api</groupId>
<artifactId>ssc-cloud-authority-api</artifactId>
<dependencies>
<dependency>
<groupId>com.playtika.reactivefeign</groupId>
<artifactId>feign-reactor-webclient</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.playtika.reactivefeign</groupId>
<artifactId>feign-reactor-cloud</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.playtika.reactivefeign</groupId>
<artifactId>feign-reactor-spring-configuration</artifactId>
<version>3.0.3</version>
</dependency>
</dependencies>
</project>
openfeign接口
@ReactiveFeignClient(value = "ssc-cloud-authority",url = "http://localhost:12121")
public interface IAuthorityFeign {
@GetMapping("/api/authority/login")
Mono<HttpResp> login(@RequestParam("username") String username,
@RequestParam("password") String password);
@PostMapping("/api/authority/verifyToken")
Mono<HttpResp> verifyToken(String token);
}
业务接口(sevice)
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.wnhz.ssc.cloud.authority</groupId>
<artifactId>ssc-cloud-authority</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.wnhz.ssc.cloud.authority.service</groupId>
<artifactId>ssc-cloud-authority-service</artifactId>
<dependencies>
<dependency>
<groupId>com.wnhz.ssc.cloud.springcloud</groupId>
<artifactId>ssc-cloud-springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.wnhz.ssc.cloud</groupId>
<artifactId>ssc-cloud-tools</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.wnhz.ssc.cloud</groupId>
<artifactId>ssc-cloud-ssm</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>
配置
bootstrap.yml
spring:
cloud:
nacos:
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
file-extension: yaml
extension-configs:
- data-id: db_ssc.yaml
group: DEV
refresh: true
- data-id: redis.yaml
group: DEV
refresh: true
discovery:
server-addr: 192.168.201.107
namespace: ssc-cloud-id
group: DEV
application:
name: ssc-cloud-authority
application
-
application-dev.yml
server: port: 12121 knife4j: enable: true logging: level: com.wnhz: debug
-
application.yml
spring: profiles: active: dev
启动类
@EnableDiscoveryClient
@SpringBootApplication
public class AuthorityApp {
public static void main(String[] args) {
SpringApplication.run(AuthorityApp.class);
}
}
dao
@Mapper
public interface ILoginDao extends BaseMapper<Login> {
}
异常
/**
* 鉴权异常
*/
public class AuthorityException extends RuntimeException{
public AuthorityException(String message) {
super(message);
}
}
/**
* 用户名或密码错误异常
*/
public class BadCredentialsException extends AuthorityException{
public BadCredentialsException(String message) {
super(message);
}
}
service
-
接口
public interface ILoginService { Login loginCheck(String username, String password); List<Login> findByUsername(String username); }
-
实现类
package com.wnhz.ssc.cloud.authority.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.wnhz.ssc.cloud.authority.dao.ILoginDao; import com.wnhz.ssc.cloud.authority.exception.BadCredentialsException; import com.wnhz.ssc.cloud.authority.exception.UsernameEmptyException; import com.wnhz.ssc.cloud.authority.exception.UsernameNotFoundException; import com.wnhz.ssc.cloud.authority.service.ILoginService; import com.wnhz.ssc.domain.entity.po.Login; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.List; import java.util.Objects; @Service public class LoginServiceImpl implements ILoginService { @Autowired private ILoginDao loginDao; @Override public Login loginCheck(String username, String password) { if (!StringUtils.hasText(username)) throw new UsernameEmptyException("用户名为空异常"); LambdaQueryWrapper<Login> lambdaQueryWrapper = new LambdaQueryWrapper<>(); lambdaQueryWrapper.eq(Login::getUsername, username); List<Login> logins = findByUsername(username); Login login = logins.stream() .filter(lg -> username.equals(lg.getUsername())).findFirst().get(); if (Objects.isNull(login)) throw new BadCredentialsException("用户名|密码错误"); return login; } /** * 判读用户名是否存在 * @param username 用户名 * @return 所有查询服务用户名的用户集合 */ @Override public List<Login> findByUsername(String username) { List<Login> logins = loginDao.selectList(new LambdaQueryWrapper<Login>().eq(Login::getUsername, username)); if (Objects.isNull(logins) || logins.isEmpty()) throw new UsernameNotFoundException("用户名不存在异常"); return logins; } }
controller
package com.wnhz.ssc.cloud.authority.controller;
import com.wnhz.ssc.cloud.authority.service.ILoginService;
import com.wnhz.ssc.cloud.tools.exception.jwt.JwtException;
import com.wnhz.ssc.cloud.tools.exception.jwt.JwtExpiredException;
import com.wnhz.ssc.cloud.tools.jwt.JwtUtil;
import com.wnhz.ssc.common.result.HttpResp;
import com.wnhz.ssc.domain.entity.po.Login;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Api(tags = "鉴权模块api")
@RestController
@RequestMapping("/api/authority")
@Slf4j
public class AuthorityController {
@Autowired
private ILoginService loginService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private final String REDIS_PREFIX="token:";
/**
* 用户登录
*
* @param username
* @param password
* @return
*/
@ApiOperation(value = "login", notes = "用户登录验证")
@GetMapping("/login")
public HttpResp login(String username, String password) {
try {
Login login = loginService.loginCheck(username, password);
log.debug("用户名登录成功:{}",login);
Map<String, Object> map = new HashMap<>();
map.put("username", username);
String token = JwtUtil.getInstance().creatToken(
map, 1000 * 30, login.getSign());
stringRedisTemplate.opsForValue().set(REDIS_PREFIX+token, login.getSign(),
10, TimeUnit.MINUTES);
log.debug("token产生成功,并存入redis中:{}",token);
return HttpResp.success(new String[]{"ssc_login_token", token});
} catch (Exception e) {
return HttpResp.failed(e.getMessage());
}
}
/**
* 验证token
*
* @param token
* @return
*/
@ApiOperation(value = "verifyToken", notes = "token验证")
@GetMapping("/verifyToken")
public Mono<HttpResp> verifyToken(String token) {
String sign = stringRedisTemplate.opsForValue().get(REDIS_PREFIX+token);
log.debug("从redis中获取token的值: {}", sign);
if (Objects.isNull(sign)) {
return Mono.just(HttpResp.failed("token不存在"));
}
try {
JwtUtil.getInstance().verifyToken(token, sign);
return Mono.just(HttpResp.success("token验证成功"));
} catch (JwtExpiredException e) {//token已过期,判断是否续期
return refreshToken(token, sign);
} catch (JwtException e) {
return Mono.just(HttpResp.failed(e.getMessage()));
}
}
private Mono<HttpResp> refreshToken(String originalToken, String sign) {
Long expireLeftTime = stringRedisTemplate.getExpire(REDIS_PREFIX+originalToken);
log.debug("redis中token剩余时间:{}", expireLeftTime);
if (expireLeftTime > 0) {//token续期
String newToken = JwtUtil.getInstance().cloneToken(originalToken, expireLeftTime);
stringRedisTemplate.delete(REDIS_PREFIX+originalToken);
stringRedisTemplate.opsForValue().set(REDIS_PREFIX+newToken, sign);
log.debug("新token产生成功,续期完成,token==> {}",newToken);
return Mono.just(HttpResp.success(new String[]{"ssc_login_token", newToken}));
} //token已经过期,redis也过期
return Mono.just(HttpResp.failed("token已经过期"));
}
}
全局网关
负责对鉴权控制
pom
<?xml version="1.0" encoding="UTF-8"?>
<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>wnhz-ssc-cloud</artifactId>
<groupId>com.wnhz.ssc.cloud</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.wnhz.ssc.cloud.gateway</groupId>
<artifactId>ssc-cloud-gateway</artifactId>
<dependencies>
<dependency>
<groupId>com.wnhz.ssc.cloud.springcloud</groupId>
<artifactId>ssc-cloud-springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.wnhz.ssc.cloud.common</groupId>
<artifactId>ssc-cloud-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.playtika.reactivefeign</groupId>
<artifactId>feign-reactor-spring-configuration</artifactId>
<version>3.0.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.wnhz.ssc.cloud.authority.api</groupId>
<artifactId>ssc-cloud-authority-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<outputDirectory>../out</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
配置
bootstrap.yml
spring:
main:
web-application-type: reactive
application:
name: ssc-cloud-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.201.81:7777
namespace: ssc-cloud-id
group: DEV
register-enabled: true
config:
enabled: off
gateway:
discovery:
locator:
enabled: true
routes:
- id : ssc-order-id #路由编号(自定义)
# uri: https://www.bilibili.com/ #跳转地址(网页跳转),lb--负载调整(微服务)
uri: lb://ssc-cloud-older
predicates:
- Path=/ssc-older/** #url 的映射名称(让客户看到的)
filters:
- StripPrefix=1
- id: ssc-paycenter-id #路由编号(自定义)
uri: lb://ssc-cloud-paycenter
predicates:
- Path=/ssc-pay/**
filters:
- StripPrefix=1
- id: ssc-authority-id #路由编号(自定义)
uri: lb://ssc-cloud-authority
predicates:
- Path=/ssc-auth/**
filters:
- StripPrefix=1
全局过滤器
package com.wnhz.ssc.cloud.gateway.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wnhz.ssc.cloud.authority.feign.IAuthorityFeign;
import com.wnhz.ssc.common.result.HttpResp;
import com.wnhz.ssc.common.result.RespCode;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
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;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
@Component
@Slf4j
public class SscGlobalGatewayFilter implements GlobalFilter, Ordered {
@Lazy
@Autowired
private IAuthorityFeign authorityFeign;
private final String LOGIN_TOKEN="ssc_login_token";
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.debug("我进入过滤中.....");
ObjectMapper objectMapper = new ObjectMapper();
DataBuffer dataBuffer = null;
String requestURI =request.getURI().getPath() ;
log.debug("请求uri:{}",requestURI);
if(requestURI.endsWith("/authority/login")){//客户访问登录业务,直接跳转验证
return chain.filter(exchange);
}
//1. Token不存在,客户未登录状态访问系统
if(!isLogin(request)) {//如果客户没有登录
log.debug("客户还没有登录,重定向到登录(baidu)页面");
String redirectUrl = "https://www.baidu.com";
log.debug("将客户请求重定向到指定页面: {}", redirectUrl);
response.getHeaders().set(HttpHeaders.LOCATION, redirectUrl);
//303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
response.setStatusCode(HttpStatus.SEE_OTHER);
response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
return response.setComplete();
}
//2. token存在,调用验证微服务验证token是否有效
String token = getToken(request);
HttpResp block = authorityFeign.verifyToken(token).block();
if(block.getCode()== RespCode.FAILED.getCode()){ //token验证失败
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
dataBuffer = response.bufferFactory().wrap(
objectMapper.writeValueAsString(
block)
.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(dataBuffer));
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
}
/**
* 客户未登录,没有token
* @param request
* @return
*/
private boolean isLogin(ServerHttpRequest request){
HttpHeaders headers = request.getHeaders();
log.debug("header===> {}", headers);
List<String> loginToken = headers.get(LOGIN_TOKEN);
if(Objects.nonNull(loginToken)){
return true;
}
return false;
}
/**
* 从请求中获取token
* @param request
*/
private String getToken(ServerHttpRequest request){
HttpHeaders headers = request.getHeaders();
log.debug("header===> {}", headers);
List<String> tokens = headers.get(LOGIN_TOKEN);
String token = tokens.get(0);
return token;
}
}
启动类
package com.wnhz.ssc.cloud.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import reactivefeign.spring.config.EnableReactiveFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableReactiveFeignClients(basePackages = "com.wnhz.ssc.cloud.authority.feign")
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class);
}
}
HttpHeaders headers = request.getHeaders();
log.debug("header===> {}", headers);
List<String> tokens = headers.get(LOGIN_TOKEN);
String token = tokens.get(0);
return token;
}
}
### 启动类
```java
package com.wnhz.ssc.cloud.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import reactivefeign.spring.config.EnableReactiveFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableReactiveFeignClients(basePackages = "com.wnhz.ssc.cloud.authority.feign")
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class);
}
}