跳转至

网关

搭建模块环境

架构的问题分析

目前为止共三个微服务:内容管理、系统管理、媒资管理。

后期还会添加更多的微服务,当前这种由前端直接请求微服务的方式存在弊端:

开发环境在自己电脑上使用localhost,测试环境使用公司局域网ip,生产环境使用域名。

如果在前端对每个请求地址都配置绝对路径,非常不利于系统维护。

客户端请求接口需要改动,基于这个问题可以通过网关来解决。

这样在前端的代码中只需要指定每个接口的相对路径,在前端代码的一个固定的地方在接口地址前统一加网关的地址,每个请求统一到网关,由网关将请求转发到具体的微服务。

为什么所有的请求先到网关呢?

有了网关就可以对请求进行路由,路由到具体的微服务,减少外界对接微服务的成本,比如:400电话转接,路由的试可以根据请求路径进行路由、根据host地址进行路由等, 当一个微服务有多个实例时可以通过负载均衡算法进行路由。

另外,网关还可以实现权限控制、限流等功能。

项目采用Spring Cloud Gateway作为网关,网关在请求路由时需要知道每个微服务实例的地址,项目使用Nacos作为服务发现中心和配置中心,整体的架构图如下:

1de1ec2d-eb83-44f7-8781-d705788f35ff

流程如下:

  1. 微服务启动,将自己注册到Nacos,Nacos记录了各微服务实例的地址。
  2. 网关从Nacos读取服务列表,包括服务名称、服务地址等。
  3. 请求到达网关,网关将请求路由到具体的微服务。

要使用网关首先搭建Nacos

搭建Gateway

使用Spring Cloud Gateway作为网关,新建一个网关工程。

c9c71e1c-8fbd-4550-9450-023ef79a32e6

在heima-leadnews-gateway添加依赖:

<dependencies>

    <!--网关-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- nacos的注册依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- nacos配置-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <!-- 排除 Spring Boot 依赖的日志包冲突 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!-- Spring Boot 集成 log4j2 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    <!-- jwt解析的jar包-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>

在heima-leadnews-gateway下创建heima-leadnews-app-gateway微服务Module

heima-leadnews-app-gateway|src|main|java|heima-leadnews-app-gateway包下创建引导类:

package com.heima.app.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient  //开启注册中心
public class AppGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(AppGatewayApplication.class,args);
    }
}

heima-leadnews-app-gateway|src|main|java|resource中配置网关的bootstrap.yaml配置文件

server:
  port: 51601
#微服务配置
spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery:
        #注册中心的地址
        #server-addr: 192.168.200.130:8848
        namespace: dev
        group: xuecheng-plus-project
      config:
        #配置中心的地址
        #server-addr: 192.168.200.130:8848
        namespace: dev
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: true
        shared-configs:
          - data-id: logging-${spring.profiles.active}.yaml
            group: xuecheng-plus-common
            refresh: true

  profiles:
    active: dev

在nacos上配置网关路由策略:

详细配置如下:

server:
  port: 63010 # 网关端口
spring:
  cloud:
    gateway:
#      globalcors:
#        add-to-simple-url-handler-mapping: true
#        corsConfigurations:
#          '[/**]': #匹配所有的请求
#            allowedHeaders: "*" #允许所有的header
#            allowedOrigins: "*" #跨域处理 允许所有的域
#            allowedMethods:        #支持的方法
#              - GET
#              - POST
#              - DELETE
#              - PUT
#              - OPTION

#      filter:
#        strip-prefix:
#          enabled: true
      routes: # 网关路由配置
        - id: content-api # 路由id,自定义,只要唯一即可
          # uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
          uri: lb://content-api # 路由的目标地址 lb就是负载均衡,后面跟服务名称
          predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
            - Path=/content/** # 这个是按照路径匹配,只要以/content/开头就符合要求
#          filters:
#            - StripPrefix=1
        - id: system-api
          # uri: http://127.0.0.1:8081
          uri: lb://system-api
          predicates:
            - Path=/system/**
#          filters:
#            - StripPrefix=1
        - id: media-api
          # uri: http://127.0.0.1:8081
          uri: lb://media-api
          predicates:
            - Path=/media/**
#          filters:
#            - StripPrefix=1

环境搭建完成以后,启动项目网关和用户两个服务,使用postman进行测试

请求地址:http://localhost:51601/user/api/v1/login/login_auth

认证过滤器

全局过滤器实现jwt校验

思路分析:

  1. 用户进入网关,网关过滤器进行判断是否是登录
  2. 如果是登录则路由到后台管理微服务进行登录,用户登录成功,后台管理微服务签发JWT TOKEN信息返回给用户。
  3. 如果不是登录,网关过滤器接收用户携带的TOKEN。
    1. 网关过滤器解析TOKEN ,判断是否有权限。
      1. 如果有,则放行,路由到具体微服务。
      2. 如果没有,则返回401未认证错误。

具体实现:

1、在认证过滤器中需要用到jwt的解析,所以需要把工具类拷贝一份到网关微服务

2、在网关微服务中新建全局过滤器:

package com.heima.app.gateway.filter;

import com.heima.app.gateway.util.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
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;

@Component//被spring管理的注解
@Slf4j//打印日志的注解
public class AuthorizeFilter implements Ordered, GlobalFilter {//需要实现Ordered, GlobalFilter
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1.获取request和response对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //2.判断是否是登录
        if(request.getURI().getPath().contains("/login")){
            //放行
            return chain.filter(exchange);
        }

        //3.获取token
        String token = request.getHeaders().getFirst("token");

        //4.判断token是否存在
        if(StringUtils.isBlank(token)){//token为空
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();//结束请求
        }

        //5.判断token是否有效
        try {
            Claims claimsBody = AppJwtUtil.getClaimsBody(token);
            //是否是过期
            int result = AppJwtUtil.verifyToken(claimsBody);
            if(result == 1 || result  == 2){
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }

            //获得token解析后中的用户信息
            Object userId = claimsBody.get("id");
            //在header中添加新的信息
            ServerHttpRequest serverHttpRequest = request.mutate().headers(httpHeaders -> {
                httpHeaders.add("userId", userId + "");
            }).build();
            //重置header
            exchange.mutate().request(serverHttpRequest).build();
        } catch (Exception e) {
            e.printStackTrace();//打印失败日志
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //6.放行
        return chain.filter(exchange);
    }

    /**
     * 优先级设置  值越小  优先级越高
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

测试:

启动user服务,继续访问其他微服务,会提示需要认证才能访问,这个时候需要在headers中设置设置token才能正常访问。

拦截器

  • 前端请求先到网关。

  • 认证过滤器在网关中实现,网关拿到token之后解析,保存到header中。

  • 拦截器在具体的微服务中实现,从header中获取用户id,把用户信息存到当前线程中。请求放到当前线程中。这个请求下,所有的位置都可以从当前线程中获取用户。

image-20210426144603541

①:前端发送上传图片请求,类型为MultipartFile。

②:AuthorizeFilter.java网关进行token解析后,把解析后的用户信息存储到header中。

③:自媒体微服务使用拦截器获取到header中的的用户信息,并放入到threadlocal中。

在heima-leadnews-utils中新增工具类

package com.heima.utils.thread;

import com.heima.model.wemedia.pojos.WmUser;

public class WmThreadLocalUtil {

    private final static ThreadLocal<WmUser> WM_USER_THREAD_LOCAL = new ThreadLocal<>();

    /**
     * 添加用户
     * @param wmUser
     */
    public static void  setUser(WmUser wmUser){
        WM_USER_THREAD_LOCAL.set(wmUser);
    }

    /**
     * 获取用户
     */
    public static WmUser getUser(){
        return WM_USER_THREAD_LOCAL.get();
    }

    /**
     * 清理用户
     */
    public static void clear(){
        WM_USER_THREAD_LOCAL.remove();
    }
}

在heima-leadnews-wemedia微服务中新增拦截器

package com.heima.wemedia.interceptor;

import com.heima.model.wemedia.pojos.WmUser;
import com.heima.utils.thread.WmThreadLocalUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Optional;

@Slf4j
public class WmTokenInterceptor implements HandlerInterceptor {

    //前置
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //得到header中的信息,并且存入到当前线程中
        String userId = request.getHeader("userId");
        Optional<String> optional = Optional.ofNullable(userId);
        if(optional.isPresent()){
            //把用户id存入threadloacl中
            WmUser wmUser = new WmUser();
            wmUser.setId(Integer.valueOf(userId));
            WmThreadLocalUtils.setUser(wmUser);
            log.info("wmTokenFilter设置用户信息到threadlocal中...");
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        WmThreadLocalUtil.clear();
    }

    //后置,清理线程中的数据
    /*@Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("清理threadlocal...");
        WmThreadLocalUtils.clear();
    }*/
}

配置使拦截器生效,拦截所有的请求

heima-leadnews/heima-leadnews-service/heima-leadnews-wemedia/src/main/java/com/heima/wemedia/config/WebMvcConfig.java

package com.heima.wemedia.config;

import com.heima.wemedia.interceptor.WmTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration//注解
public class WebMvcConfig implements WebMvcConfigurer {//实现WebMvcConfigurer

    @Override
    public void addInterceptors(InterceptorRegistry registry) {//添加自定义的拦截器
        registry.addInterceptor(new WmTokenInterceptor()).addPathPatterns("/**");
    }
}