KnowFlow ④:JWT 双令牌 + RBAC 多租户 + K8s 云原生化部署(面试深挖)

KnowFlow ④:JWT 双令牌 + RBAC 多租户 + K8s 云原生化部署

项目:KnowFlow 企业级 RAG 知识库系统 | 整理时间:2026-06-07 | 适用于后端实习/校招面试


一、为什么需要”双令牌”机制?(面试必问)

1.1 单 Token 方案的痛点

1
2
3
4
5
6
7
用户登录 → 拿到一个 JWT Token(有效期 1 小时)

用户正在用系统干活... 第 59 分钟过去了

突然!Token 过期 → 所有请求 401 → 用户被踢出登录 😡

用户:??我刚才在写什么?!(体验极差)

这是单 Token 方案的最大问题:安全性(短过期)和用户体验(不想频繁登录)是矛盾的。

1.2 双令牌方案的核心思想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
登录时同时拿到两个 Token

┌─────────────────────────────────────────────┐
│ Access Token(访问令牌) │
│ - 有效期:1 小时 │
│ - 用途:每次请求带在 Header 里认证 │
│ - 特点:短过期,被盗走也用不了多久 │
├─────────────────────────────────────────────┤
│ Refresh Token(刷新令牌) │
│ - 有效期:7 天 │
│ - 用途:Access Token 过期时,拿来换新的 │
│ - 特点:只用来刷新,不参与日常请求 │
│ - 存储:存在 Redis,可以主动吊销 │
└─────────────────────────────────────────────┘

体验流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
用户登录 → 拿到 Access Token1h)+ Refresh Token7天)

每次请求带 Access Token

59 分钟,Access Token 快过期了

后端自动发一个新 Access Token 给前端(无感知刷新!)

用户毫无感觉,继续用

7 天内都在无感知刷新,不用重新登录

7 天后 Refresh Token 也过期了 → 才需要重新登录

二、源码深度解析:JwtUtils.java

2.1 Access Token 的生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public String generateToken(String username) {
// 1. 生成唯一 tokenId(存到 Redis,用于吊销)
String tokenId = generateTokenId(); // UUID 去横杠

// 2. 计算过期时间(1 小时)
long expireTime = System.currentTimeMillis() + 3600000;

// 3. 往 JWT 里塞自定义 claims
Map<String, Object> claims = new HashMap<>();
claims.put("tokenId", tokenId); // ← Redis 吊销用的 ID
claims.put("role", user.getRole()); // ← 用户角色(USER/ADMIN)
claims.put("userId", user.getId()); // ← 数据库 ID(不是 username!)
claims.put("orgTags", user.getOrgTags()); // ← 组织标签列表

// 4. 签名生成 JWT(HS256 算法)
String token = Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setExpiration(new Date(expireTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();

// 5. 缓存到 Redis(双重验证:Redis 里有效 + JWT 签名有效)
tokenCacheService.cacheToken(tokenId, userId, username, expireTime);
return token;
}

2.2 Refresh Token 的生成(独立的一套)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public String generateRefreshToken(String username) {
String refreshTokenId = generateTokenId();
long expireTime = System.currentTimeMillis() + 604800000; // 7 天

Map<String, Object> claims = new HashMap<>();
claims.put("refreshTokenId", refreshTokenId);
claims.put("type", "refresh"); // ← 标记为刷新令牌(验证时要检查!)
claims.put("userId", user.getId());

String refreshToken = Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setExpiration(new Date(expireTime))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();

// 独立缓存在 Redis
tokenCacheService.cacheRefreshToken(refreshTokenId, userId, null, expireTime);
return refreshToken;
}

💡 面试追问:”为什么 Access Token 和 Refresh Token 要分别缓存到 Redis?”
:为了主动吊销能力。JWT 本身是无状态的,发出去之后在过期前一直有效,无法主动让它失效。把 tokenId 存 Redis,吊销时把 tokenId 加入黑名单,下次请求验证时 Redis 里查不到就等于失效了。这是 JWT 无状态和吊销需求之间的经典折中方案。

2.3 无感知刷新机制(面试超级亮点!)

核心逻辑在 JwtAuthenticationFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
protected void doFilterInternal(HttpServletRequest request, 
HttpServletResponse response,
FilterChain filterChain) {

String token = extractToken(request); // 从 Header 取 Bearer Token

if (token != null) {
String newToken = null;
String username = null;

// ★ 情况 1:Token 还有效,但快过期了(剩余 < 5 分钟)
if (jwtUtils.validateToken(token)) {
if (jwtUtils.shouldRefreshToken(token)) {
// 自动刷新!生成新 Token
newToken = jwtUtils.refreshToken(token);
logger.info("Token auto-refreshed proactively");
}
username = jwtUtils.extractUsernameFromToken(token);
}
// ★ 情况 2:Token 已经过期,但在 10 分钟宽限期内
else {
if (jwtUtils.canRefreshExpiredToken(token)) {
newToken = jwtUtils.refreshToken(token);
logger.info("Expired token refreshed within grace period");
username = jwtUtils.extractUsernameFromToken(newToken);
}
}

// ★ 如果有新 Token,通过响应头返回给前端!
if (newToken != null) {
response.setHeader("New-Token", newToken); // ← 前端拦截这个 Header 替换本地存储
}

// 设置 Spring Security 上下文(后续 Controller 可以直接取用户信息)
if (username != null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
filterChain.doFilter(request, response);
}

前端的配合(简单描述):

1
2
3
4
5
6
7
8
9
10
// 前端 axios 拦截器
axios.interceptors.response.use(response => {
// 拦截响应头里的 New-Token
const newToken = response.headers['new-token'];
if (newToken) {
localStorage.setItem('accessToken', newToken); // 静默替换
console.log('Token 已自动刷新,用户无感知');
}
return response;
});

2.4 Token 吊销(退出登录时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 用户主动退出登录
public void invalidateToken(String token) {
String tokenId = extractTokenIdFromToken(token);
if (tokenId != null) {
Claims claims = extractClaimsIgnoreExpiration(token);
long expireTime = claims.getExpiration().getTime();

// ★ 加入 Redis 黑名单(在 Token 过期前都有效)
tokenCacheService.blacklistToken(tokenId, expireTime);
// ★ 从正常缓存里移除
tokenCacheService.removeToken(tokenId, userId);
}
}

// 验证时会检查黑名单
public boolean validateToken(String token) {
String tokenId = extractTokenIdFromToken(token);
// 先查 Redis 黑名单
if (!tokenCacheService.isTokenValid(tokenId)) {
return false; // ← 在黑名单里,直接无效
}
// 再验证 JWT 签名...
}

三、RBAC 多租户权限模型(KnowFlow 的业务核心)

3.1 传统 RBAC vs KnowFlow 的多租户 RBAC

1
2
3
4
5
6
7
传统 RBAC:
用户 → 角色 → 权限
zhangshan → ADMIN → 所有权限

KnowFlow 的多租户 RBAC:
用户 → 角色 + 组织标签 → 数据权限
zhangshan → USER + ["技术部-后端组"] → 能看:自己的 + 公开的 + 技术部及子组的所有文档

多了一个维度:orgTag(组织标签),控制数据级别的权限隔离。

3.2 组织标签的层级关系(源码解析)

1
2
3
4
5
6
7
8
组织架构(树形结构):

公司总部(ROOT)
├── 技术部(ORG-TECH)
│ ├── 后端组(ORG-TECH-BACKEND)
│ └── 前端组(ORG-TECH-FRONTEND)
└── 产品部(ORG-PRODUCT)
└── 体验组(ORG-PRODUCT-UX)

权限规则:上级能看到下级的数据

1
2
3
4
5
6
7
8
9
用户在 "后端组"ORG-TECH-BACKEND)

有效标签 = ["ORG-TECH-BACKEND", "ORG-TECH", "DEFAULT"]
↑ 自己 ↑ 父级 ↑ 默认所有人都有

能看到的文档:
- userId = 自己 的文档
- orgTag IN ["ORG-TECH-BACKEND", "ORG-TECH", "DEFAULT"] 的文档
- isPublic = true 的文档

源码:OrgTagCacheService.getUserEffectiveOrgTags()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public List<String> getUserEffectiveOrgTags(String username) {
// 1. 先查 Redis 缓存(24 小时有效)
String cacheKey = "user:effective_org_tags:" + username;
List<Object> cached = redisTemplate.opsForList().range(cacheKey, 0, -1);
if (cached != null && !cached.isEmpty()) {
return cached.stream().map(Object::toString).toList();
}

// 2. 缓存未命中,递归查找所有父标签
List<String> userTags = getUserOrgTags(username); // 直接拥有的标签
Set<String> allEffectiveTags = new HashSet<>(userTags);

for (String tagId : userTags) {
collectParentTags(tagId, allEffectiveTags); // 递归收集父标签
}

allEffectiveTags.add("DEFAULT"); // 默认标签永远有效

// 3. 写入缓存
redisTemplate.opsForList().rightPushAll(cacheKey, allEffectiveTags.toArray());
redisTemplate.expire(cacheKey, 24, TimeUnit.HOURS);

return new ArrayList<>(allEffectiveTags);
}

// 递归查找父标签
private void collectParentTags(String tagId, Set<String> result) {
OrganizationTag tag = organizationTagRepository.findByTagId(tagId).orElse(null);
if (tag != null && tag.getParentTag() != null && !tag.getParentTag().isEmpty()) {
String parentTagId = tag.getParentTag();
result.add(parentTagId);
collectParentTags(parentTagId, result); // 继续往上递归
}
}

3.3 Spring Security 配置中的权限控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SecurityConfig.java
http.authorizeHttpRequests(authorize -> authorize
// 公开接口(不需要登录)
.requestMatchers("/api/v1/users/register", "/api/v1/users/login").permitAll()
.requestMatchers("/api/v1/upload/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/search/**").hasAnyRole("USER", "ADMIN")

// ★ 管理员专属接口
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")

// ★ WebSocket 停止 Token 获取(允许匿名,因为这时候还没 Token)
.requestMatchers("/api/chat/websocket-token").permitAll()

// 其他所有请求都需要认证
.anyRequest().authenticated()
);

Controller 层再细粒度控制(文档删除权限):

1
2
3
4
5
// DocumentController.deleteDocument()
// 只有文件所有者 或 ADMIN 可以删除
if (!file.getUserId().equals(userId) && !"ADMIN".equals(role)) {
return ResponseEntity.status(403).body("没有权限删除此文档");
}

四、Kubernetes 云原生化部署(面试高频)

4.1 为什么要用 K8s?

不用 K8s(裸机部署) 用 K8s 部署
流量突增 → 手动加机器 → 配 Nginx → 半小时过去了 HPA 自动扩容,30 秒完成
发版 → 停服务 → 替换 Jar → 重启 → 用户感知到停机 Rolling Update,零停机发版
某台机器挂了 → 手动迁移 → 恢复需要 10 分钟 K8s 自动重启 Pod,30 秒自愈
配置文件散落在各台机器 → 改配置要挨个登录 ConfigMap + Secret,一处修改全局生效

4.2 多阶段 Dockerfile(减小镜像体积)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 阶段 1:构建(用 Maven 镜像编译)
FROM maven:3.8-openjdk-17 AS build
WORKDIR /app
COPY pom.xml .
COPY src ./src
# 编译打包(跳过测试加快速度)
RUN mvn clean package -DskipTests

# 阶段 2:运行(用精简的 JRE 镜像)
FROM openjdk:17-slim
WORKDIR /app

# 从构建阶段复制 Jar 包
COPY --from=build /app/target/smartpai-*.jar app.jar

# 非 root 用户运行(安全最佳实践)
RUN addgroup --system appgroup && adduser --system --group appuser
USER appuser

# 启动命令(时区、内存参数、优雅关闭)
ENTRYPOINT ["java", \
"-Dspring.profiles.active=prod", \
"-Duser.timezone=Asia/Shanghai", \
"-Xmx512m", \
"-XX:+UseG1GC", \
"-jar", "app.jar"]

镜像体积对比:

1
2
3
4
5
单阶段 Dockerfile(把整个 Maven 镜像打进去):
→ 镜像大小:~800 MB 😱

多阶段 Dockerfile(只保留运行时):
→ 镜像大小:~250 MB ✅(缩小 70%)

4.3 K8s Deployment 资源清单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: knowflow-app
namespace: prod
spec:
replicas: 2 # 初始 2 个副本(多副本高可用)
selector:
matchLabels:
app: knowflow
template:
metadata:
labels:
app: knowflow
spec:
containers:
- name: app
image: registry.example.com/knowflow:20240607-001
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JWT_SECRET_KEY
valueFrom:
secretKeyRef:
name: knowflow-secrets
key: jwt-secret
# ★ 健康检查(最重要!)
livenessProbe: # 存活探针:失败了 K8s 会重启 Pod
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 60 # 启动后 60 秒才开始检查
periodSeconds: 10 # 每 10 秒检查一次
readinessProbe: # 就绪探针:失败了 K8s 不会把流量打过来
httpGet:
path: /actuator/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 5
# ★ 资源限制(防止撑爆节点)
resources:
requests: # 调度时保证有这么多资源
cpu: "500m"
memory: "512Mi"
limits: # 最多用这么多,超了就被 OOMKill
cpu: "2000m"
memory: "1Gi"

4.4 HPA(Horizontal Pod Autoscaler)自动扩缩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: knowflow-hpa
namespace: prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: knowflow-app
minReplicas: 2 # 最少 2 个 Pod(保证高可用)
maxReplicas: 10 # 最多 10 个 Pod(防止无限扩容)
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # ★ CPU 超过 70% 就扩容
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80 # ★ 内存超过 80% 也扩容
behavior: # ★ 扩容/缩容行为策略
scaleDown:
stabilizationWindowSeconds: 300 # 缩容前观察 5 分钟,防止抖动
policies:
- type: Percent
value: 50 # 每次最多缩容 50%
periodSeconds: 60
scaleUp:
policies:
- type: Percent
value: 100 # 扩容可以很快,每次翻倍
periodSeconds: 30

HPA 的工作原理(面试常问):

1
2
3
4
5
6
7
8
9
K8s 每隔 15 秒查询一次 Pod 的 CPU/内存使用率

当前 5 个 Pod,平均 CPU = 85%(超过 70% 阈值)

需要副本数 = 5 × (85% / 70%) ≈ 6.07 → 向上取整 = 7

K8s 把 Deployment 的 replicas 改成 7

新的 Pod 启动(约 30 秒),流量被分摊,CPU 降下来

4.5 GitHub Actions 全链路 CI/CD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# .github/workflows/deploy.yml
name: KnowFlow CI/CD

on:
push:
branches: [main] # main 分支推送时触发

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

- name: Run tests
run: mvn test # ← 单测不过,不发版!

build-and-push-image:
needs: build-and-test # ← 单测通过才继续
runs-on: ubuntu-latest
steps:
- name: Build Docker image
run: docker build -t knowflow:${{ github.sha }} .

- name: Push to Registry
run: |
docker tag knowflow:${{ github.sha }} registry.example.com/knowflow:${{ github.sha }}
docker push registry.example.com/knowflow:${{ github.sha }}

deploy-to-k8s:
needs: build-and-push-image
runs-on: ubuntu-latest
steps:
- name: Deploy to K8s
run: |
kubectl set image deployment/knowflow-app \
app=registry.example.com/knowflow:${{ github.sha }}
kubectl rollout status deployment/knowflow-app
# ← 等待 Rolling Update 完成,超时自动回滚

💡 面试追问:”如果发版后发现问题,怎么快速回滚?”
:K8s 自带版本管理,kubectl rollout undo deployment/knowflow-app 一键回滚到上一个版本,30 秒内完成。GitHub Actions 里也可以配置 rollback job,发版后自动监控 Prometheus 告警,有告警就自动触发回滚。


五、面试八股文高频题

Q1:JWT 和 Session 两种认证方式比,优劣势是什么?

对比项 Session(服务端存储) JWT(无状态)
服务端状态 需要存 Session(Redis/内存) 无状态,不需要存
扩展性 多实例需要 Session 共享(Sticky Session 或 Redis) 天然支持多实例,任意实例可验证
吊销能力 ✅ 直接删 Session 就行 ❌ JWT 过期前无法吊销(需要借助 Redis 黑名单,KnowFlow 就是这么做的)
Token 大小 很小(只有一个 SessionID) 较大(包含 payload,通常 200~500 字节)

KnowFlow 用的是 JWT + Redis 黑名单 的混合方案,兼顾了无状态扩展性和吊销能力。

Q2:Refresh Token 被盗了怎么办?

:Refresh Token 也存 Redis,可以主动吊销(用户发现账号异常,在”设备管理”里踢出所有会话)。另外 Refresh Token 只用来换 Access Token,不参与日常请求,被盗走的危害比 Access Token 小。还可以给 Refresh Token 加 IP 绑定、设备指纹等辅助验证。

Q3:K8s 的 Service 和 Ingress 有什么区别?

:Service 是 K8s 内部服务发现(ClusterIP/NodePort/LoadBalancer),负责把流量转发到 Pod;Ingress 是七层(HTTP/HTTPS)入口,负责域名路由、HTTPS 终止、路径匹配(example.com/api/* → 转发到 backend-service)。生产环境通常是:用户 → Ingress(Nginx) → Service → Pod

Q4:HPA 扩容时,新的 Pod 还没就绪,流量打过来怎么办?

:这就是 readinessProbe(就绪探针)的作用。新 Pod 启动后,readinessProbe 检查失败,K8s Service 不会把流量转发给这个 Pod,只有探针成功了才加入负载均衡池。这样就避免了”启动中 Pod 接收流量”的问题。

Q5:RBAC 和 ABAC 比,KnowFlow 为什么选 RBAC?

:RBAC(基于角色的访问控制)适合组织结构清晰的场景,权限是”角色 → 资源”的映射,好管理。ABAC(基于属性的访问控制)更灵活(可以写”早上 9 点到下午 6 点才能访问”这类规则),但配置复杂,性能也差一些。KnowFlow 的权限模型是”角色 + 组织标签”,用 RBAC 做主体授权,用 orgTag 做数据级权限过滤,是 RBAC 的扩展,不是纯 ABAC。


六、简历上怎么讲这句话(可直接用)

原句参考:”对齐企业级生产标准,基于 Spring Security + JWT 双令牌机制设计细粒度 RBAC 多租户权限模型保障数据隔离;主导项目的云原生化部署,编写多阶段 Dockerfile 及 K8s 资源清单(Deployment/HPA),依托 GitHub Actions 实现全链路 CI/CD,保障系统在突发流量下的平滑自动扩缩容。”

面试时被问到,按这个顺序讲:

  1. 先说双令牌:Access Token 短过期保安全 + Refresh Token 长过期保体验,无感知刷新;
  2. 再说 RBAC 多租户:角色(USER/ADMIN)+ 组织标签层级(递归父标签),数据级隔离;
  3. 再说 K8s 部署:多阶段 Dockerfile(镜像 250MB),Deployment + HPA(CPU 70% 触发扩容),零停机 Rolling Update;
  4. 最后说 CI/CD:GitHub Actions 全链路(单测 → 构建镜像 → 部署 K8s → 自动回滚)。

© 2026 KnowFlow 面试手册 · 转载请注明出处


KnowFlow ④:JWT 双令牌 + RBAC 多租户 + K8s 云原生化部署(面试深挖)
https://whyalwaysme.lol/2026/06/07/2026-06-07-knowflow-security-deployment/
作者
Cassiur
发布于
2026年6月7日
许可协议