微服务day04
网关
网关路由
快速入门
创建新模块:hm-gateway继承hmall父项目。
引入依赖:引入网关依赖和nacos负载均衡的依赖
<?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.heima</groupId>
<artifactId>hmall</artifactId>
<version>1.0.0</version>
</parent>
<groupId>com.wmmczk</groupId>
<artifactId>hm-gateway</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos 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>
</project>
编写配置文件:一个模块中有多个collection类就可以写多个判断路径。
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.21.101:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
路由属性
网关登陆校验
自定义过滤器
GlobaFilter自定义过滤器
//Ordered接口为spring中的排序接口,为核心接口
//GlobalFilter要在NettyRoutingFilter之前,NettyRoutingFilter的Ordered的值为int的最大值,确保其最后执行
public class MyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取request
ServerHttpRequest request = exchange.getRequest();
//获取header头
HttpHeaders headers = request.getHeaders();
System.out.println("headers = " + headers);
return chain.filter(exchange);//将exchange传给下一个过滤器
}
@Override
public int getOrder() {
// 过滤器执行顺序,值越小,优先级越高
return 0;
}
}
GatewayFilter自定义过滤器
登录校验
JWT工具
登录校验需要用到JWT,而且JWT的加密需要秘钥和加密工具。这些在hm-service
中已经有了,我们直接拷贝过来:
具体作用如下:
-
AuthProperties
:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问 -
JwtProperties
:定义与JWT工具有关的属性,比如秘钥文件位置 -
SecurityConfig
:工具的自动装配 -
JwtTool
:JWT工具,其中包含了校验和解析token
的功能 -
hmall.jks
:秘钥文件
其中AuthProperties
和JwtProperties
所需的属性要在application.yaml
中配置
hm:
jwt:
location: classpath:hmall.jks # 秘钥地址
alias: hmall # 秘钥别名
password: hmall123 # 秘钥文件密码
tokenTTL: 30m # 登录有效期
auth:
excludePaths: # 无需登录校验的路径
- /search/**
- /users/login
- /items/**
创建自定义拦截器:
package com.hmall.gateway.Filte;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.AntPathMatcher;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.RequestPath;
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.util.List;
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//用于读取配置文件中要放行的路径
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final JwtTool jwtTool;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取requset
ServerHttpRequest request = exchange.getRequest();
//获取path进行判断是否需要拦截
RequestPath path = request.getPath();
//使工具类进行判断
if (isPath(path)){
//放行
return chain.filter(exchange);
}
//获取token
String token = null;
HttpHeaders headers = request.getHeaders();
List<String> list = headers.get("Authorization");
if (!CollUtil.isEmpty(list)){
//给token赋值
token = list.get(0);
}
//校验token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
//token无效进行拦截
//获取Response进行编辑
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();//该状态可以使后面的过滤器不在执行,返回错误
}
//TODO 传递用户信息
System.out.println("userId = " + userId);
return chain.filter(exchange);
}
private boolean isPath(RequestPath path) {
//转为String类型
String string = path.toString();
//将配置文件中定义的路径进行遍历
for (String excludePath : authProperties.getExcludePaths()) {
if (antPathMatcher.match(excludePath, string)){
//使用工具类进行匹配 antPathMatcher.match ,匹配成功返回true
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
网关传递用户信息
//TODO 传递用户信息
String userinfo = userId.toString();
//exchange提供了修改请求头信息的方法mutate
ServerWebExchange build = exchange.mutate()
.request(b -> b.header("user-info",userinfo))
.build();
System.out.println("userId = " + userId);
return chain.filter(build);
package com.hmall.gateway.Filte;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.AntPathMatcher;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.RequestPath;
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.util.List;
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
//用于读取配置文件中要放行的路径
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final JwtTool jwtTool;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取requset
ServerHttpRequest request = exchange.getRequest();
//获取path进行判断是否需要拦截
RequestPath path = request.getPath();
//使工具类进行判断
if (isPath(path)){
//放行
return chain.filter(exchange);
}
//获取token
String token = null;
HttpHeaders headers = request.getHeaders();
List<String> list = headers.get("Authorization");
if (!CollUtil.isEmpty(list)){
//给token赋值
token = list.get(0);
}
//校验token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
//token无效进行拦截
//获取Response进行编辑
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();//该状态可以使后面的过滤器不在执行,返回错误
}
//TODO 传递用户信息
String userinfo = userId.toString();
//exchange提供了修改请求头信息的方法mutate
ServerWebExchange build = exchange.mutate()
.request(b -> b.header("user-info",userinfo))
.build();
System.out.println("userId = " + userId);
return chain.filter(build);
}
private boolean isPath(RequestPath path) {
//转为String类型
String string = path.toString();
//将配置文件中定义的路径进行遍历
for (String excludePath : authProperties.getExcludePaths()) {
if (antPathMatcher.match(excludePath, string)){
//使用工具类进行匹配 antPathMatcher.match ,匹配成功返回true
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
定义拦截器:
拦截器实现了HandlerInterceptor接口
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));
System.out.println(userInfo);
}
// 3.放行
return true;
}
//业务执行后的操作,删除线程存储中的数据
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.removeUser();
}
}
由于拦截器需要配置类进行注册才可以启用,创建一个配置类:
由于网关模块并没有使用springMVC进行编写,所以网关就会报错,因此使用该注解:@ConditionalOnClass(DispatcherServlet.class)//用于判断是否存在某个类,存在就加载,不存在就不加载,使得网关模块不在加载该配置类,避免报错。
package com.hmall.common.config;
import com.hmall.common.interceptor.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 MyUserInfoConfig implements WebMvcConfigurer {
//将拦截器添加到容器中
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserINfointerceptor ());
}
}
在hm-common模块下的文件 spring.factories 中添加路径,在启动时扫描该配置
添加该代码:
com.hmall.common.config.MyUserInfoConfig,\
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MyUserInfoConfig,\
com.hmall.common.config.JsonConfig
OpenFeign传递用户
在Api模块中的
DefaultFeignConfig配置类中添加拦截器,拦截所有的okhttp请求即所有的微服务之间的请求
使用匿名内部类进行配置
由于需要获取用户信息,添加hm-common的依赖获取UserContext对象。
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
@Bean
public RequestInterceptor MyRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long user = UserContext.getUser();
if (user != null){
requestTemplate.header("user-info",user.toString());
}
}
};
}
package com.hmall.api.config;
import com.hmall.common.utils.UserContext;
import feign.Logger;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor MyRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long user = UserContext.getUser();
if (user != null){
requestTemplate.header("user-info",user.toString());
}
}
};
}
}
微服务的登录解决方案:
配置管理
配置共享
我们可以把微服务共享的配置抽取到Nacos中统一管理,这样就不需要每个微服务都重复配置了。分为两步:
-
在Nacos中添加共享配置
-
微服务拉取配置
添加共享配置:
1、在nacos的配置列表下进行新建共享配置
- Data ID:表示共享配置文件的名称,即在项目中引入是的名字
- 使用默认分组
- 配置内容即要共享的配置,可以使用占位符进行动态的设置配置数据
jdbc的更共享配置:shared-jdbc.yaml ,有数据库设置和mp的配置使用占位符来进行不同的设置。
占位符后面的 :表示默认字符,即没有设置的默认数据
spring:
datasource:
url: jdbc:mysql://${hm.db.host:192.168.21.101}:${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.user: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
日志的共享配置: shared-log.yaml
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"
swagger的共享配置;shared-swagger.yaml
knife4j:
enable: true
openapi:
title: ${hm.swagger.title:黑马商城接口文档}
description: ${hm.swagger.description:黑马商城接口文档}
email: ${hm.swagger.email:zhanghuyi@itcast.cn}
concat: ${hm.swagger.concat:虎哥}
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- ${hm.swagger.package}
接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml
配置合并,完成项目上下文的初始化。
不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext
)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml
。
也就是说引导阶段,application.yaml
文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢?
SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml
(或者bootstrap.properties
)的文件,如果我们将nacos地址配置到bootstrap.yaml
中,那么在项目引导阶段就可以读取nacos中的配置了。
/2、在项目中引入依赖:
<!--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>
创建bootstrap.yml
spring:
application:
name: cart-service # 服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.21.101 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
修改原有的配置文件,为占位符赋值 application.yaml
server:
port: 8082
feign:
okhttp:
enabled: true # 开启OKHttp连接池支持
hm:
swagger:
title: 购物车服务接口文档
package: com.hmall.cart.controller
db:
database: hm-cart
配置热更新
注意文件的dataId格式:
[服务名]-[spring.active.profile].[后缀名]
文件名称由三部分组成:
-
服务名
:我们是购物车服务,所以是cart-service
-
spring.active.profile
:就是spring boot中的spring.active.profile
,可以省略,则所有profile共享该配置 -
后缀名
:例如yaml
设置nacos配置热更新文件:
名字为:cart-service
hm:
cart:
maxAmount: 10
在代码中创建配置类:,设置配置文件关联。
创建包: config
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
private Integer maxAmount;
}
修改业务文件中判断购物车最大数量:
private void checkCartsFull(Long userId) {
int count = Math.toIntExact(lambdaQuery().eq(Cart::getUserId, userId).count());
if (count >= cartProperties.getMaxAmount()) {
throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", cartProperties.getMaxAmount()));
}
}
动态路由
动态路由的相关操作