KnowFlow ③ 八股文:Spring Security + JWT 双令牌 + K8s 云原生部署

KnowFlow ③ 八股文:Spring Security + JWT + K8s

面向字节跳动 Java 后端暑期实习面试,从项目实现细节出发,每道题都结合代码讲清楚。


一、Spring Security 基础(5 道)

Q1:Spring Security 是做什么的?你们项目怎么配置的?

答:

Spring Security 是 Spring 的安全框架,负责认证(你是谁?)授权(你能做什么?)

项目中的配置SecurityConfig.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 禁用 CSRF(无状态 API 不需要)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/static/**").permitAll() // 静态资源放行
.requestMatchers("/api/v1/users/login", "/api/v1/users/register").permitAll() // 登录注册放行
.requestMatchers("/api/v1/upload/**").hasAnyRole("USER", "ADMIN") // 需要认证
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN") // 管理员专属
.anyRequest().authenticated() // 其他都要认证
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态,不创建 Session
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // 加入 JWT 过滤器
.addFilterAfter(orgTagAuthorizationFilter, JwtAuthenticationFilter.class); // 加入权限过滤器

return http.build();
}
}

面试回答话术

“我们项目是纯后端 API,不需要页面和 Session,所以用了 STATELESS 策略。认证通过 JWT Token 完成,每次请求在 Authorization 头里带上 Token,后端用过滤器解析和验证。”


Q2:为什么用 JWT 而不用 Session?两者的区别是什么?

答:

对比项 Session + Cookie JWT Token
状态 服务端保存 Session(有状态) 服务端不保存(无状态)
扩展性 多实例要做 Session 共享(Redis) 天然支持扩展,任意实例都能验证
性能 每次请求要查 Session 存储 只需要验签(CPU 计算),不用查存储
退出登录 删除服务端 Session 即可 要用 Redis 黑名单,或等 Token 过期
适用场景 传统 Web 应用(有页面) 前后端分离、微服务、移动端

为什么项目选 JWT?

  • 前端是 Vue 3,纯 API 调用,不需要 Session
  • 将来要部署多个实例(K8s),JWT 不用做 Session 共享
  • 配合 Redis 黑名单,能做到”主动登出”

Q3:SessionCreationPolicy.STATELESS 是什么意思?

答:

STATELESS 告诉 Spring Security 不要创建 HttpSession,每次请求都重新认证

1
2
3
4
5
6
7
8
9
10
有状态(STATEFUL):
1次请求 → 登录 → 创建 Session(服务端存用户信息)
2次请求 → 带上 SessionID(Cookie)→ 服务端查出用户信息 → 认证通过
...
问题:多实例部署时,实例 A 创建的 Session,实例 B 不认识

无状态(STATE_LESS):
每次请求 → 带上 JWT Token → 服务端验签 → 解析出用户信息 → 认证通过
...
优势:任意实例都能验证,天然支持水平扩展

项目中的配置SecurityConfig.java 第 74 行):

1
2
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

面试加分:无状态不代表”不保存任何数据”,项目里用 Redis 存了 Token 的黑名单刷新 Token 状态,这是为了支持”主动登出”和”刷新 Token”功能。


Q4:Spring Security 的过滤器链是什么?你们加了哪几个过滤器?

答:

Spring Security 是一个过滤器链(Filter Chain),请求依次经过多个过滤器,每个负责不同的安全功能。

1
2
3
4
5
6
7
请求 → 
[1] CsrfFilter(防 CSRF 攻击,我们禁用了)
[2] JwtAuthenticationFilter(解析 JWT Token,设置认证信息) ← 我们加的
[3] OrgTagAuthorizationFilter(检查组织标签权限) ← 我们加的
[4] UsernamePasswordAuthenticationFilter(表单登录,我们没用)
[5] AuthorizationFilter(最终授权判断)
→ 控制器方法

项目中的过滤器顺序SecurityConfig.java 第 77~79 行):

1
2
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(orgTagAuthorizationFilter, JwtAuthenticationFilter.class);

为什么这个顺序?

  1. 先解析 JWT(设置用户身份)
  2. 再检查组织标签权限(用户身份已经有了)

Q5:OncePerRequestFilter 是什么?为什么自定义过滤器要继承它?

答:

OncePerRequestFilter 保证每个请求只经过过滤器一次,防止重复执行。

为什么需要?

  • 请求可能经过多次转发(Forward)或包含(Include)
  • 如果不用 OncePerRequestFilter,过滤器逻辑可能被执行多次

项目中的使用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
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
// 1. 从请求头提取 JWT Token
String token = extractToken(request);

// 2. 验证 Token,设置用户认证信息
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.extractUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}

// 3. 继续执行过滤器链
filterChain.doFilter(request, response);
}
}

二、JWT 双令牌机制(6 道)

Q6:什么是”双令牌机制”?为什么不用单 Token?

答:

单 Token 的问题

  • Token 有效期设长了(比如 7 天)→ 泄露后风险大
  • Token 有效期设短了(比如 10 分钟)→ 用户要频繁登录

双令牌 = Access Token + Refresh Token

1
2
3
4
5
6
7
8
9
Access Token(访问令牌):
- 有效期短(1 小时)
- 每次 API 请求都带上
- 泄露后影响范围小

Refresh Token(刷新令牌):
- 有效期长(7 天)
- 只用来换新的 Access Token
- 存在 Redis,可以主动失效

项目中的实现JwtUtils.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
// Access Token(1 小时)
private static final long EXPIRATION_TIME = 3600000; // 1 hour

// Refresh Token(7 天)
private static final long REFRESH_TOKEN_EXPIRATION_TIME = 604800000; // 7 days

// 生成 Access Token
public String generateToken(String username) {
// ... 设置 claims,有效期 1 小时
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(getSigningKey())
.compact();
}

// 生成 Refresh Token
public String generateRefreshToken(String username) {
// ... 设置 claims(type = "refresh"),有效期 7 天
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION_TIME))
.signWith(getSigningKey())
.compact();
}

Q7:Token 刷新是怎么自动完成的?前端要做啥?

答:

后端自动刷新JwtAuthenticationFilter.java 第 49~53 行):

1
2
3
4
5
6
7
8
9
// 每次请求,检查 Access Token 是否快过期
if (jwtUtils.validateToken(token)) {
if (jwtUtils.shouldRefreshToken(token)) {
// 剩余时间 < 5 分钟,自动刷新
newToken = jwtUtils.refreshToken(token);
// 把新 Token 放进响应头
response.setHeader("New-Token", newToken);
}
}

前端要做的

1
2
3
4
5
6
7
8
// 每次请求后,检查响应头有没有 New-Token
axios.interceptors.response.use(response => {
const newToken = response.headers['new-token'];
if (newToken) {
localStorage.setItem('token', newToken); // 更新本地存储的 Token
}
return response;
});

面试加分:这叫**”无感知刷新”**,用户完全不知道 Token 被刷新了,体验极好。


Q8:Token 过期了但在宽限期内,怎么办?

答:

项目设计了宽限期(Grace Period)

1
2
3
4
5
6
7
8
Token 有效期:1 小时
宽限期:10 分钟

时间线:
0 min → Token 生成
60 min → Token 过期(正常)
60~70 min → 宽限期内,仍然允许刷新
> 70 min → 彻底过期,只能重新登录

代码实现JwtUtils.java):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static final long REFRESH_WINDOW = 600000;  // 10 分钟宽限期

public boolean canRefreshExpiredToken(String token) {
try {
Claims claims = extractClaimsIgnoreExpiration(token); // 忽略过期异常
long expirationTime = claims.getExpiration().getTime();
long currentTime = System.currentTimeMillis();
long expiredTime = currentTime - expirationTime;

// 过期时间在 10 分钟内,允许刷新
return expiredTime > 0 && expiredTime < REFRESH_WINDOW;
} catch (Exception e) {
return false;
}
}

面试回答话术

“我们设计了 10 分钟的宽限期。Token 过期后的 10 分钟内,用户仍然可以无感知刷新,不用重新登录。这大大提升了用户体验,特别是用户在填写长表单时 Token 过期的场景。”


Q9:JWT 的签名是怎么做的?密钥存在哪里?

答:

JWT 签名流程

1
2
3
4
5
6
7
8
9
Header(算法:HS256)

Payload(claims:username, role, userId...

用密钥(SecretKey)对上述内容进行签名

生成 Signature

最终 Token = Base64(Header) + "." + Base64(Payload) + "." + Base64(Signature)

项目中的密钥管理JwtUtils.java):

1
2
3
4
5
6
7
@Value("${jwt.secret-key}")  // 从配置文件读取(Base64 编码的密钥)
private String secretKeyBase64;

private SecretKey getSigningKey() {
byte[] keyBytes = Base64.getDecoder().decode(secretKeyBase64);
return Keys.hmacShaKeyFor(keyBytes); // 生成 HMAC-SHA256 密钥
}

密钥存在哪里安全?

方式 安全性 项目用的是?
配置文件(明文) ❌ 不安全 ❌ 不应该
配置文件(Base64) ⚠️ 可以接受(不是明文) ✅ 项目用的
环境变量 ✅ 较好 推荐生产环境
KMS(密钥管理服务) ✅✅ 最好 大厂推荐

面试加分:Base64 不是加密!只是编码。如果配置文件泄露,攻击者可以解码出原始密钥。生产环境应该用环境变量密钥管理服务(KMS)


Q10:JWT Token 泄露了怎么办?你们项目怎么防护?

答:

JWT 泄露的风险

  • JWT 一旦签发,在过期前都有效(因为服务端不保存状态)
  • 攻击者拿到 Token 可以直接冒充用户

项目中的防护措施

1. 双 Token 机制(减少泄露影响时间)

  • Access Token 只有 1 小时有效期

2. Redis 黑名单(支持主动失效)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// JwtUtils.java
public void invalidateToken(String token) {
String tokenId = extractTokenIdFromToken(token);
long expireTime = extractExpirationFromToken(token).getTime();
// 加入 Redis 黑名单
tokenCacheService.blacklistToken(tokenId, expireTime);
}

// JwtAuthenticationFilter.java
// 每次验证 Token 时,先检查黑名单
if (tokenCacheService.isTokenBlacklisted(tokenId)) {
// Token 已失效,拒绝请求
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}

3. 退出登录时清除 Refresh Token

1
2
3
4
5
6
// AuthController.java
@PostMapping("/logout")
public void logout(@RequestHeader("Authorization") String token) {
jwtUtils.invalidateToken(token); // Access Token 进黑名单
jwtUtils.invalidateRefreshToken(refreshToken); // Refresh Token 也进黑名单
}

Q11:为什么要把 tokenId 放进 JWT 里?

答:

tokenId 是 JWT 的唯一标识,用来实现主动失效(黑名单)。

1
2
3
4
5
6
7
8
9
如果没有 tokenId:
JWT 里只有 username、exp(过期时间)等信息
退出登录时,不知道要把哪条 JWT 加进黑名单
只能等过期,或者让用户清除 Cookie/LocalStorage(不可靠)

有了 tokenId:
JWT 里有唯一的 tokenId(比如 UUID)
退出登录时,把 tokenId 加进 Redis 黑名单
下次请求,先检查 tokenId 是否在黑名单里

项目中的实现JwtUtils.java):

1
2
3
4
5
6
7
8
9
10
11
12
// 生成 Token 时,加入唯一 tokenId
Map<String, Object> claims = new HashMap<>();
claims.put("tokenId", UUID.randomUUID().toString().replace("-", ""));
claims.put("userId", user.getId().toString());

String token = Jwts.builder()
.setClaims(claims)
...
.compact();

// 缓存 tokenId 到 Redis
tokenCacheService.cacheToken(tokenId, userId, expireTime);

三、RBAC 多租户权限(5 道)

Q12:什么是 RBAC?你们项目怎么实现的?

答:

RBAC(Role-Based Access Control)基于角色的访问控制

核心概念

  • 用户(User):系统的使用者
  • 角色(Role):权限的集合(比如 “ADMIN”、”USER”)
  • 权限(Permission):能做什么操作(比如 “doc:read”、”doc:write”)

项目中的简化 RBAC

1
2
3
4
5
用户 → 分配角色(USER 或 ADMIN

角色 → 决定能访问哪些 API

API → 用 @PreAuthorize 或 SecurityFilterChain 配置权限

项目中的角色判断SecurityConfig.java):

1
2
3
4
5
6
// USER 和 ADMIN 都能访问
.requestMatchers("/api/v1/upload/**", "/api/v1/search/**")
.hasAnyRole("USER", "ADMIN")

// 只有 ADMIN 能访问
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")

比 RBAC 更复杂的场景(项目中的 OrgTag):

  • 同一个角色(USER),但属于不同组织(OrgTag)
  • 用户只能访问自己组织的文档
  • 这是多租户(Multi-Tenancy) 的场景,RBAC 不够用,要用数据级权限

Q13:你们项目的”组织标签(OrgTag)”是什么?怎么实现数据隔离的?

答:

业务背景

  • 企业内网知识库,不同部门(组织)的文档要互相隔离
  • 比如”技术部”的文档,”市场部”的人不能看

实现方式

  1. 每个文档有一个 orgTag 字段(比如 “TECH”、”MARKETING”)
  2. 每个用户有一个或多个 orgTags(比如 [“TECH”, “MARKETING”])
  3. 搜索/访问文档时,强制过滤 orgTag

项目中的实现OrgTagAuthorizationFilter.java):

1
2
3
4
5
6
7
8
9
10
11
// 从 JWT Token 里提取用户的组织标签
String userOrgTags = jwtUtils.extractOrgTagsFromToken(token);

// 从请求里提取要访问的文档的 orgTag
String resourceOrgTag = extractResourceOrgTagFromRequest(request);

// 检查用户是否有权限访问这个 orgTag
if (!userOrgTags.contains(resourceOrgTag) && !isAdmin) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 禁止访问
return;
}

面试加分:这叫**”行级数据安全(Row-Level Security)”**,在数据库层面也能实现(比如 PostgreSQL 的 RLS),但在应用层实现更灵活。


Q14:如果一个用户属于多个组织,怎么处理?

答:

项目中的方案:JWT Token 里存一个 orgTags 数组(多个组织标签)。

1
2
3
4
用户 Alice:
- JWT 里的 orgTags = ["TECH", "MARKETING"]
- 能访问 TECH 的文档,也能访问 MARKETING 的文档
- 不能访问 HR 的文档

代码实现HybridSearchService.java):

1
2
3
4
5
6
7
8
9
// 从 JWT 里提取用户的所有组织标签
List<String> userOrgTags = jwtUtils.extractOrgTagsFromToken(token);

// Elasticsearch 查询时,过滤条件:文档的 orgTag 在用户的组织标签列表里
.bool(b -> b
.should(s1 -> s1.term(t -> t.field("orgTag").value(userOrgTags.get(0))))
.should(s2 -> s2.term(t -> t.field("orgTag").value(userOrgTags.get(1))))
...
)

更好的方案(如果组织很多):

  • 位图(Bitmap) 表示组织标签(每个组织一个 bit)
  • 用户属于哪几个组织,就把对应 bit 设为 1
  • 检查时用位运算,极快

Q15:管理员(ADMIN)能看所有文档吗?怎么实现的?

答:

项目中的实现:管理员绕过组织标签检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// OrgTagAuthorizationFilter.java
String role = jwtUtils.extractRoleFromToken(token);

if ("ADMIN".equals(role)) {
// 管理员,直接放行
filterChain.doFilter(request, response);
return;
}

// 普通用户,检查组织标签权限
if (!userOrgTags.contains(resourceOrgTag)) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
}

面试追问:”这样做有什么安全隐患?”

回答

“管理员的权限太大了,如果管理员账号被盗,所有文档都会泄露。更好的做法是给管理员也加审计日志(每次访问文档都记录),并且敏感操作需要二次认证(比如短信验证码)。”


Q16:你们项目的权限控制是在哪里做的?过滤器还是注解?

答:

两层权限控制

第一层:URL 级别(在 SecurityFilterChain 里配置)

1
2
// SecurityConfig.java
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN") // URL 级别拦截

第二层:数据级别(在 OrgTagAuthorizationFilter 里做)

1
2
3
4
5
6
7
// OrgTagAuthorizationFilter.java
// 检查用户是否有权限访问这个具体资源(文档、文件等)
String resourceOrgTag = extractResourceOrgTag(request);
if (!userOrgTags.contains(resourceOrgTag)) {
response.setStatus(403);
return;
}

为什么需要两层?

  • URL 级别:粗粒度(比如”/admin/**” 只有管理员能访问)
  • 数据级别:细粒度(比如”技术部的 Alice 只能访问技术部的文档”)

四、Kubernetes 部署(5 道)

Q17:你们项目怎么容器化的?Dockerfile 是怎么写的?

答:

多阶段构建(Multi-stage Build),减小镜像体积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 阶段 1:构建
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline # 下载依赖(利用 Docker 缓存)
COPY src ./src
RUN mvn package -DskipTests # 打包,生成 JAR

# 阶段 2:运行
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

为什么多阶段构建?

  • 阶段 1 有 Maven、JDK,镜像很大(> 500 MB)
  • 阶段 2 只有 JRE,镜像很小(< 200 MB)
  • 最终镜像只含阶段 2,推送和拉取都快

面试加分dependency:go-offline 这一步很重要!如果写在 COPY src 之后,每次改代码都要重新下载依赖(很慢)。写在前面可以利用 Docker 的层缓存


Q18:K8s 的 Deployment 和 Service 是什么?你们怎么配置的?

答:

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
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: knowflow-backend
spec:
replicas: 3 # 跑 3 个实例(Pod)
selector:
matchLabels:
app: knowflow
template:
metadata:
labels:
app: knowflow
spec:
containers:
- name: app
image: my-registry/knowflow:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"

Service:定义怎么访问这组 Pod(负载均衡)。

1
2
3
4
5
6
7
8
9
10
11
12
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: knowflow-service
spec:
selector:
app: knowflow # 选中上面 Deployment 创建的 Pod
ports:
- port: 80
targetPort: 8080
type: LoadBalancer # 暴露到外网(云厂商的负载均衡器)

Q19:HPA(Horizontal Pod Autoscaler)是什么?你们怎么配置的?

答:

HPA 自动调整 Pod 数量,根据 CPU 使用率或自定义指标(比如 QPS)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: knowflow-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: knowflow-backend
minReplicas: 3 # 最少 3 个 Pod
maxReplicas: 10 # 最多 10 个 Pod
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 超过 70%,扩容

扩容流程

1
2
3
4
5
1. 用户请求增多 → CPU 使用率涨到 85%
2. HPA 检测到 → 把 Deployment 的 replicas 从 3 改成 6
3. K8s 创建新的 Pod → 加入 Service 的负载均衡
4. 请求被分流 → CPU 使用率降到 60%
5. HPA 检测到 → 把 replicas 从 6 改回 3(缩容)

面试加分:HPA 默认每 15 秒采集一次指标。如果要按 QPS 扩容,要安装 KEDA(Kubernetes Event-driven Autoscaling)。


Q20:K8s 的滚动更新(Rolling Update)是什么?怎么保证不中断服务?

答:

滚动更新:先启动新版本的 Pod,等它健康检查通过后,再杀掉旧版本的 Pod。

1
2
3
4
5
6
7
# deployment.yaml 里的策略
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多多出 1 个 Pod
maxUnavailable: 0 # 更新期间,不能少于 replicas 的数量

更新流程(假设 replicas=3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
初始状态:Pod1(v1), Pod2(v1), Pod3(v1)

1 步:创建一个新 Pod
状态:Pod1(v1), Pod2(v1), Pod3(v1), Pod4(v2)【启动中...】

2 步:Pod4 健康检查通过
状态:Pod1(v1), Pod2(v1), Pod3(v1), Pod4(v2)【就绪】

3 步:删掉一个旧 Pod
状态:Pod2(v1), Pod3(v1), Pod4(v2)

4 步:再创建一个新 Pod
状态:Pod2(v1), Pod3(v1), Pod4(v2), Pod5(v2)【启动中...】

...重复直到所有 Pod 都是 v2

maxSurge=1, maxUnavailable=0 的效果

  • 更新期间,服务容量不减少(maxUnavailable=0)
  • 更新期间,可能会超出容量(maxSurge=1,最多多出 1 个 Pod)

Q21:你们项目的 CI/CD 是怎么做的?

答:

GitHub Actions 自动构建、推送镜像、部署到 K8s。

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
# .github/workflows/deploy.yml
name: Deploy to K8s

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

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Build Docker image
run: |
docker build -t my-registry/knowflow:${{ github.sha }} .

- name: Push to Registry
run: |
echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
docker push my-registry/knowflow:${{ github.sha }}

- name: Deploy to K8s
run: |
kubectl set image deployment/knowflow-backend app=my-registry/knowflow:${{ github.sha }}
kubectl rollout status deployment/knowflow-backend # 等待滚动更新完成

面试回答话术

“我们用 GitHub Actions 做 CI/CD。每次推送到 main 分支,自动构建 Docker 镜像、推送到镜像仓库、然后更新 K8s 的 Deployment。整个流程全自动,不用手动操作服务器。”


五、综合设计题(4 道)

Q22:如果让你从零设计多租户权限系统,你会怎么做?

答:

V1(当前项目):组织标签(OrgTag)+ 过滤器

  • 优点:简单,够用
  • 缺点:不能精细控制”用户 A 能访问文档 1,但不能访问文档 2”

V2(更精细):在 V1 基础上加入资源级权限

1
2
3
4
5
用户 → 角色 → 权限(能访问哪些资源)

资源权限表:
user_id | resource_type | resource_id | permission
123 | "document" | "abc123" | "read"

V3(最灵活):ABAC(Attribute-Based Access Control)

1
2
3
4
5
策略:"技术部的员工,在工作时间内,可以访问技术部的文档"
→ 用户属性(部门 = 技术部)
→ 环境属性(时间 = 工作时间)
→ 资源属性(文档的 orgTag = 技术部)
→ 动作(访问)

Q23:如果 JWT Secret 泄露了,你们系统会怎样?怎么应急?

答:

影响

  • 攻击者可以用这个 Secret 伪造任意用户的 JWT(包括管理员)
  • 现有所有 Token 都有效(因为签名能验证通过)

应急步骤

  1. 立即更换 Secret(在配置文件或环境变量里)
  2. 重启所有后端实例(让新 Secret 生效)
  3. 所有现有 Token 全部失效(因为新 Secret 验签旧 Token 会失败)
  4. 强制所有用户重新登录

更好的方案(提前预防):

  • Secret 存在 K8s Secret 里,不进代码仓库
  • 定期轮换 Secret(比如每 30 天)
  • KMS(密钥管理服务) 管理 Secret,支持自动轮换

Q24:如果 K8s 集群挂了,你们系统会怎样?怎么应对?

答:

影响

  • 所有 Pod 不可用 → API 全部 503
  • 如果 MinIO、Kafka、Elasticsearch 也跑在 K8s 里 → 整个系统瘫痪

应对方案

1. 多可用区部署(Multi-AZ)

1
2
K8s 集群分布在 3 个可用区(AZ1, AZ2, AZ3)
一个 AZ 挂了,其他两个 AZ 继续服务

2. 关键组件独立部署

1
2
3
MinIO、Kafka、Elasticsearch 不跑在 K8s 里
用云厂商的托管服务(比如阿里云 OSS、Kafka、Elasticsearch Service)
K8s 挂了,这些组件仍然可用(虽然 API 不可用,但数据不丢)

3. 备份与恢复

1
2
3
定时备份 K8s 的 YAML 配置(Git)
定时备份关键数据(MinIO、Elasticsearch 的快照)
K8s 集群挂了,可以在另一个云厂商快速重建

Q25:如果让你设计一个支持百万级用户的 K8s 集群,你会怎么做?

答:

控制平面(Control Plane)高可用

  • 3 个或 5 个 Master 节点(奇数,方便选举)
  • etcd 集群(K8s 的状态存储)3 节点或 5 节点

工作节点(Worker Node)规划

1
2
3
4
业务 Pod:50 个节点(每个节点跑 10~20 个 Pod)
MinIO:独立部署(不跑在 K8s 里)
Kafka:独立部署
Elasticsearch:独立部署(或 K8s StatefulSet + PVC)

网络

  • CNI 插件选 Calico(性能好)或 Flannel(简单)
  • Ingress Controller 用 NGINX IngressTraefik

监控

  • Prometheus + Grafana(监控 K8s 集群状态)
  • EFK(Elasticsearch + Fluentd + Kibana)收集日志

六、简历话术准备

面试官问:”你在简历里写了 Spring Security + JWT 双令牌机制,能详细讲一下吗?”

回答模板(背下来!):

“这个问题我从四个方面来讲。

第一,为什么用双令牌。单 Token 要么有效期太长(泄露后风险大),要么太短(用户要频繁登录)。双令牌把 Token 分成两种:Access Token(1 小时,每次请求都带)和 Refresh Token(7 天,只用来换新 Token)。这样即使 Access Token 泄露,影响也只限于 1 小时内。

第二,自动刷新是怎么做的。我们在 JWT 过滤器里加了逻辑:每次请求,检查 Access Token 的剩余时间,如果少于 5 分钟,就自动刷新,把新 Token 放进响应头。前端拦截响应,更新本地存储的 Token。用户完全无感知。

第三,Token 主动失效怎么做。JWT 天生不支持主动失效(因为服务端不保存状态)。我们在 Redis 里存了 Token 黑名单,退出登录时把 Token ID 加进黑名单。每次验证 Token 时,先查黑名单,如果在就拒绝请求。

第四,K8s 部署怎么做。我们用多阶段 Dockerfile 构建镜像(最终镜像 < 200 MB)。K8s 用 Deployment 跑 3 个实例,用 HPA 根据 CPU 使用率自动扩容(最多 10 个)。GitHub Actions 做 CI/CD,推送到 main 分支自动部署。”


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


KnowFlow ③ 八股文:Spring Security + JWT 双令牌 + K8s 云原生部署
https://whyalwaysme.lol/2026/06/08/2026-06-08-knowflow-security-qa/
作者
Cassiur
发布于
2026年6月8日
许可协议