前端请求网关根据请求路径路由到微服务,网关从nacos获取微服务实例地址将请求转发到具体的微服务实例上.
现在要根据需求使用Java在网关实现路由转发和用户身份认证的功能:
- 根据请求Url路由到具体的微服务
- 校验用户的token,取出token中的用户信息
- 从nacos中取出服务实例进行负载均衡
所以使用java开发的网关,如
- Spring Cloud Gateway:基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强
- NetFlix Zuul:早期实现,已淘汰

1. 实现网关路由
1.1. 添加依赖
在网关工程的pom.xml
中添加依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?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>xxx-parent</artifactId> <groupId>com.xxx</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion>
<artifactId>xxx-gateway</artifactId>
<properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.xxx</groupId> <artifactId>xxx-common</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
|
1.2. 新建启动类
1 2 3 4 5 6 7 8 9 10 11
| package com.xxx.gateway;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
|
1.3. 配置路由
在application.yml
中配置路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| server: port: 8080 spring: application: name: gateway cloud: nacos: server-addr: 虚拟机IP:8848 gateway: routes: - id: aaa uri: lb://aaa-service predicates: - Path=/aaas/** - id: bbb uri: lb://bbb-service predicates: - Path=/bbbs/** - id: user uri: lb://user-service predicates: - Path=/users/**,/addresses/**
|
路由规则routes包括四个属性,定义语法如下:
- id:路由的唯一标示
- predicates:路由断言,
Predicates
是用于判断请求是否满足特定条件的组件
- filters:路由过滤条件
- uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问
predicates路由断言
的类型:

2. 网关鉴权
把身份校验的工作放到网关,确保只有经过授权的用户或设备才能访问特定的服务或资源:
- 在网关和用户服务保存秘钥
- 在网关开发身份校验功能

流程如下:
- 用户登录成功生成token并存储在前端
- 前端携带token访问网关
- 网关解析token中的用户信息,网关将请求转发到微服务,转发时携带用户信息
- 微服务从http头信息获取用户信息
- 微服务之间远程调用使用内部接口(无状态接口)
2.1. 网关内置过滤器
内置过滤器有很多,具体在工作中根据需求去使用

例如使用StripPrefix过滤器的网关的路由配置如下
1 2 3 4 5 6
| - id: product uri: lb://item-service predicates: - Path=/product/** filters: - StripPrefix=1
|
StripPrefix=1表示去除一级路径前缀,使用StripPrefix=1后
请求:http://localhost:8080/product/items/page?pageNo=1&pageSize=1
路径到:http://localhost:8081/items/page?pageNo=1&pageSize=1(8081是目标微服务的端口)
2.2. 自定义过滤器
无论是GatewayFilter还是GlobalFilter都支持自定义,只不过编码方式、使用方式略有差别
2.2.1. 自定义GlobalFilter
全局过滤器不用在路由中配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Component @Slf4j public class PrintAnyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { log.info("打印全局过滤器"); return chain.filter(exchange);
}
@Override public int getOrder() { return 0; } }
|
2.2.2. 自定义GatewayFilter
自定义GatewayFilter不是直接实现GatewayFilter,而是继承AbstractGatewayFilterFactory,该类的名称一定要以GatewayFilterFactory为后缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Component @Slf4j public class FirstFilterGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override public GatewayFilter apply(Object config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest();
log.info("请求路径:{}",request.getPath()); log.info("网关过滤器FirstFilterGatewayFilterFactory执行啦..."); return chain.filter(exchange); } }; } }
|
配置仅在product路由中有效的路由
1 2 3 4 5 6 7
| - id: product uri: lb://item-service predicates: - Path=/product/** filters: - StripPrefix=1 - FirstFilter
|
配置在所有路由中都有效的路由
1 2 3 4 5
| spring: cloud: gateway: default-filters: - FirstFilter
|
2.3. 身份校验过滤器
利用自定义GlobalFilter
来完成身份校验,JWT工具类准备好,如:
- AuthProperties:配置身份校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
- JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置
- SecurityConfig:工具的自动装配
- JwtTool:JWT工具,其中包含了校验和解析token的功能
- hmall.jks:秘钥文件
2.3.1. 配置白名单
其中AuthProperties
和JwtProperties
所需的属性要在application.yaml中配置
1 2 3 4 5 6 7 8 9 10 11
| hm: jwt: location: classpath:hmall.jks # 秘钥地址 alias: hmall # 秘钥别名 password: hmall123 # 秘钥文件密码 tokenTTL: 30m # 登录有效期 auth: excludePaths: # 无需身份校验的路径 - /search/** - /users/login - /items/**
|
2.3.2. 定义过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| @Component @RequiredArgsConstructor @EnableConfigurationProperties(AuthProperties.class) public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().toString(); List<String> excludePaths = authProperties.getExcludePaths(); for (String excludePath : excludePaths) { boolean match = antPathMatcher.match(excludePath, path); if (match) { return chain.filter(exchange); } } String token = exchange.getRequest().getHeaders().getFirst("authorization"); if(token==null){ ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); } Long userId = null; try { userId = jwtTool.parseToken(token); } catch (Exception e) { ServerHttpResponse response = exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); }
exchange.getRequest().mutate().header("user-info",userId.toString()); return chain.filter(exchange); }
@Override public int getOrder() { return 0; } }
|
2.4. 网关传递用户信息到微服务

利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal:
- 改造网关过滤器,在获取用户信息后保存到请求头,转发到下游微服务
- 编写微服务拦截器,拦截请求获取用户信息,保存到ThreadLocal后放行
2.4.1. 改造过滤器
身份校验拦截器的处理逻辑,保存用户信息到请求头中
1
| exchange.getRequest().mutate().header("user-info",userId.toString());
|
ThreadLocal工具返回从threadLocal中获取的userId
1 2 3
| public static Long getUser() { return tl.get(); }
|
2.4.2. 编写拦截器
编写拦截器,获取用户信息并保存到UserContext,然后放行
拦截器我们直接写在xxx-common中,并写好自动装配.这样微服务只需要引入xxx-common就可以直接具备拦截器功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.xxx.common.interceptor; public class UserInfoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String userInfo = request.getHeader("user-info"); if (StrUtil.isNotBlank(userInfo)) { UserContext.setUser(Long.valueOf(userInfo)); } return true; }
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserContext.removeUser(); } }
|
2.4.3. 配置拦截器
接着在xxx-common模块下编写SpringMVC的配置类,配置登录拦截器
1 2 3 4 5 6 7 8 9
| package com.xxx.common.config; @Configuration @ConditionalOnClass(DispatcherServlet.class) public class MvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } }
|
需要注意的是,这个配置类默认是不会生效的,因为它所在的包是com.xxx.common.config,与其它微服务的扫描包不一致,无法被扫描到,因此无法生效。
基于SpringBoot的自动装配原理,我们要将其添加到resources目录
下的META-INF/spring.factories
文件中
1 2 3 4
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.hmall.common.config.MyBatisConfig,\ com.hmall.common.config.JsonConfig,\ com.hmall.common.config.MvcConfig
|
2.5. Feign接口传递用户
有些业务是比较复杂的,请求到达微服务后还需要调用其它多个微服务,并没有传递用户信息
2.5.1. 方案1:OpenFeign拦截器
微服务之间调用是基于OpenFeign来实现的,在进行OpenFeign调用时可以将用户信息放在http头中传递
借助实现Feign中提供的一个拦截器接口:feign.RequestInterceptor可以实现Feign拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class FeignInterceptorConfig { @Bean public RequestInterceptor userInfoRequestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { Long userId = UserContext.getUser(); if(userId == null) { return; } template.header("user-info", userId.toString()); } }; } }
|
方案1就是在feign远程调用前在http头中添加用户信息,请求到达微服务由微服务拦截器解析出http头中的user-info放入ThreadLocal
2.5.2. 方案2: 单独编写对应接口