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()) .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)) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .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);
|
为什么这个顺序?
- 先解析 JWT(设置用户身份)
- 再检查组织标签权限(用户身份已经有了)
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) { String token = extractToken(request); 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); } 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
| private static final long EXPIRATION_TIME = 3600000;
private static final long REFRESH_TOKEN_EXPIRATION_TIME = 604800000;
public String generateToken(String username) { return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(getSigningKey()) .compact(); }
public String generateRefreshToken(String username) { 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
| if (jwtUtils.validateToken(token)) { if (jwtUtils.shouldRefreshToken(token)) { newToken = jwtUtils.refreshToken(token); response.setHeader("New-Token", newToken); } }
|
前端要做的:
1 2 3 4 5 6 7 8
| axios.interceptors.response.use(response => { const newToken = response.headers['new-token']; if (newToken) { localStorage.setItem('token', newToken); } 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;
public boolean canRefreshExpiredToken(String token) { try { Claims claims = extractClaimsIgnoreExpiration(token); long expirationTime = claims.getExpiration().getTime(); long currentTime = System.currentTimeMillis(); long expiredTime = currentTime - expirationTime; 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}") private String secretKeyBase64;
private SecretKey getSigningKey() { byte[] keyBytes = Base64.getDecoder().decode(secretKeyBase64); return Keys.hmacShaKeyFor(keyBytes); }
|
密钥存在哪里安全?
| 方式 |
安全性 |
项目用的是? |
| 配置文件(明文) |
❌ 不安全 |
❌ 不应该 |
| 配置文件(Base64) |
⚠️ 可以接受(不是明文) |
✅ 项目用的 |
| 环境变量 |
✅ 较好 |
推荐生产环境 |
| KMS(密钥管理服务) |
✅✅ 最好 |
大厂推荐 |
面试加分:Base64 不是加密!只是编码。如果配置文件泄露,攻击者可以解码出原始密钥。生产环境应该用环境变量或密钥管理服务(KMS)。
Q10:JWT Token 泄露了怎么办?你们项目怎么防护?
答:
JWT 泄露的风险:
- JWT 一旦签发,在过期前都有效(因为服务端不保存状态)
- 攻击者拿到 Token 可以直接冒充用户
项目中的防护措施:
1. 双 Token 机制(减少泄露影响时间)
2. Redis 黑名单(支持主动失效)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public void invalidateToken(String token) { String tokenId = extractTokenIdFromToken(token); long expireTime = extractExpirationFromToken(token).getTime(); tokenCacheService.blacklistToken(tokenId, expireTime); }
if (tokenCacheService.isTokenBlacklisted(tokenId)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return; }
|
3. 退出登录时清除 Refresh Token
1 2 3 4 5 6
| @PostMapping("/logout") public void logout(@RequestHeader("Authorization") String token) { jwtUtils.invalidateToken(token); jwtUtils.invalidateRefreshToken(refreshToken); }
|
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
| 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();
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
| .requestMatchers("/api/v1/upload/**", "/api/v1/search/**") .hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
|
比 RBAC 更复杂的场景(项目中的 OrgTag):
- 同一个角色(USER),但属于不同组织(OrgTag)
- 用户只能访问自己组织的文档
- 这是多租户(Multi-Tenancy) 的场景,RBAC 不够用,要用数据级权限
Q13:你们项目的”组织标签(OrgTag)”是什么?怎么实现数据隔离的?
答:
业务背景:
- 企业内网知识库,不同部门(组织)的文档要互相隔离
- 比如”技术部”的文档,”市场部”的人不能看
实现方式:
- 每个文档有一个
orgTag 字段(比如 “TECH”、”MARKETING”)
- 每个用户有一个或多个
orgTags(比如 [“TECH”, “MARKETING”])
- 搜索/访问文档时,强制过滤 orgTag
项目中的实现(OrgTagAuthorizationFilter.java):
1 2 3 4 5 6 7 8 9 10 11
| String userOrgTags = jwtUtils.extractOrgTagsFromToken(token);
String resourceOrgTag = extractResourceOrgTagFromRequest(request);
if (!userOrgTags.contains(resourceOrgTag) && !isAdmin) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); 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
| List<String> userOrgTags = jwtUtils.extractOrgTagsFromToken(token);
.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
| 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
| .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
|
第二层:数据级别(在 OrgTagAuthorizationFilter 里做)
1 2 3 4 5 6 7
|
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
| FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests
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
| apiVersion: apps/v1 kind: Deployment metadata: name: knowflow-backend spec: replicas: 3 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
| apiVersion: v1 kind: Service metadata: name: knowflow-service spec: selector: app: knowflow 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
| apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: knowflow-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: knowflow-backend minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 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
| spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0
|
更新流程(假设 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
| name: Deploy to K8s
on: push: branches: [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 都有效(因为签名能验证通过)
应急步骤:
- 立即更换 Secret(在配置文件或环境变量里)
- 重启所有后端实例(让新 Secret 生效)
- 所有现有 Token 全部失效(因为新 Secret 验签旧 Token 会失败)
- 强制所有用户重新登录
更好的方案(提前预防):
- 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 Ingress 或 Traefik
监控:
- 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 面试手册 · 转载请注明出处