基于Spring Cloud + Vue3的微服务架构实践:Eureka、Gateway、Sentinel、Spring Security6 RBAC与Spring Boot Admin
随着业务复杂度上升,单体应用逐渐暴露出部署慢、扩展难、耦合高等问题。微服务架构成为主流选择,但随之而来的是服务治理、流量管控、安全认证、可观测性等一系列新挑战。本文结合 Spring Cloud Netflix 生态(Eureka服务注册与发现)、Spring Cloud Gateway 路由网关、Sentinel 流量控制、Spring Boot Admin 监控以及 Spring Security6 基于RBAC的权限控制,并采用 Vue3 构建前端,提供一套前后端分离的完整微服务解决方案。架构中各组件各司其职,既保证系统弹性,又简化开发运维。
Admin 监控以及 Spring Security6 基于RBAC的权限控制,并采用 Vue3 构建前端,提供一套前后端分离的完整微服务解决方案。架构中各组件各司其职,既保证系统弹性,又简化开发运维。
2. 整体架构设计
下图展示了本方案的总体架构,包含用户端、API网关、服务注册中心、业务微服务、监控与流控组件,以及前端Vue应用。

核心流程:
-
Vue3前端发起请求 → Gateway网关
-
Gateway中Sentinel进行流量控制,JWT过滤器验证Token合法性
-
Gateway根据请求路径动态路由到后端微服务(基于Eureka服务发现)
-
微服务内部利用Spring Security6 + RBAC进行细粒度权限校验
-
所有微服务注册到Eureka Server,Spring Boot Admin通过Eureka自动发现并监控各实例健康状态
下面我们先看看Eureka ,spring admin,后台:






代码结构如图:

涉及到4个模块:
<modules>
<module>eureka</module>
<module>gateway</module>
<module>back</module>
<module>monitor</module>
</modules>
下面我来讲一下怎么实现和原理。
首先建个maven父工程项目:
pom文件:
<properties>
<java.version>17</java.version>
<spring-cloud.version>2024.0.1</spring-cloud.version>
<spring-boot-admin.version>3.4.5</spring-boot-admin.version>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-dependencies</artifactId>
<version>${spring-boot-admin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
然后依次创建每个子项目工程。
3. 核心组件详解与集成步骤
3.1 Eureka 服务注册中心
作用:提供服务注册与发现能力,所有微服务启动时向Eureka Server注册自身地址,Gateway可通过服务名调用具体实例。
配置示例(Eureka Server):
yaml
server:
address: 127.0.0.1
port: 20000
servlet:
context-path: /
session:
timeout: 86400
compression:
enabled: true
mime-types: application/json,application/xml,text/html,text/xml,text/plain
min-response-size: 2048
spring:
application:
name: eureka-server
main:
web-application-type: servlet
eureka:
client:
#由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己
register-with-eureka: true
#由于注册中心的职责就是维护服务实例,它并不需要去检索服务,所以也设置为false
fetch-registry: false
service-url:
defaultZone: http://127.0.0.1:20000/eureka/
server:
#注册中心清理间隔,单位为毫秒
eviction-interval-timer-in-ms: 10000
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
instance-id: ${spring.application.name}:${server.port}
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 15
客户端(微服务)依赖:
pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
添加@EnableEurekaServer 注解即可。

配置服务名称和Eureka地址后,启动时自动注册。
3.2 Spring Boot Admin 监控
作用:集中展示微服务的健康状态、日志、JVM指标等信息。
Admin Server 需要添加依赖并配置:
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
默认是没有登录密码的,添加一个登录密码即可。
配置SecurityConfig
@Bean
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
SavedRequestAwareAuthenticationSuccessHandler successHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminServer.path("/"));
http.authorizeHttpRequests(auth -> auth
.requestMatchers(
adminServer.path("/assets/**"),
adminServer.path("/login"),
"/actuator/**",
"/instances",
"/instances/*"
).permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage(adminServer.path("/login"))
.successHandler(successHandler)
)
.logout(logout -> logout
.logoutUrl(adminServer.path("/logout"))
.logoutSuccessUrl(adminServer.path("/login"))
)
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers(
new AntPathRequestMatcher("/instances"),
new AntPathRequestMatcher("/instances/*"),
new AntPathRequestMatcher("/actuator/**")
)
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.builder()
.username("serviceUaa")
.password(encoder.encode("123456"))
.roles("SERVICE_NODE_ROLE")
.build();
return new InMemoryUserDetailsManager(admin);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
yaml文件:
server:
port: 8999
address: 127.0.0.1
spring:
application:
name: admin-server
profiles:
active: dev # 环境 dev test prod
security:
user:
name: serviceUaa
password: 123456
roles: SERVICE_NODE_ROLE
eureka:
instance:
health-check-url-path: /actuator/health
instance-id: ${spring.application.name}:${server.port}:@project.version@
# 优先使用IP地址方式进行注册服务
prefer-ip-address: true
# 配置使用指定IP
ip-address: ${server.address}
# 配置使用主机名注册服务
hostname: ${server.address} #保持和服务器ip一致
lease-renewal-interval-in-seconds: 5 # 续约更新时间间隔(默认30秒)
lease-expiration-duration-in-seconds: 15 # 续约到期时间(默认90秒)
metadata-map:
user.name: ${spring.security.user.name}
user.password: ${spring.security.user.password}
client:
registryFetchIntervalSeconds: 30
registerWithEureka: true # 是否从其他的服务中心同步服务列表,单机false,集群true
fetchRegistry: true # 是否把自己作为服务注册到其他服务注册中心,单机false,集群true
healthcheck:
enabled: true
service-url:
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@127.0.0.1:20000/eureka/
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: ALWAYS
enabled: true
shutdown:
enabled: true
添加
@EnableAdminServer @EnableDiscoveryClient ,启动可以看到注册中心能发现监控服务
在主类标注 @EnableAdminServer,并配置Eureka客户端,从Eureka抓取服务列表。
配置 spring.boot.admin.client.url 指向Admin Server地址,并开放端点 management.endpoints.web.exposure.include=*。最终Admin UI展示所有注册的服务实例。

3.3 Spring Cloud Gateway + Sentinel 流控
依赖(使用Spring Cloud Alibaba Sentinel):
xml
server:
address: 127.0.0.1
port: 8090
spring:
application:
name: gateway-service
main:
web-application-type: reactive
# # 安全参数配置
security:
user:
name: serviceUaa
password: 123456
roles: SERVICE_NODE_ROLE
cloud:
inetutils:
# 忽略指定网卡,支持正则表达式(这里使用正则表达式忽略所有虚拟机网卡)
ignoredInterfaces: [ 'VMware.*' ]
gateway:
httpclient:
connect-timeout: 2000
response-timeout: 60000
pool:
max-idle-time: 60000
max-connections: 200
max-life-time: 1800000
acquire-timeout: 2000
eviction-interval: 5000
discovery:
locator:
# 是否和服务注册与发现组件结合,设置为 true 后可以直接使用应用名称调用服务
enabled: true
##是否将请求路径的服务名配置改成小写 因为服务注册的时候,向注册中心注册时将服务名转成大写的了
# lower-case-service-id: true
globalcors: #网关跨域设置
cors-configurations:
'[/**]':
allowed-origins-patterns:
- "*"
allowed-headers:
- "*"
allow-credentials: true
allowed-methods:
- GET
- POST
- DELETE
- PUT
- OPTIONS
- HEAD
routes:
- id: api #API文档服务熔断
uri: lb://admin-server
predicates:
- Path=/monitor/**
filters:
- StripPrefix=1
- id: back-server
uri: lb://back-server
predicates:
- Path=/iot-api/**
filters:
- StripPrefix=1
security:
ignore-urls:
- /auth/login
- /auth/register
- /auth/captcha
- /public/**
- /actuator/**
- /health
- /swagger-ui/**
- /v3/api-docs/**
- /doc.html
- /webjars/**
jwt:
key: uaaa.cn
blacklist-enabled: true
ribbon:
eureka:
enabled: true
eager-load:
enabled: true
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:20000/eureka/
register-with-eureka: true
fetch-registry: true
#Eureka客户端开启健康检查
healthcheck:
enabled: true
#采用IP注册
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
#定义实例ID格式
instance-id: ${spring.application.name}:${server.port}
#设置Eureka客户端租期更新时间,默认30秒
lease-renewal-interval-in-seconds: 10
#设置租期到期时间,默认90秒
lease-expiration-duration-in-seconds: 15
## 暴露所有端点(开发测试用)
management:
endpoints:
web:
exposure:
include: '*' # 暴露所有端点。生产环境建议按需暴露,如 health, info, gateway
endpoint:
health:
show-details: always
添加GatewayConfiguration配置:
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(-1)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
// 注册自定义的限流异常处理器
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@PostConstruct
public void initBlockHandler() {
BlockRequestHandler blockRequestHandler = (exchange, t) -> {
Map<String, Object> result = new HashMap<>();
result.put("code", 429);
result.put("message", "系统繁忙,请稍后再试");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(result);
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
流控配置:
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
// 为 user-service-route 路由设置 QPS 限流规则
GatewayFlowRule routeRule = new GatewayFlowRule("back-server")
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID)
.setCount(10) // QPS 阈值
.setIntervalSec(1);
rules.add(routeRule);
GatewayRuleManager.loadRules(rules);
}
token拦截配置
private List<String> ignoreUrls = new ArrayList<>();
private Jwt jwt = new Jwt();
@Data
public static class Jwt {
private String key = "uaaa.cn";
private Boolean blacklistEnabled = true;
}
添加全局过滤器,拦截token
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
log.info("========== 网关收到请求 ==========");
log.info("请求路径: {}", path);
log.info("请求方法: {}", request.getMethod());
AntPathMatcher pathMatcher = new AntPathMatcher();
List<String> ignoreUrls = securityProperties.getIgnoreUrls();
// 智能匹配:支持多种路径格式
boolean shouldIgnore = ignoreUrls.stream().anyMatch(url -> {
// 1. 直接匹配原始路径
if (pathMatcher.match(url, path)) {
log.debug("路径 {} 直接匹配白名单: {}", path, url);
return true;
}
// 2. 尝试去掉第一段路径后匹配(处理 /iot-api/auth/login -> /auth/login)
String[] segments = path.split("/");
if (segments.length > 2) {
String strippedPath = "/" + String.join("/", Arrays.copyOfRange(segments, 2, segments.length));
if (pathMatcher.match(url, strippedPath)) {
log.debug("路径 {} 去掉前缀后 {} 匹配白名单: {}", path, strippedPath, url);
return true;
}
}
return false;
});
if (shouldIgnore) {
log.info("✅ 路径 {} 在白名单中,跳过JWT验证", path);
return chain.filter(exchange);
}
log.info("❌ 路径 {} 不在白名单中,需要JWT验证", path);
String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (token == null || !token.startsWith(TOKEN_PREFIX)) {
log.warn("请求路径: {}, 缺少有效的 Authorization Header", path);
return onError(exchange, HttpStatus.UNAUTHORIZED, "未提供有效的认证令牌");
}
token = token.substring(TOKEN_PREFIX.length());
String secretKey = securityProperties.getJwt().getKey();
boolean isValid = JwtUtil.validateToken(token, secretKey);
if (!isValid) {
log.warn("JWT Token 验证失败,路径: {}", path);
return onError(exchange, HttpStatus.UNAUTHORIZED, "Token 无效或已过期");
}
String username = JwtUtil.getUsername(token);
if (username != null) {
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Name", username)
.build();
ServerWebExchange mutatedExchange = exchange.mutate()
.request(mutatedRequest)
.build();
log.info("用户 {} 认证成功,路径: {}", username, path);
return chain.filter(mutatedExchange);
}
return chain.filter(exchange);,
启动配置类加上
@EnableDiscoveryClient 注解即可
Sentinel限流规则:通过Sentinel Dashboard为Gateway路由配置流控规则(如QPS限制),也可通过代码动态加载。例如对 /api/user/login 接口设置单机阈值为10 QPS。
3.4 Spring Security6 RBAC 权限控制(核心)
本方案在微服务内部(以User Service为例)实现基于RBAC的权限校验,数据模型包含用户、角色、权限三张表,采用JWT作为认证凭证。
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- MyBatis-Plus https://baomidou.com-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!--Knife4j https://doc.xiaominfo.com/-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- Java工具类库 https://doc.hutool.cn -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- 允许使用Lombok的Java Bean类中使用MapStruct注解 (Lombok 1.18.20+) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
yaml
server:
address: 127.0.0.1
port: 9999
spring:
application:
name: back-server
profiles:
active: dev # 环境 dev test prod
#mybatis
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.iot.*.domain
global-config:
#数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: none
# 逻辑删除全局属性名(驼峰和下划线都支持)
logic-delete-field: valid
logic-delete-value: 0 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 1 # 逻辑未删除值(默认为 0)
banner: false
#原生配置
configuration:
# 驼峰下划线转换
map-underscore-to-camel-case: true
cache-enabled: false
call-setters-on-nulls: true
jdbc-type-for-null: 'null'
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 安全配置
security:
jwt:
# JWT 秘钥
key: aaaau.cn
# JWT 有效期(单位:秒)
ttl: 7200
# JWT 的黑名单功能是否启用
blacklistEnabled: true
# 白名单列表
ignore-urls:
- /v3/api-docs/**
- /doc.html
- /swagger-resources/**
- /webjars/**
- /swagger-ui/**
- /swagger-ui.html
- /auth/login
- /auth/captcha
- /ws/**
# springdoc配置: https://springdoc.org/properties.html
springdoc:
swagger-ui:
path: /swagger-ui.html
operationsSorter: alpha
tags-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: '认证中心'
paths-to-match: "/**"
packages-to-scan:
- com.iot.business.auth.controller
- group: '系统管理'
paths-to-match: "/**"
packages-to-scan:
- com.iot.business.system.controller
# 验证码配置
captcha:
# 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码
type: circle
# 验证码宽度
width: 130
# 验证码高度
height: 48
# 验证码干扰元素个数
interfere-count: 2
# 文本透明度(0.0-1.0)
text-alpha: 0.8
# 验证码字符配置
code:
# 验证码字符类型 math-算术|random-随机字符
type: math
# 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数
length: 1
# 验证码字体
font:
# 字体名称 Dialog|DialogInput|Monospaced|Serif|SansSerif
name: SansSerif
# 字体样式 0-普通|1-粗体|2-斜体
weight: 1
# 字体大小
size: 20
# 验证码有效期(秒)
expire-seconds: 120
# 日志配置
logging:
config: classpath:logback-spring.xml
file:
path: ../logs/iot
level:
root: info
com.iot: debug
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:20000/eureka/
healthcheck:
enabled: true
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
instance-id: ${spring.application.name}:${server.port}
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 15
步骤:
3.4.1 JWT 生成与解析
登录成功后生成Token,返回给前端。后续请求放入Header Authorization: Bearer <token>。
3.4.2 自定义JwtValidationFilter
继承 OncePerRequestFilter,从请求头提取Token,解析用户信息并创建 UsernamePasswordAuthenticationToken 存入 SecurityContextHolder。
java
@Slf4j
public class JwtValidationFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
// 密钥
private final byte[] secretKey;
// 忽略的URL列表
private final List<String> ignoreUrls;
private final Boolean blacklistEnabled;
private final StringRedisTemplate redisTemplate;
public JwtValidationFilter(UserDetailsService userDetailsService, SecurityProperties properties,StringRedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.secretKey = properties.getJwt().getKey().getBytes();
this.ignoreUrls = properties.getIgnoreUrls();
this.blacklistEnabled = properties.getJwt().getBlacklistEnabled();
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @Nonnull HttpServletResponse response, @Nonnull FilterChain chain) throws ServletException, IOException {
String method = request.getMethod();
String requestUri = request.getRequestURI();
// ✅ 最关键:直接放行所有OPTIONS预检请求
if ("OPTIONS".equalsIgnoreCase(method)) {
log.debug("OPTIONS预检请求,直接放行: {}", requestUri);
chain.doFilter(request, response);
return;
}
System.out.println(request.getServletPath());
// 如果黑名单功能启用,且请求头中有Authorization信息,则检查Token是否在黑名单中
if(Boolean.TRUE.equals(blacklistEnabled)){
log.info("JwtValidationFilter: 检查黑名单功能启用");
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
JWT jwt = JWTUtil.parseToken(token);
String jti = jwt.getPayloads().getStr(JWTPayload.JWT_ID);
if (StrUtil.isNotBlank(jti) && Boolean.TRUE.equals(redisTemplate.hasKey("jwt:blacklist:" + jti))) {
log.error("用户尝试使用已失效的Token访问系统,Token已被加入黑名单,Payload: {}", jwt.getPayloads());
SecurityContextHolder.clearContext();
throw new ApiException(ResultCode.UNAUTHORIZED);
}
}
}else {
log.info("JwtValidationFilter: 黑名单功能未启用");
}
// 忽略的URL列表
AntPathMatcher pathMatcher = new AntPathMatcher();
if (ignoreUrls.stream().anyMatch(url -> pathMatcher.match(url, requestUri))) {
log.debug("路径 {} 在白名单中,直接放行", requestUri);
chain.doFilter(request, response);
return;
}
// 获取请求token
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
try {
// 如果请求头中没有Authorization信息,或者Authorization以Bearer开头,则认为是匿名用户
if (StrUtil.isBlank(token) || !token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 去除 Bearer 前缀
token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
// 解析 Token
JWT jwt = JWTUtil.parseToken(token);
// 检查 Token 是否有效(验签 + 是否过期)
boolean isValidate = jwt.setKey(secretKey).validate(0);
if (!isValidate) {
log.error("JwtValidationFilter error: token is invalid");
throw new ApiException(ResultCode.UNAUTHORIZED);
}
JSONObject payloads = jwt.getPayloads();
String username = payloads.getStr(JWTPayload.SUBJECT);
SysUserDetails userDetails = (SysUserDetails) this.userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
log.error("JwtValidationFilter error: {}", e.getMessage());
SecurityContextHolder.clearContext();
throw new ApiException(ResultCode.UNAUTHORIZED);
}
// Token有效或无Token时继续执行过滤链
chain.doFilter(request, response);
}
}
3.4.3 Spring Security6 配置(基于方法注解)
在 SecurityConfig 中关闭默认表单登录,启用 @EnableMethodSecurity,并配置无状态会话、放行登录/注册路径。
java
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final SecurityProperties securityProperties;
private final UserDetailsService userDetailsService;
private final StringRedisTemplate redisTemplate;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
protected SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
http.authorizeHttpRequests(requestMatcherRegistry -> requestMatcherRegistry
.requestMatchers(org.springframework.http.HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers(securityProperties.getIgnoreUrls().toArray(new String[0])).permitAll()
.anyRequest().authenticated()
);
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(configurer ->
configurer
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.exceptionHandling(customizer ->
customizer
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint));
http.addFilterBefore(new JwtValidationFilter(userDetailsService, securityProperties,redisTemplate), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
3.4.4 RBAC 实现
自定义权限,当注解上有 @ss.hasPermission(“xxx”)的时候,会执行这个xxx权限判断:
@Service("ss")
public class PermissionService {
public Boolean hasPermission(String... permissions) {
if (StringUtils.isEmpty(permissions)) {
return false;
}
// 获取当前用户的所有权限
List<String> perms = SecurityUtils.getUserDetails().getAuthorities().stream().map(GrantedAuthority::getAuthority).toList();
// 判断当前用户的所有权限是否包含接口上定义的权限
return perms.contains(CommonConstant.ALL_PERMISSION) || Arrays.stream(permissions).anyMatch(perms::contains);
}
}
-
数据库表:
sys_user(用户),sys_role(角色),sys_user_role(用户角色),sys_role_menu(角菜单色),sys_menu(菜单)。CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(64) DEFAULT NULL COMMENT '用户名', `password` varchar(64) DEFAULT NULL COMMENT '密码', `dept_id` bigint(20) DEFAULT NULL COMMENT '所属部门', `dept_name` varchar(255) DEFAULT NULL COMMENT '所属部门名称', `post_ids` json DEFAULT NULL COMMENT '岗位组', `icon` varchar(500) DEFAULT NULL COMMENT '头像', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `phone` varchar(11) DEFAULT NULL COMMENT '手机号', `nick_name` varchar(200) DEFAULT NULL COMMENT '昵称', `sex` varchar(1) DEFAULT NULL COMMENT '性别,0:男 1:女 2:未知', `note` varchar(500) DEFAULT NULL COMMENT '备注信息', `status` char(1) DEFAULT '1' COMMENT '启用状态,0:禁用 1:启用', `login_time` datetime DEFAULT NULL COMMENT '最后登录时间', `login_ip` varchar(255) DEFAULT NULL COMMENT '最后登陆IP', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `create_by` varchar(64) DEFAULT NULL COMMENT '创建人', `modify_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', `modify_by` varchar(64) DEFAULT NULL COMMENT '修改人', `valid` int(11) DEFAULT '1' COMMENT '有效状态,0:无效 1:有效', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户表';CREATE TABLE `sys_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID', `name` varchar(100) DEFAULT NULL COMMENT '名称', `role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符', `data_scope` char(1) DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 )', `description` varchar(500) DEFAULT NULL COMMENT '描述', `user_count` int(11) DEFAULT NULL COMMENT '后台用户数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `status` char(1) DEFAULT '1' COMMENT '启用状态,0:禁用 1:启用', `sort` int(11) DEFAULT '0' COMMENT '排序', `valid` int(11) DEFAULT '1' COMMENT '有效状态,0:无效 1:有效', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='角色表';CREATE TABLE `sys_role_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', `role_id` bigint(20) DEFAULT NULL COMMENT '角色ID', `menu_id` bigint(20) DEFAULT NULL COMMENT '菜单ID', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=213 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='角色和菜单关系表';CREATE TABLE `sys_menu` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', `pid` bigint(20) DEFAULT NULL COMMENT '父级菜单id', `name` varchar(100) DEFAULT NULL COMMENT '名称', `permission` varchar(200) DEFAULT NULL COMMENT '权限标识', `icon` varchar(500) DEFAULT NULL COMMENT '图标', `type` int(11) DEFAULT NULL COMMENT '权限类型,0:目录 1:菜单 2:按钮(接口绑定权限)3:外链', `uri` varchar(200) DEFAULT NULL COMMENT '组件路径', `sort` int(11) DEFAULT NULL COMMENT '排序', `keep_alive` tinyint(4) DEFAULT NULL COMMENT '【菜单】是否开启页面缓存(1-是 0-否)', `always_show` tinyint(4) DEFAULT NULL COMMENT '【目录】只有一个子路由是否始终显示(1-是 0-否)', `path` varchar(255) DEFAULT NULL COMMENT '路由地址', `params` json DEFAULT NULL COMMENT '【菜单】路由参数', `status` char(1) DEFAULT '1' COMMENT '显示状态,0:隐藏 1:显示', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `valid` int(11) DEFAULT '1' COMMENT '有效状态,0:无效 1:有效', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=1060 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用户权限表'; -
自定义UserDetailsServices实现loadUserByUsername方法:根据用户名加载用户及其角色、权限。
-
@Override public SysUserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 获取登录用户信息 SysUser user = sysUserMapper.selectByUsername(username); // 用户不存在 if (BeanUtil.isEmpty(user)) { throw new ApiException(SysExceptionEnum.USER_NOT_EXIST); } Long userId = user.getId(); // 用户停用 if (StatusEnums.DISABLE.getKey().equals(user.getStatus())) { throw new ApiException(SysExceptionEnum.USER_DISABLED); } // 获取角色 Set<String> roles = sysUserRoleMapper.listRoleKeyByUserId(userId); // 获取数据范围标识 Integer dataScope = sysUserRoleMapper.getMaximumDataScope(roles); Set<String> permissions = new HashSet<>(); // 如果 roles 包含 root 则拥有所有权限 if (roles.contains(CommonConstant.SUPER_ADMIN_ROOT)) { permissions.add(CommonConstant.ALL_PERMISSION); } else { // 获取菜单权限标识 permissions = sysMenuMapper.getMenuPermission(userId); // 过滤空字符串 permissions.remove(""); } return new SysUserDetails(user, permissions, roles, username, dataScope); }方法级权限控制:在Controller方法上使用@PreAuthorize("@ss.hasPermission('sys_log_page')") ,Spring Security6自动根据上下文中的权限列表进行判断。
-
如果不需要权限,就不加,表示放行,比如获取菜单列表:
private final SysMenuService sysMenuService; @Operation(summary = "菜单下拉列表") @GetMapping("/options") public R<?> listMenuOptions(@Parameter(description = "是否只查询父级菜单") @RequestParam(required = false, defaultValue = "false") boolean onlyParent) { List<Option> menus = sysMenuService.listMenuOptions(onlyParent); return R.success(menus); }获取用户列表:
-

@Operation(summary = "分页查询") @PreAuthorize("@ss.hasPermission('sys_user_page')") @GetMapping(value = "/page") public R<IPage<SysUser>> page(@ParameterObject Page<SysUser> page, @ParameterObject SysUser sysUser) { return R.success(sysUserService.getPage(page, sysUser)); }登录的时候,动态获取用户权限菜单列表:
@Operation(summary = "获取菜单路由信息") @GetMapping("/getRouters") public R<List<RouterVo>> getRouters() { List<SysMenu> menus = sysMenuService.getRouters(SecurityUtils.getUserId()); return R.success(sysMenuService.buildMenus(menus)); } @Override public List<SysMenu> getRouters(Long userId) { List<SysMenu> menus; if (SecurityUtils.isRoot()) { menus = list(new LambdaQueryWrapper<SysMenu>().in(SysMenu::getType, Arrays.asList( MenuTypeEnums.CATALOG.getValue(), MenuTypeEnums.MENU.getValue(), MenuTypeEnums.EX_LINK.getValue() ))); } else { menus = this.baseMapper.getRoutersByUserId(userId); } return getChildPerms(menus, 0); } public List<SysMenu> getChildPerms(List<SysMenu> list, int parentId) { List<SysMenu> returnList = new ArrayList<>(); for (SysMenu t : list) { // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点 if (t.getPid() == parentId) { recursionFn(list, t); returnList.add(t); } } return returnList; } <select id="getRoutersByUserId" resultType="com.iot.business.system.domain.SysMenu"> SELECT DISTINCT m.id, m.pid, m.NAME, uri, m.type, m.icon, m.sort, m.path, m.always_show, m.keep_alive, m.params FROM sys_menu m LEFT JOIN sys_role_menu rm ON m.id = rm.menu_id LEFT JOIN sys_user_role ur ON rm.role_id = ur.role_id WHERE ur.user_id = #{userId} AND m.type IN ( '0', '1' ,'3') AND m.valid = 1 ORDER BY m.pid, m.sort </select>
3.5 Vue3 前端分离
前端技术栈:Vue3 + Vue Router + Axios + Element Plus。
关键实现:
-
登录页调用
/auth/login接口,成功后将JWT存入localStorage。 -
Axios请求拦截器统一添加
Authorization: Bearer ${token}。 -
路由守卫(permission.js)根据用户角色动态生成可访问的路由表(前端路由权限),实现菜单级控制。
-
响应拦截器处理401 Token过期,自动跳转登录。
javascript
# 应用端口
VITE_APP_PORT = 3000
# 代理前缀
VITE_APP_BASE_API = '/iot-api'
# 网关地址 通过网关地址转发到http://127.0.0.1:9999 接口真实地址
VITE_APP_API_URL = http://127.0.0.1:8090
# 是否启用 Mock 服务
VITE_MOCK_DEV_SERVER = false

前端vue页面就不做详细j介绍了,Node ≥18 可以直接去官网看看
登录获取权限菜单:


4. 完整请求流程示例
-
用户在前端输入用户名密码,点击登录 → 请求经过Gateway,Sentinel限流检查通过 → 路由到User Service的
/auth/login。 -
User Service校验密码,生成JWT并返回。
-
前端保存JWT,之后每次请求携带该Token。
-
用户访问
/iot-api/sys/user接口 → Gateway限流 + 全局Token有效性验证(可选) → 路由到back Service。
-
back Service的JWT过滤器解析Token,构建包含用户角色/权限的Authentication。
-
Controller方法上标注 @PreAuthorize("@ss.hasPermission('sys_user_page')"),Spring Security6自动拦截校验权限,通过则执行业务逻辑,否则返回403。



5. 架构优势与总结
-
高可用与弹性:Eureka集群支持多实例注册,Gateway与Sentinel结合实现流量洪峰防护。
-
安全可靠:JWT + RBAC实现无状态认证与细粒度授权,符合前后端分离场景。
-
可观测性:Spring Boot Admin实时监控服务健康、内存、线程,便于运维排错。
-
解耦高效:前端只与Gateway通信,后端服务通过服务名调用,扩展性强。
本文介绍的架构已在多个生产项目中验证,能够支撑中等规模的微服务体系。开发者可根据实际业务需求调整限流策略、权限模型及监控告警规则,进一步集成链路追踪(如SkyWalking)、日志中心等组件,构建更为完善的云原生应用平台。
更多推荐




所有评论(0)