一、编写 bootstrap 配置文件
spring: application: name: gateway-service main: web-application-type: reactive # 设置为响应式应用类型,避免与Spring MVC冲突 # Jackson配置 jackson: date-format: HH:mm:ss time-zone: UTC serialization: write-dates-as-timestamps: false fail-on-empty-beans: false deserialization: fail-on-unknown-properties: false fail-on-null-for-primitives: false accept-empty-string-as-null-object: true accept-single-value-as-array: true # Redis配置 - Gateway 服务使用更长的超时时间 data: redis: host: *** port: 6379 password: *** database: 0 timeout: 10000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms # Nacos服务发现配置 cloud: nacos: discovery: server-addr: ***:8848 username: nacos password: *** namespace: public enabled: true # 启用 Nacos 服务发现 gateway: # 默认过滤器 default-filters: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin # - JwtAuth # 全局跨域配置 globalcors: cors-configurations: '[/**]': # 允许跨域的源(使用 allowedOriginPatterns 支持通配符和凭证) allowedOriginPatterns: "*" # 允许跨域的HTTP方法 allowedMethods: - GET - POST - PUT - DELETE - OPTIONS # 允许跨域的请求头 allowedHeaders: "*" # 允许携带凭证 allowCredentials: true # 跨域预检的有效期,单位为秒 maxAge: 3600 # 路由配置 routes: # 用户服务路由 - id: user-service uri: lb://user-service predicates: - Path=/api/v1/users/** filters: - StripPrefix=2 # 去掉 /api/v1,保留后续路径 # 文件服务路由 - id: file-service uri: lb://file-service predicates: - Path=/api/v1/files/** filters: - StripPrefix=2 # 去掉 /api/v1,保留后续路径 # AI服务路由 - llm接口 - id: ai-service-llm uri: lb://ai-service predicates: - Path=/api/v1/llm/** filters: - StripPrefix=2 # 去掉 /api/v1,保留后续路径 # AI服务路由 - ai接口 - id: ai-service-ai uri: lb://ai-service predicates: - Path=/api/v1/ai/** filters: - StripPrefix=2 # 去掉 /api/v1,保留后续路径 # MyBatis Plus公共配置 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto table-underline: true # 安全配置 - 网关令牌(所有服务共享) security: gateway: token: "internal-gateway-secret-token-2024" enabled: true # 设置为 false 可以禁用网关检查(开发环境) whitelist: - /users/auth - /users/verification-code # 自定义日志配置 logging: pattern: console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){cyan} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx"
二、创建配置类
java
package com.linzhixin.gateway.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 网关配置类(空路由,在bootstrap里面已经配置类路由规则,其实这个配置类可以省略)
*/
@Configuration
public class GateWayConfig {
/**
* 自定义路由配置
* @param builder RouteLocatorBuilder构造器
* @return RouteLocator实例对象
*/
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().build();
}
}
java
package com.linzhixin.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis 配置类
* Spring 确实会自动配置,但默认序列化方式不友好。这个配置类主要是为了把序列化方式改成 String,确保 Redis 数据可读、可共享。
*/
@Configuration
public class RedisConfig {
/**
* 构建 ReactiveRedisTemplate -》 用于在响应式编程环境中(如 Gateway/WebFlux)异步操作 Redis。
* @param connectionFactory 连接工厂
* @return ReactiveRedisTemplate示例对象
*/
@Bean
public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory) {
// 1、序列化
// StringRedisSerializer 是 Spring Data Redis 提供的一个序列化器,负责 Java 对象 ↔ Redis 存储格式 的转换。
StringRedisSerializer serializer = new StringRedisSerializer();
// 2、创建 builder
RedisSerializationContext.RedisSerializationContextBuilder<String, String> builder =
RedisSerializationContext.newSerializationContext();
RedisSerializationContext<String, String> context = builder
.key(serializer)
.value(serializer)
.hashKey(serializer)
.hashValue(serializer)
.build();
return new ReactiveRedisTemplate<>(connectionFactory, context);
}
}
三、实现令牌过滤器(写在 common-service 中)
java
package com.linzhixin.common.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* 网关令牌验证过滤器
* GatewayTokenFilter 会作用于所有依赖 common-service 的服务,只要这些服务启动时被 Spring 扫描到。
*/
@Component
@Slf4j
@Order(1)
public class GatewayTokenFilter implements Filter {
/**
* 网关令牌
*/
@Value("${security.gateway.token:}")
private String gatewayToken;
/**
* 是否启用网关令牌(网关控制器)
*/
@Value("${security.gateway.enabled:true}")
private boolean gatewayCheckEnable;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if (gatewayCheckEnable) {
log.info("网关令牌过滤器已经启动");
} else {
log.info("允许直接访问后端");
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws
IOException, ServletException {
// 1、需要请求request
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
String path = httpServletRequest.getRequestURI();
String method = httpServletRequest.getMethod();
// 2、网关控制器做判断
if(!gatewayCheckEnable) {
//将请求和响应传递给过滤器链中的下一个过滤器,如果已是最后一个过滤器,则传递给目标 Servlet/Controller。
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 3、获取网关令牌
String gatewayToke = httpServletRequest.getHeader("X-Gateway-Token");
if(gatewayToke == null || gatewayToke.isEmpty()) {
log.warn("网关令牌为空");
unAuthorized(httpServletResponse, "禁止访问");
return;
}
// 4、验证网关令牌
if(!gatewayToke.equals(this.gatewayToken)) {
log.warn("网关令牌错误");
unAuthorized(httpServletResponse, "禁止访问");
return;
}
log.info("网关令牌验证通过:{} {}", method, path);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
@Override
public void destroy() {
Filter.super.destroy();
}
private void unAuthorized(HttpServletResponse response, String message) {
response.setStatus(403);
//设置 HTTP 响应的内容类型和字符编码,告诉浏览器/客户端"我返回的是 JSON 数据,用 UTF-8 解码"。
response.setContentType("application/json;charset=UTF-8");
//用 String.format 把错误码和错误信息动态拼成 JSON 字符串。
// { : JSON 对象开始
// \"code\" : 转义双引号 → 表示 JSON 的 key 是 code
//:%d :占位符,填入整数
String json = String.format(
"{\"code\":%d,\"message\":\"%s\"}",
403,
message
);
try {
//获取响应的字符输出流,将 JSON 字符串写入响应体,发送给客户端。
response.getWriter().write(json);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}