🌟 后端 | SpringCloud -> 网关路由



网关的三块:

  • 网关路由,解决前端请求入口的问题。
  • 网关鉴权,解决统一登录校验和用户信息获取的问题。
  • 统一配置管理,解决微服务的配置文件重复和配置热更新问题。

前端请求不能直接访问微服务,而是要请求网关:

  • 网关可以做安全控制,也就是登录身份校验,校验通过才放行
  • 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去

SpringCloudGateway

网关本身是一个独立的微服务,所以与其他微服务交流就需要注册中心的中转。

  1. 在 hm-gateway 模块的 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
<dependencies>
<!--common-->
<dependency>
<groupId>com.mps</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
  1. 在hm-gateway模块的com.opgs.gateway包下新建一个启动类
1
2
3
4
5
6
7
8
9
10
11
package com.opgs.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. 在 hm-gateway 模块的 resources 目录新建一个 application.yaml
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
server:
port: 8080
spring:
application:
name: gateway-service
cloud:
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
register-with-eureka: true # 将 gateway 注册到 eureka
fetch-registry: true # 拉取注册信息
instance:
prefer-ip-address: true # 注册时使用 IP 而不是主机名
gateway:
routes:
- id: mps # 路由规则id,自定义,唯一
uri: lb://mps-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/mps/** # 这里是以请求路径作为判断规则
- id: cup
uri: lb://cup-service
predicates:
- Path=/cup/**
- id: opgs
uri: lb://gateway-service
predicates:
- Path=/opgs/**
- id: cips
uri: lb://cips-service
predicates:
- Path=/cips/**

网关登录验证

每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,而现在通过网关鉴权则不用重复去做。
网关作为所有微服务的入口,一切请求都需要先经过网关。登录校验的工作由网关去做,这样就解决了:

  • 只需要在网关和用户服务保存秘钥
  • 只需要在网关开发登录校验功能

网关过滤器

网关转发之前需要登录校验,网关校验后,还需将用户信息传给微服务,微服务间不经过网关还需相互调用。

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

Gateway 内置的 GatewayFilter 过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route

自定义 GlobalFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("未登录,无法访问");
// 放行
// return chain.filter(exchange);

// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}

网关应用的 application.yaml 中配置:

1
2
3
4
5
spring:
cloud:
gateway:
default-filters: # default-filters下的过滤器可以作用于所有路由
- name: PrintAny

登录校验

登录校验需要用到JWT,而且JWT的加密需要秘钥和加密工具。
需要对应的 config 配置文件:

  • AuthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
  • JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置
  • SecurityConfig:工具的自动装配
  • JwtTool:JWT工具,其中包含了校验和解析token的功能
  • opgs.jks:秘钥文件
1
2
3
4
5
6
7
8
9
10
opgs:
jwt:
location: classpath:opgs.jks # 秘钥地址
alias: opgs # 秘钥别名
password: 123 # 秘钥文件密码
tokenTTL: 30m # 登录有效期
auth:
excludePaths: # 无需登录校验的路径
- /login
- /opgs/**

接着定义登录校验过滤器,在 com.opgs.gateway 下创建 filter/AuthGobalFilter.java。

  1. 检查当前访问路径是否是需要登录后需要访问的,例如 /login,或登录页、其他页上的一些图片也需要提前获取到,/images
  2. 此类在经过第一层 isExclude 校验
  3. 接着验证请求头的 authorization, 解析出 token 是否正确
  4. 验证符合条件后进入下一轮过滤链 GatewayFilterChain
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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.opgs.gateway.filter;

import com.opgs.common.exception.UnauthorizedException;
import com.opgs.common.utils.CollUtils;
import com.opgs.gateway.config.AuthProperties;
import com.opgs.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

@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) {
// 1.获取Request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否不需要拦截
if(isExclude(request.getPath().toString())){
// 无需拦截,直接放行,进行下一步过滤
return chain.filter(exchange);
}
// 3.获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

// TODO 5.如果有效,传递用户信息
System.out.println("userId = " + userId);
// 6.放行,进行下一步过滤
return chain.filter(exchange);
}

private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}

@Override
public int getOrder() {
return 0;
}
}

用户信息获取

经过过滤后完成登录校验,用户信息通过请求头继续传递到下游其他微服务。通常会使用 SpringMVC 的拦截器来实现登录用户信息获取,并存入 ThreadLocal,方便后续使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 接着上边代码第 5 步,存储用户信息

// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
// 5.获取用户信息后 传递用户信息
String userInfo = userId.toString();
ServerWebExchange ex = exchange.mutate()
.request(b -> b.header("user-info", userInfo))
.build();

接着定义另一个拦截器,获取用户信息并保存到 UserContext

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
package com.hmall.common.interceptor;

import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的用户信息
String userInfo = request.getHeader("user-info");
// 2.判断是否为空
if (StrUtil.isNotBlank(userInfo)) {
// 不为空,保存到ThreadLocal
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserContext.removeUser();
}
}

编写SpringMVC的配置类,配置登录拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.opgs.common.config;

import com.opgs.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}

同时在 resources 目录下的 META-INF/spring.factories 文件中增加

1
2
3
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig

接下来使用用户信息只需要在 mps-service 模块的 com.mps.service.impl.MsgServiceImpl 中使用:

1
UserContext.getUser()

微服务间调用携带用户信息

微服务发起调用时把用户信息存入请求头,需借助 OpenFeign 中提供的一个拦截器接口:feign.RequestInterceptor

在上篇将 微服务调用 的笔记中,openFeign 可以单独写在微服务 API 一起,这样只需像其他 API 调用一样的写法。因此可以在 opgs-api 模块的 com.opgs.api.config.DefaultFeignConfig 中编写这个拦截器, 此后在每次使用 openFeign 调用时请求头都会携带用户信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface RequestInterceptor {

/**
* Called for every request.
* Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template) {
// 获取登录用户
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
};
}