从单体到微服务:重新理解 Gateway、Spring Security 与请求入口职责

在学习后端认证体系的时候,很容易遇到一个问题:

后端接口请求到底是不是一定先进入 Gateway,然后再进入 Controller?

以前我也容易把这件事想混:

复制代码
前端请求
   ↓
Gateway
   ↓
Controller

好像所有后端接口前面都应该有一个 Gateway。

但真正理解单体应用和微服务架构之后,会发现这个理解并不准确。

更准确的说法是:

有 Gateway 的架构,请求才会先进入 Gateway;
没有 Gateway 的单体应用,请求会直接进入 Spring Boot 应用,再由 Spring Security 和 Spring MVC 处理。


一、先看单体应用的请求链路

单体应用通常是一个 Spring Boot 项目。

里面虽然也有很多 Controller、Service、Mapper,比如:

复制代码
AuthController
UserController
OrderController
ProductController
AdminController

以及:

复制代码
AuthService
UserService
OrderService
ProductService
AdminService

但是它们都在同一个 Spring Boot 进程里面。

所以单体应用的请求链路通常是:

复制代码
App / Web
   ↓
Nginx
   ↓
Spring Boot 单体应用
   ↓
Spring Security Filter Chain
   ↓
DispatcherServlet
   ↓
Controller
   ↓
Service
   ↓
Mapper / DB / Redis

比如请求:

复制代码
GET /user/info

进入 Spring Boot 之后,Spring MVC 会根据 URL 找到对应的 Controller:

复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/info")
    public UserInfoVO getUserInfo() {
        return userService.getUserInfo();
    }
}

也就是说:

复制代码
GET /user/info
   ↓
Spring Boot
   ↓
Spring MVC
   ↓
UserController.getUserInfo()

如果请求是:

复制代码
GET /order/list

那就是:

复制代码
GET /order/list
   ↓
Spring Boot
   ↓
Spring MVC
   ↓
OrderController.list()

这里不需要 Gateway。

因为这些 Controller 本来就在同一个应用里,Spring MVC 自己就可以完成接口分发。


二、单体应用为什么通常用 Spring Security?

单体应用真正要解决的问题不是:

复制代码
这个请求要转发到哪个服务?

而是:

复制代码
当前请求是谁发的?
用户有没有登录?
Token 是否有效?
Redis 中的登录态是否存在?
用户有没有权限访问这个接口?
Controller 或 Service 中怎么拿到当前用户?

这些问题正是 Spring Security 擅长解决的。

单体应用中的认证链路一般是:

复制代码
请求
   ↓
Spring Security Filter Chain
   ↓
JwtAuthenticationFilter
   ↓
解析 JWT
   ↓
校验 Token 是否过期
   ↓
校验 Redis 登录态
   ↓
构造 Authentication
   ↓
放入 SecurityContextHolder
   ↓
进入 Controller

比如一个常见的安全配置:

复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(
            HttpSecurity http,
            JwtAuthenticationFilter jwtAuthenticationFilter
    ) throws Exception {

        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/login", "/auth/register").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/user/**").authenticated()
                .requestMatchers("/order/**").authenticated()
                .anyRequest().authenticated()
            )
            .addFilterBefore(
                jwtAuthenticationFilter,
                UsernamePasswordAuthenticationFilter.class
            );

        return http.build();
    }
}

这里表达的意思是:

复制代码
/auth/login      不需要登录
/auth/register   不需要登录
/admin/**        需要 ADMIN 角色
/user/**         需要登录
/order/**        需要登录
其他接口          默认需要登录

JWT Filter 负责解析 Token,并把用户信息放入 Spring Security 的上下文:

复制代码
Authentication authentication =
        new UsernamePasswordAuthenticationToken(
                loginUser,
                null,
                loginUser.getAuthorities()
        );

SecurityContextHolder.getContext().setAuthentication(authentication);

后面在 Controller 或 Service 中,就可以获取当前登录用户。

这就是 Spring Security 的价值:

复制代码
认证
鉴权
用户上下文
角色权限
方法权限
安全异常处理

所以在单体应用中,Spring Security 更合适。


三、那 Gateway 到底解决什么问题?

Gateway 主要解决的是:

复制代码
多个后端服务的统一入口和路由转发问题

这通常出现在微服务架构中。

微服务不是一个 Spring Boot 应用,而是多个独立运行的 Spring Boot 服务。

比如:

复制代码
auth-service      8081
user-service      8082
order-service     8083
payment-service   8084
message-service   8085

每个服务都是一个独立进程。

每个服务都有自己的 Controller。

比如:

复制代码
auth-service 里面有 AuthController
user-service 里面有 UserController
order-service 里面有 OrderController
payment-service 里面有 PaymentController

这时候,如果前端直接调用各个服务,就会变得很乱:

复制代码
登录接口调用 auth-service
用户接口调用 user-service
订单接口调用 order-service
支付接口调用 payment-service

前端不应该关心后端拆成了多少个服务,也不应该知道每个服务的地址和端口。

所以微服务架构中通常会加一层 Gateway:

复制代码
App / Web
   ↓
Nginx
   ↓
Gateway
   ↓
auth-service
   ↓
user-service
   ↓
order-service
   ↓
payment-service

Gateway 对外暴露统一地址:

复制代码
https://api.xxx.com

然后根据路径做路由转发:

复制代码
/auth/**    → auth-service
/user/**    → user-service
/order/**   → order-service
/pay/**     → payment-service
/message/** → message-service

比如前端请求:

复制代码
GET https://api.xxx.com/order/list

Gateway 收到后判断:

复制代码
/order/** 应该转发给 order-service

于是请求链路变成:

复制代码
App
   ↓
Gateway
   ↓
order-service
   ↓
OrderController.list()

这就是 Gateway 的核心价值之一:服务路由转发


四、Gateway 的"转发"和 Spring MVC 的"分发"不是一回事

这个区别非常关键。

1. Gateway 的转发

Gateway 的转发是跨服务、跨进程的。

比如:

复制代码
Gateway 收到 /order/list
   ↓
转发到 order-service

这里的 order-service 是另一个独立运行的 Spring Boot 应用。

所以 Gateway 解决的是:

复制代码
这个请求应该交给哪个微服务?

2. Spring MVC 的分发

Spring MVC 的分发是在当前 Spring Boot 应用内部完成的。

比如单体应用中:

复制代码
Spring Boot 收到 /order/list
   ↓
Spring MVC 匹配 URL
   ↓
找到 OrderController.list()

这里没有跨进程,也没有跨服务。

所以 Spring MVC 解决的是:

复制代码
当前应用内部,哪个 Controller 方法处理这个请求?

所以可以这样记:

复制代码
Gateway:把请求转发给哪个服务
Spring MVC:把请求分发给哪个 Controller

再直白一点:

复制代码
Gateway 是应用之间的转发
Spring MVC 是应用内部的分发

五、为什么微服务更需要 Gateway?

因为微服务拆分之后,会出现很多公共问题。

比如:

复制代码
每个服务都要校验 Token 吗?
每个服务都要查 Redis 判断登录态吗?
每个服务都要做限流吗?
每个服务都要处理黑白名单吗?
每个服务都要记录入口日志吗?

如果每个服务都重复做一遍,就会出现几个问题:

复制代码
代码重复
规则分散
维护困难
容易不一致
安全策略不好统一

所以微服务中通常会把这些公共入口能力放到 Gateway:

复制代码
统一认证
统一鉴权
统一限流
统一跨域
统一日志
统一黑白名单
统一路由
统一灰度发布

比如认证链路可以设计成:

复制代码
App 请求接口
   ↓
Gateway
   ↓
读取 Authorization Token
   ↓
校验 JWT
   ↓
校验 Redis 登录态
   ↓
校验接口权限
   ↓
通过后转发到具体微服务

如果 Token 无效,Gateway 可以直接返回:

复制代码
401 Unauthorized

请求根本不会进入后面的 Controller。

这就是 Gateway 作为统一入口的价值。


六、微服务有了 Gateway,还需要 Spring Security 吗?

需要看场景,但很多企业项目里仍然会保留。

更合理的设计是:

复制代码
Gateway 做第一层入口校验
具体微服务内部做第二层业务权限控制

Gateway 适合做粗粒度校验:

复制代码
用户有没有登录?
Token 是否有效?
这个接口是否需要登录?
请求是否被限流?
是否在黑名单中?

但是具体业务权限,很多时候还是要在服务内部判断。

比如订单服务中:

复制代码
用户能不能查看这个订单?
这个订单是不是当前用户的?
当前用户有没有商户权限?
当前用户有没有管理员权限?
当前用户能不能导出订单?

这些判断和业务强相关,Gateway 不一定知道。

所以微服务中常见的安全结构是:

复制代码
App / Web
   ↓
Gateway
   ↓
统一认证、限流、路由
   ↓
order-service
   ↓
Spring Security / Method Security
   ↓
OrderController
   ↓
OrderService

也就是说:

复制代码
Gateway 管入口
Spring Security 管服务内部安全

不是有了 Gateway,后面的服务就可以完全裸奔。


七、那单体能不能用 Gateway?

能用。

单体也可以这样部署:

复制代码
App / Web
   ↓
Nginx
   ↓
Gateway
   ↓
Spring Boot 单体应用

但是大多数普通单体项目没有必要这么做。

因为单体只有一个后端应用,没有多个服务需要路由转发。

很多事情在单体内部就可以完成:

复制代码
认证鉴权:Spring Security
接口拦截:Filter / Interceptor
日志记录:AOP / Filter
跨域处理:Spring MVC / Nginx
限流控制:Nginx / Redis / Sentinel

如果只是一个普通后台系统,使用:

复制代码
Nginx + Spring Boot + Spring Security

通常已经够了。

单体强行加 Gateway,反而会增加部署复杂度。


八、最终怎么判断要不要 Gateway?

可以按下面几个问题判断。

1. 后端是不是只有一个 Spring Boot 应用?

如果是,通常不需要 Gateway。

复制代码
App
   ↓
Nginx
   ↓
Spring Boot
   ↓
Spring Security
   ↓
Controller

2. 后端是不是拆成了多个独立服务?

如果是,通常需要 Gateway。

复制代码
App
   ↓
Gateway
   ↓
user-service / order-service / pay-service

3. 是否需要统一入口治理?

如果需要,可以考虑 Gateway。

比如:

复制代码
统一鉴权
统一限流
统一日志
统一跨域
统一灰度
统一黑白名单
统一路由

4. 是否只是普通单体后台?

如果只是普通单体后台,Spring Security 基本够用。


九、最终总结

单体应用和微服务最大的区别,不是 Controller 和 Service 数量多少,而是是不是多个独立进程

单体应用虽然也有很多 Controller 和 Service,但它们都在同一个 Spring Boot 进程中。

所以单体应用中:

复制代码
请求进入 Spring Boot
   ↓
Spring Security 做认证鉴权
   ↓
Spring MVC 分发到 Controller
   ↓
Controller 调用 Service

微服务架构中,每个服务都是独立进程。

所以微服务中:

复制代码
请求进入 Gateway
   ↓
Gateway 做统一入口治理
   ↓
Gateway 根据路径转发到具体微服务
   ↓
微服务 Controller 处理请求

最终可以记住这句话:

复制代码
Gateway 解决的是多个服务的统一入口和路由转发问题;
Spring Security 解决的是当前应用内部的认证授权问题。

再压缩一下:

复制代码
单体:
Nginx → Spring Boot → Spring Security → Controller

微服务:
Nginx → Gateway → 微服务 → Controller

所以不是所有后端接口都必须先经过 Gateway。

更准确的理解是:

单体应用通常以 Spring Security 为主;
微服务架构通常以 Gateway 作为统一入口,同时各个服务内部仍然可以保留 Spring Security 做业务权限保护。