1 创建网关微服务-gateway
1.1 网关作用
①作为调用入口:随着微服务越来越多,前端调用地址会越来越复杂,有了网关后,前端的所有请求都走网关,由网关去做请求路由转发
②身份校验:由于所有请求都走网关,一些公共的校验就可以在网关层做,比如用户登录校验
1.2 引入依赖
(网关模块不用openFeign依赖,因为不需要调用其他微服务,它只负责路由转发,当然加上此依赖也无伤大雅)
<!--SpringMVC的web开发的场景启动器与gateway冲突,因此设置继承common模块的依赖的排除该依赖--> <dependency> <groupId>xyz.aboluo.common</groupId> <artifactId>common</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> </exclusions> </dependency> <!--网关--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>1.3 配置路由规则(application.yaml)
spring: cloud: routes: - id: route-engineering #路由规则id,保证唯一 uri: lb://engineering #路由目标微服务名称,lb代表负载均衡 predicates: #路由断言(规则) - Path=/engineering/guardNet/** #Path以请求路径(忽略请求路径前的IP和端口)断言,符合要求的请求路径被路由到uri对应的微服务去做请求 - Path=/engineering/alWindow/** #Path以请求路径(忽略请求路径前的IP和端口)断言,符合要求的请求路径被路由到uri对应的微服务去做请求 - id: route-minio #路由规则id,保证唯一 uri: lb://minio #路由目标微服务名称,lb代表负载均衡 predicates: #路由断言(规则) - Path=/minio/** #Path以请求路径(忽略请求路径前的IP和端口)断言,符合要求的请求路径被路由到uri对应的微服务去做请求filters:["StripPrefix=1"],// 去掉第一层/engineering
1.4 增加application.properties文件,去掉数据源配置
#禁用SpringBoot的自动数据源配置,因为gateway无需访问数据库,如果不开启这个配置,就需要在application.yaml中配置数据源 spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration2 身份校验
2.1 网关请求处理流程
2.2 自定义过滤器
自定义一个过滤器,让它在NettyRoutingFilter之前执行,将登录校验的内容放在自定义过滤器的PRE中,以此保证在NettyRoutingFilter将请求转发到微服务之前执行完登录校验
2.2.1 两种网关过滤器
GatewayFilter(约30多种):路由过滤器,配置在哪个路由下就在此路由下生效(也可以配置在default-filters下,让所有路由生效) //自定义比较麻烦,不建议使用
GlobalFilter:全局过滤器,作用范围是所有路由,声明后自动生效
2.2.2 代码示例
@Component public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取请求 ServerHttpRequest request = exchange.getRequest(); // 2.拦截处理 System.out.println("打印请求头信息:" + request.getHeaders()); if (false) { // 如果程序不符合要求 ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); // 设置response的状态码401 return response.setComplete(); // Complete表示完结,不会再往下执行,返回值是Mono } // 3.放行 return chain.filter(exchange); } @Override public int getOrder() { // 过滤器执行顺序(值越小,优先级越高(NettyRoutingFilter的ordered优先级是21亿,总能保证在最后执行)) return 0; } }3 路由转发
3.1 用户信息从网关传递给微服务
在自定义过滤器中过滤后得到的用户信息,需要从geteway模块传递到业务微服务模块,而网关和微服务之间是通过http协议进行数据传输的,因此将用户信息放在请求头中比较合适// 如果不做任何处理,网关会将请求头信息从gateway带到微服务的请求头
要在gateway模块的自定义实现GlobalFilter接口的filter类中修改请求头,需要用到ServerWebExchange提供的API(mutate()),判断loginCode的有效性
// 2.拦截处理(做用户校验,校验结果保存在请求头中) ServerWebExchange newExchange = null; List<String> loginCodes = request.getHeaders().get("loginCode"); if (CollectionUtils.isEmpty(loginCodes)) { // loginCode无效(下游微服务需要做登录校验的,拿到true才行;不需要做登录校验的,true/false都放行) newExchange = exchange.mutate() //mutate表示对下游(请求,响应...)做更改 .request(builder -> builder.header("loginCode-verification", "false")) .build(); } else { String loginCode = loginCodes.get(0); // 用获取的loginCode去redis查询是否有效 String redisString = Common.getRedisString("login-code▲" + loginCode); if (StringUtils.isNotBlank(redisString)) { // loginCode生效(下游微服务需要做登录校验的,拿到true才行;不需要做登录校验的,true/false都放行) newExchange = exchange.mutate() //mutate表示对下游(请求,响应...)做更改 .request(builder -> builder.header("loginCode-verification", "true")) .build(); } else { // loginCode无效(下游微服务需要做登录校验的,拿到true才行;不需要做登录校验的,true/false都放行) newExchange = exchange.mutate() //mutate表示对下游(请求,响应...)做更改 .request(builder -> builder.header("loginCode-verification", "false")) .build(); } } // 3.放行 return chain.filter(newExchange);3.2 用户信息在微服务之间的传递-OpenFeign
OpenFeign提供了一个拦截器接口,所有OpenFeign发起的请求都会先(在上一个微服务最后,即下一个微服务的AOP之前执行)调用RequestInterceptor(gateway==>微服务不会触发)拦截器处理请求// 如果不做任何处理,OpenFeign不会将一个微服务的请求头的信息带到另一个微服务的请求头
3.2.1 注册RequestInterceptor
在common模块(其它微服务模块依赖common模块),新建DefaultFeignConfig类,将RequestInterceptor注册进SpringIOC
public class DefaultFeignConfig { @Bean public RequestInterceptor getRequestInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate requestTemplate) { // 当用OpenFeign调用下个微服务之前,将从拦截的请求头"loginCode-verification"数据封装到调用下个微服务的请求头中 // 虽然RequestContextHolder.getRequestAttributes()底层使用的是ThreadLocal技术,即无法跨微服务传递数据,但此拦截器是在此微服务中运行,即数据还未"出"此微服务之前就被处理了 ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs != null) { String aaa = attrs.getRequest().getHeader("loginCode-verification"); if (StringUtils.isNotBlank(aaa)) { requestTemplate.header("loginCode-verification", aaa); } } } }; } }3.2.2 让RequestInterceptor生效
在对应微服务模块的启动类MainApplication加上@EnableFeignClients注解,让RequestInterceptor生效
@EnableFeignClients(basePackages = "xxx.xxx.xxx", defaultConfiguration = DefaultFeignConfig.class)3.3 微服务AOP拦截Controller请求
由于gateway(GlobalFilter)=>微服务和OpenFeign(RequestInterceptor)=>微服务都提前将loginCode的校验结果封装在了请求头"loginCode-verification"中,因此在被请求的微服务的对Controller层进行拦截的AOP中做的操作逻辑相同
微服务里面再通过AOP,拦截对Controller层的请求,根据Controller方法是否有loginCode参数
// 获取请求头 ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs != null) { HttpServletRequest request = attrs.getRequest(); String aaa = request.getHeader("loginCode-verification"); if(StringUtils.isNotBlank(aaa)){ //校验loginCode是否有效 } }