之前学习了Nacos,用于发现并注册、管理项目里所有的微服务,而OpenFeign简化微服务之间的通信,而为了使得前端可以使用微服务项目里的每一个微服务的接口,就应该将所有微服务的接口管理起来方便前端调用,所以有了网关。
前端调用后端微服务项目的接口时,不需要指定每一个接口具体的地址,只需要将请求发送到后端的网关即可。
网关介绍
网关是网络的关口,负责请求的路由、转发、身份校验 。
网关模块的配置
1、新建一个maven空模块,配置一下依赖
<dependencies>
......<!-- 其它依赖 -->
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nocos discovery-->
<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>
2、创建com.<XXX项目名称>.gateway包,报下名新建配置类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
3、在静态资源目录下新建application.yaml文件,配置网关相关属性
server:
port: 8080 # 网关服务的端口号,指定网关运行在 8080 端口
spring:
application:
name: gateway # 应用名称,注册到 Nacos 的服务名称
cloud:
nacos:
server-addr: 192.168.52.128:8848 # Nacos 服务器地址,配置 Nacos 注册中心地址
gateway:
routes: # 路由配置
- id: item-service # 路由 ID,唯一标识,可以随便命名
uri: lb://item-service # 目标服务地址,即从注册中心获取 item-service 的地址
predicates: # 断言,即路由转发的规则
- Path=/items/**,/search/** # 匹配 /items/ 开头的和 /search/ 开头的请求到 item-service 服务获取响应
- id: user-service
uri: lb://user-service
predicates:
- Path=/items/**,/search/**
4、最后启动整个项目的时候也要把网关启动
由下图可见网关的效果有了
网关的登录校验
网关过滤器有两种,分别是:
- GatewayFilter: 路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效。
- GlobalFilter: 全局过滤器,作用范围是所有路由;声明后自动生效。
网关加公共依赖XXX-common实现请求的校验
1、网关过滤器过滤请求(Filters文件夹)
@Component // 将该类标记为Spring组件,使其成为Spring容器管理的Bean
@RequiredArgsConstructor // Lombok注解,自动生成一个包含所有final字段的构造函数
public class AuthGlobalFilter implements GlobalFilter, Ordered {
// 依赖注入JwtTool,用于JWT的解析和验证
private final JwtTool jwtTool;
// 依赖注入AuthProperties,包含认证相关的配置信息,如排除路径等
private final AuthProperties authProperties;
// AntPathMatcher用于路径匹配,判断请求路径是否在排除路径中
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 获取当前请求对象
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 (headers != null && !headers.isEmpty()) {
token = headers.get(0); // 获取第一个authorization头,通常为Bearer Token
}
// 4. 校验并解析token
Long userId = null;
try {
// 使用JwtTool解析token,获取用户ID
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果token无效或解析失败,拦截请求并返回401 Unauthorized状态码
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete(); // 结束请求处理
}
// 打印用户ID(通常用于调试,生产环境中不建议直接打印敏感信息)
System.out.println("userId = " + userId);
String userInfo = userId.toString();
// 将用户信息存入请求头
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
// 5. 如果token有效,继续执行后续的过滤器链
return chain.filter(swe);
}
// 判断请求路径是否在排除路径列表中
private boolean isExclude(String path) {
for (String pathPattern : authProperties.getExcludePaths()) {
// 使用AntPathMatcher进行路径匹配
if (antPathMatcher.match(pathPattern, path)) {
return true; // 如果匹配到排除路径,返回true
}
}
return false; // 否则返回false
}
@Override
public int getOrder() {
// 返回过滤器的执行顺序,0表示最高优先级
return 0;
}
}
过滤器里涉及的一些依赖
// jwt校验工具
@Component
public class JwtTool {
private final JWTSigner jwtSigner;
public JwtTool(KeyPair keyPair) {
this.jwtSigner = JWTSignerUtil.createSigner("rs256", keyPair);
}
public String createToken(Long userId, Duration ttl) {
// 1.生成jws
return JWT.create()
.setPayload("user", userId)
.setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis()))
.setSigner(jwtSigner)
.sign();
}
/**
* 解析token
*
* @param token token
* @return 解析刷新token得到的用户信息
*/
public Long parseToken(String token) {
// 1.校验token是否为空
if (token == null) {
throw new UnauthorizedException("未登录");
}
// 2.校验并解析jwt
JWT jwt;
try {
jwt = JWT.of(token).setSigner(jwtSigner);
} catch (Exception e) {
throw new UnauthorizedException("无效的token", e);
}
// 2.校验jwt是否有效
if (!jwt.verify()) {
// 验证失败
throw new UnauthorizedException("无效的token");
}
// 3.校验是否过期
try {
JWTValidator.of(jwt).validateDate();
} catch (ValidateException e) {
throw new UnauthorizedException("token已经过期");
}
// 4.数据格式校验
Object userPayload = jwt.getPayload("user");
if (userPayload == null) {
// 数据为空
throw new UnauthorizedException("无效的token");
}
// 5.数据解析
try {
return Long.valueOf(userPayload.toString());
} catch (RuntimeException e) {
// 数据格式有误
throw new UnauthorizedException("无效的token");
}
}
}
// 拦截器拦截
@Data
@Component
@ConfigurationProperties(prefix = "hm.auth")
public class AuthProperties {
private List<String> includePaths;
private List<String> excludePaths;
}
2、网关的yaml文件里配置不需要校验直接放行的请求
hm:
jwt: #解析jwt密钥文件
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi
3、由于每一个微服务都导入了XX-common模块的依赖,所以在XX-common模块里配置并注册拦截器,拦截所有发送到每个微服务里的请求,用于将请求头里用户信息存入线程池。
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. 判断是否获取了用户,如果有,存入ThreadLocal
if (StrUtil.isNotBlank(userInfo)) {
UserContext.setUser(Long.valueOf(userInfo));
}
// 3. 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理用户
UserContext.removeUser();
}
}
4、注册XX-common模块里的拦截器
@Configuration
@ConditionalOnClass(DispatcherServlet.class) // 使得网关不去生效改拦截器
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
// 默认拦截所有的请求,目的是为了将每一个请求里包含的用户信息存入线程池
}
}
5、配置静态资源文件夹下的spring.factories文件,取保每个微服务可以读取到XX-common模块里的拦截器
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
OpenFeign传递用户信息
使用OpenFeign时,一个微服务发送给另一个微服务的请求也要携带用户信息到请求头里,要和网关发送给微服务的请求一样。所有要在公共api模块里加拦截器,使得每一个请求的请求头里添加用户信息。
写到OpenFeign的配置类里,且微服务的启动类加上@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)的注解
// 写到OpenFeign的配置类里,且微服务的启动类加上
// @EnableFeignClients(basePackages = "com.hmall.api.client",
// defaultConfiguration = DefaultFeignConfig.class)的注解
@Bean // 声明为一个Bean,可以被Spring容器管理
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取当前用户的ID
Long userId = UserContext.getUser(); // 导入XXX-common模块里的线程池
// 如果用户ID不为空,则添加到请求头中
if (userId != null) { // 确保每一个微服务之间发送的请求也携带user-info到请求头里
// 将用户ID添加到请求头中,key为"user-info"
System.out.println("将用户ID添加到请求头中,key为user-info,id为" + userId);
template.header("user-info", userId.toString());
}
}
};
}
nacos共享配置
由于每一个微服务的yaml文件里有多个共同的配置信息,所有可以将其抽取出来的配置共同使用nacos注册中心配置。
每一个微服务里导入如下依赖即可实现。
<!-- nacos配置管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 读取bootstrap文件 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
nacos里共同配置的信息()${XXX:YY}表示如果读取不到XXX则默认为YY
# 数据库和mybatis
spring:
datasource:
url: jdbc:mysql://${hm.db.host:192.168.52.128}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${hm.db.un:root}
password: ${hm.db.pw:123}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto
# 日志记录
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"
# swagger配置
knife4j:
enable: true
openapi:
title: ${hm.swagger.title:黑马商城接口文档}
description: ${hm.swagger.desc:黑马商城接口文档}
email: zhanghuyi@itcast.cn
concat: 虎哥
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- ${hm.swagger.package}
拉取nacos里的配置文件到本地微服务(以下为bootstrap.yaml文件)
spring:
main:
additional-properties: --add-opens=java.base/java.lang.invoke=ALL-UNNAMED
application:
# 应用程序名称,用于标识该服务,在Nacos或其他服务注册中心中可用到
name: cart-service
cloud:
nacos:
# Nacos的服务地址,用于连接到Nacos服务器
server-addr: localhost:8848 # nacos地址
config:
# 配置文件的格式,这里指定为YAML格式
file-extension: yaml
# 定义共享配置文件列表,这些配置将从Nacos服务器加载
shared-configs: # 一定对应好nacos里的Data ID
- data-id: shared-jdbc.yaml # JDBC共享配置文件
- data-id: shared-log.yaml # 日志共享配置文件
- data-id: shared-swagger.yaml # Swagger共享配置文件
nacos配置里的变量在本地微服务里配置好(以下为application.yaml文件)
server:
port: 8082
feign:
okhttp:
enabled: true
hm:
db:
database: hm-cart
swagger:
title: "黑马城市购物车服务接口文档"
package: com.hmall.cart.controller
配置热更新
配置热更新:修改配置文件里的配置的时候,不需要重新启动微服务项目配置就可以生效配置。
具体应用实例
需求:购买车的限定数量目前是写死在业务中的,将其改为读取配置文件属性,并将配置置交给Nacos管理,实现热更新。
首先在nocas配置要限定数量所在的微服务的yaml文件
之后在对应的微服务里添加config文件
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart") // 对应yaml文件里的配置
public class CartProperties {
private Integer maxItems;
}
最后在业务文件里面就可以去使用了
private final CartProperties cartProperties; // 导入依赖
......
private void checkCartsFull(Long userId) {
int count = lambdaQuery().eq(Cart::getUserId, userId).count();
if (count >= cartProperties.getMaxItems()) {
throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", cartProperties.getMaxItems()));
}
}
......