山东大学软件学院项目实训——V-Track:虚拟现实环境下的远程教育和智能评估系统(12)后端网关服务实现

在现代分布式系统中,项目网关服务充当着系统的入口点和核心组件之一。它不仅负责接收和转发所有外部请求,还承担了认证、授权、异常处理和流量控制等关键功能。本文将详细介绍我们项目中网关服务的设计动机、主要功能和实现逻辑。

一、设计动机

在复杂的微服务架构中,统一的网关服务是必不可少的。它简化了客户端与后端服务之间的交互,提供了集中管理和保护微服务的能力。我们选择实现一个基于 Spring Cloud Gateway 的网关服务,因为它提供了丰富的功能和灵活的配置选项,同时与我们的 Spring Boot 应用无缝集成。

二、主要功能

1. 请求路由与转发

网关根据预定义的路由规则将收到的请求动态转发到不同的微服务实例。例如,我们可以定义 /api/user 路径将请求转发到用户服务,而 /api/order 路径则转发到订单服务。

2. 全局异常处理

异常是分布式系统中不可避免的一部分。为了统一处理异常情况并向客户端提供友好的错误信息,我们实现了 GlobalErrorWebExceptionHandler 类。它捕获并处理所有网关内部及外部服务的异常,并返回符合标准的 JSON 格式错误响应。

3. 请求过滤与认证授权

EduGlobalFilter 类是我们自定义的全局过滤器。它负责对进入网关的请求进行认证和授权的检查。例如,我们可以通过 JWT 验证用户的身份,并根据用户角色或权限决定是否允许请求访问特定的服务端点。

4. 实用工具类支持

FilterUtil 是我们的实用工具类,封装了各种常用的过滤器和认证相关方法。例如,它包含了检查请求 URI、解析 JWT 令牌等功能,有助于提高代码的复用性和可维护性。

三、实现

1. IndexController 类

描述: IndexController 是网关服务的入口控制器。它处理根路径 ("/") 的 HTTP GET 请求,并返回一个包含基本信息的 HTML 响应。通常用于确认网关服务是否正常运行以及提供基本的服务信息。

功能:

  • 处理根路径的 GET 请求。
  • 构建并返回包含网关基本信息的 HTML 页面,如网关运行状态和地址。

代码:

package com.roncoo.education.gateway;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

/**

 */
@RestController
public class IndexController {

    @GetMapping("/")
    public Mono<String> index() {
        String html = "<center>Gateway Run Success</center><br/>";
        html = html + "<center>网关地址:http://localhost:7700</center>";
        return Mono.just(html);
    }
}
2. GlobalErrorWebExceptionHandler 类

描述: GlobalErrorWebExceptionHandler 是网关服务的全局异常处理器。它实现了 Spring WebFlux 的 ErrorWebExceptionHandler 接口,用于统一处理网关内部和外部服务抛出的异常。

功能:

  • 捕获并处理所有异常,确保在发生异常时客户端能够接收到统一格式的错误响应。
  • 设置响应的内容类型为 JSON 格式。
  • 根据异常类型设置适当的 HTTP 状态码。
  • 将错误信息转换为 JSON 字符串并写入响应体。
package com.roncoo.education.gateway;

import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.roncoo.education.common.core.base.BaseException;
import com.roncoo.education.common.core.base.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**

 */
@Slf4j
@Configuration
@RequiredArgsConstructor
public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {

    private final ObjectMapper objectMapper;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        log.error(JSONUtil.toJsonStr(ex));
        ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            return Mono.error(ex);
        }

        // 设置返回JSON
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        if (ex instanceof ResponseStatusException) {
            response.setStatusCode(((ResponseStatusException) ex).getStatus());
        }

        return response.writeWith(Mono.fromSupplier(() -> {
            DataBufferFactory bufferFactory = response.bufferFactory();
            try {
                //返回响应结果
                if (ex instanceof BaseException) {
                    return bufferFactory.wrap(objectMapper.writeValueAsBytes(Result.error(((BaseException) ex).getCode(), ex.getMessage())));
                }
                return bufferFactory.wrap(objectMapper.writeValueAsBytes(Result.error(ex.getMessage())));
            } catch (Exception e) {
                log.error("Error writing response", ex);
                return bufferFactory.wrap(new byte[0]);
            }
        }));
    }
}
3. EduGlobalFilter 类

描述: EduGlobalFilter 是网关服务的全局过滤器。它实现了 Spring Cloud Gateway 的 GlobalFilter 接口和 Ordered 接口,用于拦截进入网关的所有请求并进行处理。

功能:

  • 实现了请求的认证和授权逻辑,例如基于 JWT 的身份验证。
  • 根据预定义的规则过滤并处理请求,如跳过不需要认证的路径。
  • 更新请求的头部信息,如添加用户 ID。
  • 将处理后的请求继续传递给下一个过滤器或目标服务。
package com.roncoo.education.gateway.filter;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.roncoo.education.common.core.base.BaseException;
import com.roncoo.education.common.core.enums.ResultEnum;
import com.roncoo.education.common.core.tools.Constants;
import com.roncoo.education.common.core.tools.JWTUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**

 */
@Slf4j
@Component
public class EduGlobalFilter implements GlobalFilter, Ordered {

    /**
     * admin不需要token校验的接口
     */
    private static final List<String> EXCLUDE_TOKEN_URL = Arrays.asList(
            "/system/admin/login/password",
            "/system/admin/register/password",
            "/system/admin/sys/role/user/save",
            "/user/admin/lecturer/save",
            "/course/admin/user/course/save",
            "/course/admin/user/study/page"
    );

    /**
     * admin不需要权限校验的接口
     */
    private static final List<String> EXCLUDE_URL = Arrays.asList(
            "/system/admin/sys/menu/user/list",
            "/system/admin/sys/menu/permission/list",
            "/system/admin/sys/user/current",
            "/system/admin/sys/role/user/save",
            "/user/admin/lecturer/save",
            "/course/admin/category/list"
    );

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 优先级,order越大,优先级越低
     *
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String uri = request.getPath().value();
        if (FilterUtil.checkUri(uri, FilterUtil.CALLBACK_URL_PREFIX)) {
            // 路径存在关键词:/callback,不鉴权
            return chain.filter(exchange);
        }
        if (FilterUtil.checkUri(uri, FilterUtil.API_URL_PREFIX)) {
            // 路径存在关键词:/api,不鉴权
            return chain.filter(exchange);
        }
        if (FilterUtil.checkUri(uri, FilterUtil.API_V3)) {
            // 路径存在关键词:/v3,不鉴权
            return chain.filter(exchange);
        }
        if (FilterUtil.checkUri(uri, FilterUtil.IMAGES)) {
            // 路径存在关键词:/images
            return chain.filter(exchange);
        }
        // 额外不需要token认证的接口
        if (EXCLUDE_TOKEN_URL.contains(uri)) {
            return chain.filter(exchange);
        }

        Long userId = getUserId(request);
        if (FilterUtil.checkUri(uri, FilterUtil.ADMIN_URL_PREFIX)) {
            // admin校验
            if (!stringRedisTemplate.hasKey(Constants.RedisPre.ADMINI_MENU.concat(userId.toString()))) {
                throw new BaseException(ResultEnum.MENU_PAST);
            }
            String tk = stringRedisTemplate.opsForValue().get(Constants.RedisPre.ADMINI_MENU.concat(userId.toString()));
            // 校验接口是否有权限
            if (!checkUri(uri, tk)) {
                throw new BaseException(ResultEnum.MENU_NO);
            }
            // 更新时间,使用户菜单不过期
            stringRedisTemplate.expire(Constants.RedisPre.ADMINI_MENU.concat(userId.toString()), Constants.SESSIONTIME, TimeUnit.MINUTES);
        }
        request.mutate().header(Constants.USER_ID, String.valueOf(userId));
        return chain.filter(exchange);
    }

    // 校验用户是否有权限
    private static Boolean checkUri(String uri, String tk) {
        if (StringUtils.hasText(uri) && uri.endsWith("/")) {
            uri = uri.substring(0, uri.length() - 1);
        }

        // 额外不需要权限校验的接口
        if (EXCLUDE_URL.contains(uri)) {
            return true;
        }
        // 权限校验
        uri = uri.substring(1).replace("/", ":");
        if (tk.contains(uri)) {
            return true;
        }

        log.info("用户没该权限点,{}", uri);
        return false;
    }

    private Long getUserId(ServerHttpRequest request) {
        // 头部
        String token = request.getHeaders().getFirst(Constants.TOKEN);
        if (!StringUtils.hasText(token)) {
            throw new BaseException("token不存在,请重新登录");
        }

        if (!stringRedisTemplate.hasKey(token)) {
            throw new BaseException(ResultEnum.TOKEN_PAST);
        }

        // 解析 token
        DecodedJWT jwt = null;
        try {
            jwt = JWTUtil.verify(token);
        } catch (Exception e) {
            log.error("token异常,token={}", token);
            throw new BaseException(ResultEnum.TOKEN_ERROR);
        }

        // 校验token
        if (null == jwt) {
            throw new BaseException(ResultEnum.TOKEN_ERROR);
        }
        Long userId = JWTUtil.getUserId(jwt);
        if (userId <= 0) {
            throw new BaseException(ResultEnum.TOKEN_ERROR);
        }

        // 更新时间,使token不过期
        stringRedisTemplate.expire(token, Constants.SESSIONTIME, TimeUnit.MINUTES);
        return userId;
    }

}
4. FilterUtil 类

描述: FilterUtil 是一个实用工具类,包含了在网关服务中常用的方法和常量。它提供了各种静态方法,用于帮助过滤器和其他组件执行特定的功能。

功能:

  • 定义了各种常量,如 API 路径前缀和不需要认证的路径列表。
  • 封装了通用的过滤器方法,如检查请求 URI 是否以特定前缀开头的功能。
  • 包含了解析 JWT 令牌等与认证相关的实用方法。
package com.roncoo.education.gateway.filter;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.roncoo.education.common.core.base.BaseException;
import com.roncoo.education.common.core.enums.ResultEnum;
import com.roncoo.education.common.core.tools.Constants;
import com.roncoo.education.common.core.tools.JWTUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;

/**
 * 过滤器工具类
 *
 */
@Slf4j
public final class FilterUtil {

    private FilterUtil() {
    }

    /**
     * 技术文档
     */
    public static final String API_V3 = "/v3";
    /**
     * 图片
     */
    public static final String IMAGES = "/images";
    /**
     * Api路径前缀
     */
    public static final String API_URL_PREFIX = "/api";
    /**
     * Boss路径前缀
     */
    public static final String CALLBACK_URL_PREFIX = "/callback";
    /**
     * Admin路径前缀
     */
    public static final String ADMIN_URL_PREFIX = "/admin";

    /**
     * 校验uri里面的第二个斜杠的字符串
     *
     * @param uri 请求URL
     * @param key 需要校验的字符串
     * @return 校验结果
     */
    public static boolean checkUri(String uri, String key) {
        String result = uri.substring(uri.indexOf("/", uri.indexOf("/") + 1));
        return result.startsWith(key);
    }

    public static boolean startsWith(String uri, String key) {
        return uri.startsWith(key);
    }

    /**
     * 获取JWT解码信息
     *
     * @param request 访问请求
     * @return JWT解码信息
     */
    public static DecodedJWT getDecodedJWT(ServerHttpRequest request) {
        // 头部
        String token = request.getHeaders().getFirst(Constants.TOKEN);
        // 路径
        if (!StringUtils.hasText(token)) {
            token = request.getQueryParams().getFirst(Constants.TOKEN);
        }
        // 检验token
        if (!StringUtils.hasText(token)) {
            throw new BaseException(ResultEnum.TOKEN_PAST);
        }

        // 解析 token
        try {
            return JWTUtil.verify(token);
        } catch (Exception e) {
            log.error("token异常,token={}", token, e);
            throw new BaseException(ResultEnum.TOKEN_ERROR);
        }
    }
}

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值