跳转至

网关

模块环境架构的问题分析

项目中的微服务越来越多,由前端直接请求微服务的方式存在弊端:

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

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

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

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

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

1de1ec2d-eb83-44f7-8781-d705788f35ff

流程如下:

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

要使用网关首先搭建Nacos

网关认证

一个前端请求首先会经过网关,之后再路由到一个个的微服务。

API网关(Gateway)是应用程序客户端的单一入口点,它位于客户端和应用程序的后端服务集合之间。

流程

  1. 客户端向API网关发送请求,该请求通常基于HTTP协议。
  2. API网关验证HTTP请求。
  3. API网关根据其允许列表和拒绝列表检查调用者的IP地址和其它HTTP请求头,还可以针对IP地址和HTTP请求头等属性执行基本的速率限制检查。例如:它可以拒绝来自超过一定速率的IP地址的请求。
  4. API网关将请求传递给身份验证服务,以进行身份验证和授权。API网关从身份验证服务处接收经过身份验证的会话,其中包含允许请求执行的操作范围。
  5. 对经过身份验证的会话应用更高级别的速率限制检查,如果超过限制,此时请求将被拒绝。
  6. 服务发现组件的帮助下,API网关通过路径匹配找到适当的后端服务来处理请求。
  7. 网关将请求转换为适当的协议,并将转换后的请求发送到后端服务(例如gRPC)。当响应从后端服务返回时,网关会将响应转换回面向公众的协议,并将响应返回给客户端。

技术方案

认证服务颁发jwt令牌,客户端携带jwt访问微服务,统一让网关校验令牌的合法性,在每一个微服务处不再校验令牌的合法性。

所有访问微服务的请求都要经过网关,在网关进行用户身份的认证可以将很多非法的请求拦截到微服务以外,这叫做网关认证。

网关是基础设置的关键部分,应将其部署到多个区域以提高可用性。

网关的职责

网站白名单维护

针对不用认证的URL全部放行。

身份验证和安全性

进行身份验证和授权,保护后端服务。

除了白名单剩下的就是需要认证的请求,网关需要验证jwt的合法性,jwt合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问。

路由转发

将请求转发到合适的微服务。减少外界对接微服务的成本。路由可以根据请求路径进行路由、根据host地址进行路由等。

负载均衡

一个微服务有多个实例时可以通过负载均衡算法进行路由,以提高性能和可用性。

协议转换和服务发现

处理不同协议之间的转换,例如从 HTTP 到 MQTT。

断路

网关应跟踪错误,并提供断路功能以防止服务过载。

监控与日志记录

收集和分析流量数据,以便于监控和故障排查。

分析和计费

缓存

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

网关不负责授权

对请求的授权操作在各个微服务进行,因为微服务最清楚用户有哪些权限访问哪些接口。

实现网关认证

1、在网关工程添加依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

2、配置白名单文件security-whitelist.properties

/**=临时全部放开
/auth/**=认证地址,也就是注册登录申请令牌的接口
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口

注意:由于目前还没有开发认证功能,前端请求网关的URL不在白名单中时会“没有认证”的错误,暂时在白名单中添加全部放行配置,待认证功能开发完成再屏蔽全部放行配置。

重启网关工程,进行测试

1、申请令牌

2、通过网关访问资源服务

当token正确时可以正常访问资源服务,token验证失败返回token无效。

由于是在网关处进行令牌校验,所以在微服务处不再校验令牌的合法性,修改内容管理服务的ResouceServerConfig类,屏蔽authenticated()。

 @Override
 public void configure(HttpSecurity http) throws Exception {
  http.csrf().disable()
          .authorizeRequests()
//          .antMatchers("/r/**","/course/**").authenticated()//所有/r/**的请求必须认证通过
          .anyRequest().permitAll()
  ;
 }

搭建Gateway

使用Spring Cloud Gateway作为网关。

新建一个网关工程,添加依赖:

<!--网关-->
<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>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</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>

配置网关的application.yml配置文件

#微服务配置
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

启动网关和用户两个服务,使用postman进行测试。

网关工程搭建完成即可将前端工程中的接口地址改为网关的地址。

GlobalFilter

全局过滤器实现jwt校验

思路分析:

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

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

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;
    }
}

测试:

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

HandlerInterceptor拦截器

  • 前端请求先到网关。

  • 认证过滤器在网关中实现,网关拿到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();
    }
}

在具体微服务中新增拦截器

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();
    }*/
}

注册拦截器

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

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("/**");
    }
}