在现代分布式系统中,项目网关服务充当着系统的入口点和核心组件之一。它不仅负责接收和转发所有外部请求,还承担了认证、授权、异常处理和流量控制等关键功能。本文将详细介绍我们项目中网关服务的设计动机、主要功能和实现逻辑。
一、设计动机
在复杂的微服务架构中,统一的网关服务是必不可少的。它简化了客户端与后端服务之间的交互,提供了集中管理和保护微服务的能力。我们选择实现一个基于 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);
}
}
}