1. 为什么“Spring Boot + 微服务安全”成了Java面试的必考组合
最近后台收到不少朋友私信,问的都是同一个问题:Java面试到底怎么准备才能不虚?尤其是现在招聘JD里几乎必写Spring Boot、微服务和一堆安全关键词。我自己经历过几轮从初级到中高级的面试,也和做过面试官的朋友反复聊过招人标准,今天干脆把这条线上的实战经验整理出来,从Spring Boot的启动原理讲到微服务安全的认证授权链路,把高频考点、底层原理和真实踩坑一次性说透。
先说个现象:我帮朋友做模拟面试时,发现很多人简历里写着“熟悉Spring Boot”“了解微服务”,可一追问“Spring Boot为什么能自动配置”“网关里的Token到底怎么校验”“服务间调用怎么保证安全”,多数人就卡住了。这说明工具用得很熟,原理讲不透,是普遍存在的一个短板。
我的感觉是,现在的Java面试早就不该靠“背八股文”过关了。面试官真正想考察的是三件事:第一,你能不能把一个Spring Boot项目从零搭起来,并且说清楚它的启动过程;第二,你有没有真正处理过微服务环境下的安全问题,比如认证、授权、密钥管理、数据权限;第三,你在遇到“接口被重复调用”“Token被人伪造”“数据不一致”这类真实问题时,是只会搜答案,还是能给出成体系的设计。
1.1 面试官在筛选什么:不只是会写CRUD
我和几个做技术负责人的朋友聊招人标准,共识出奇一致:初级岗位看基础,中高级岗位看“能不能兜住线上问题”。一个只会写Controller、Service、Mapper的候选人,简历上写三年经验,面试官其实十来分钟就能试出来。怎么试?抛一个场景:“用户在下单接口连续点了两次,你怎么防止重复扣款?”这个问题看似简单,背后串起了幂等设计、分布式锁、事务边界、接口安全性好几块知识。能拆开讲细的人,基本不会太差。
所以面试准备的核心,不是把某个框架的文档背熟,而是建立“一个问题能串起多个知识点”的思维。Spring Boot和微服务安全恰好是这种思维的完美载体:Spring Boot的自动配置能串起IoC容器、Bean生命周期、条件注解;微服务安全能串起网关、JWT、OAuth2、数据权限、分布式事务。这也是为什么现在越来越多面试题从这两个方向出。
1.2 从热搜词看当前市场的真实需求
我整理后台搜索热词的时候发现,和Java相关的搜索里,“spring boot”“java面试题”“java八股文”出现频率极高,其次是“java怎么保证数据一致性”“spring boot 集成 web socket yml 配置”“行级权限java”“java对象深度拷贝”这类非常具体的问题。这反映出求职者两个焦虑:一是不知道面试到底会问多深,二是不知道从哪块开始补。
从需求侧看,中小型公司在用Spring Boot搭建服务,稍微有规模的公司都在往微服务拆,安全、数据一致性、性能优化就成了躲不开的话题。对应到面试题上,就是“你项目里怎么做登录鉴权”“网关层做过什么安全策略”“订单状态怎么保持一致”。这些题没有标准答案,但能体现系统设计能力。所以我这篇不打算罗列面试题,而是把从Spring Boot到微服务安全这条线上的高频考点拆开,配合实际场景和踩坑经验来讲,帮你把零散的知识串成网。
2. Spring Boot面试高频考点:从启动原理到配置陷阱
Spring Boot是Java面试绕不开的第一关。很多候选人能熟练创建项目、写接口,但被问到“自动配置如何生效”“@SpringBootApplication里有哪些注解”“为什么改了yml不生效”就露馅了。这一章我挑几个最容易被追问的考点来拆。
2.1 starter与自动配置:别只背“约定大于配置”
先说结论:Spring Boot的自动配置可以拆成三段来理解——条件注解、自动配置文件、自定义starter。这三段式是回答的核心骨架。
@SpringBootApplication是一个组合注解,包含@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。其中最关键的是@EnableAutoConfiguration,它会通过@Import导入AutoConfigurationImportSelector,这个选择器会去读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(旧版本是spring.factories)里声明的自动配置类。
但自动配置类不是全都生效,每个配置类上几乎都有一堆条件注解,比如@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty。这就像“按需加载”:classpath里有了DataSource相关的类,才去配置DataSource;容器里没有用户自定义的ObjectMapper,才注入一个默认的。很多框架的starter就是这么搭起来的。
如果面试官继续追问“你想自己写一个starter,怎么设计”,可以从这几步回答:在META-INF目录下放自动配置文件,定义一个@Configuration配置类,用@ConditionalOnClass和@ConditionalOnMissingBean控制加载时机,最后把自定义属性类绑定到spring.xxx配置前缀。最好还能补一句“注意命名,官方starter叫spring-boot-starter-xxx,自定义的一般叫xxx-spring-boot-starter,避免和官方规则混淆”。
这里有个特别容易踩的坑:条件注解的评估顺序和@AutoConfigureBefore/@AutoConfigureAfter。如果两个starter存在依赖关系,但没有声明先后顺序,可能出现“这个配置类都执行完了,才发现另一个依赖没准备好”的情况。我在项目里就遇到过集成第三方短信SDK时,它的自动配置总是先于自定义的Redis配置执行,导致初始化时拿不到RedisTemplate。解决办法是在自己的自动配置类上标注@AutoConfigureAfter(RedisAutoConfiguration.class),并配合@ConditionalOnBean(RedisTemplate.class)做兜底。
2.2 YAML配置与WebSocket集成:细节最容易翻车
热搜词里有“spring boot 集成web socket yml 配置”,这也是我面试时喜欢问的点,因为它非常能检验一个人有没有真的配过。
先说YAML。很多新人觉得yml只是比properties好看,实际上坑不少。比如字符串里的冒号和井号会被当成特殊字符,端口配置写成port: 8080:80会直接解析报错;比如多环境配置用spring.profiles.active激活application-dev.yml,但不同环境下配置文件里的公共部分被反复复制,导致改一处忘一处;更隐蔽的是数字类型解析,版本号1.0会被读成double,用@ConfigurationProperties绑定到String字段时会出问题,必须加引号。这里强烈建议配置项集中放到一个XxxProperties类里,用@ConfigurationProperties(prefix = "xxx")绑定,而不是在代码里到处用@Value,改配置时能少掉一半头发。
再聊WebSocket集成。Spring Boot里走WebSocket的路子大体有两条:一是直接用Spring WebSocket模块,基于STOMP协议做消息代理,适合聊天、通知类场景;二是用Netty自己实现长连接服务,适合游戏、设备通信等对吞吐和协议定制要求高的场景。面试常见问题是“WebSocket握手阶段怎么做鉴权”。
我当时在一个物联网项目里就踩过这个坑:客户端连接时拿一个Token放在URL参数里,在WebSocketHandler的beforeHandshake里校验Token,通过才允许握手。这个方案能跑,但有两个隐患:Token出现在URL里,会被网关日志和浏览器历史记录留下,属于安全隐患;另外握手之后,后续消息没法再校验Token是否过期。
后来调整成“握手时用Token换一个短期会话ID,后续消息带会话ID,由后端维护会话与用户的关系”,同时给会话加心跳和过期时间。这个方案在面试里讲出来很加分,因为它体现了“安全不是一步到位,而是分层设计”。
2.3 Bean注入与生命周期:说出容器层面的理解
“说说Spring的Bean生命周期”在一线岗位面试里几乎必问。如果只回答“实例化、属性填充、初始化、销毁”,只能算及格。面试官通常会追加几个问题,我列一下:
- 构造器、@Autowired、initMethod的执行顺序是怎样的?
- 如果出现循环依赖,Spring为什么能解决setter注入却解决不了构造器注入?
- 你在项目里用BeanPostProcessor做过什么?
逐个说。构造器注入在实例化阶段就会触发,属性填充阶段处理@Autowired和@Value,之后才是各种Aware回调、@PostConstruct、InitializingBean.afterPropertiesSet,以及BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization。注意@PostConstruct和BeanPostProcessor的执行顺序,这跟最终生成的是代理对象还是原始对象有关,特别容易被追问。
关于循环依赖,Spring用三级缓存解决:一级缓存存成品Bean,二级缓存存早期暴露的Bean,三级缓存存ObjectFactory工厂。为什么是三级不是两级?因为需要处理“Bean被代理增强”的情况:如果只用两级缓存,早期暴露的是原始对象,后面AOP代理生成后,容器里就出现了两个不同实例。三级缓存的目的,是在真正需要注入时通过ObjectFactory提前生成代理或者原始对象,保证整个容器中始终只有一个最终实例。这个解释能甩开一大半候选人。
至于BeanPostProcessor,很多框架扩展都靠它,比如MyBatis的MapperScannerConfigurer、Spring Security的过滤器链配置。面试时如果能说一句“我用BeanPostProcessor做过统一日志增强或者动态代理注入”,会显得确实深入用过容器,而不是只看过八股文。
3. 微服务安全:从“配JWT”到“讲透认证授权链路”
如果说Spring Boot考点还能靠背八股文糊弄,微服务安全真的是“会就会,不会就露馅”。因为安全问题的答案高度依赖场景,没有标准答案。
3.1 为什么JWT不能解决全部安全问题
我见过太多人把“项目用了JWT”当成安全亮点,但一问“JWT的密钥放在哪里”“Token被窃取了怎么处理”“refresh token为什么需要存Redis”,就开始含糊。不能说JWT没有价值,但要清醒:JWT解决的是“服务端无状态校验”的问题,不是“安全”的全部。
JWT的典型模型是:用户登录成功后,服务端生成access token(短期)和refresh token(长期),access token携带用户ID、角色权限等信息,后续请求带上它,网关或服务端验签即可。优点是天然适合微服务,服务之间不用回调认证中心;缺点是Token一旦签发,在过期前基本无法撤销,除非引入黑名单机制。
所以在微服务架构里,常见做法是“JWT + Redis黑名单/白名单”:每次请求到网关时,先验签,再查一下Redis里的Token状态。用户登出、改密或管理员封号时,把Token塞进黑名单,并设置过期时间。这本质是在无状态协议上叠加了一层可控状态,等于用有状态服务换回了“可撤销”能力。
面试时可以把反过来用:很多安全漏洞不是因为JWT本身弱,而是因为把用户角色、部门这些敏感信息全塞进Token,还忘了限制签名算法。有个经典漏洞是把算法字段改成none,服务端如果没限制算法,攻击者就能伪造任意Token。所以服务端必须显式指定验签算法,不能信任Token里的alg字段。
3.2 网关、令牌与密钥管理:安全链路的完整拼图
微服务安全从来不是某一个服务的事情,而是一条链路:客户端 -> 网关 -> 业务服务。网关做统一鉴权,业务服务做细粒度授权,认证中心负责发Token和管理密钥。
先说网关层。在Spring Cloud Gateway里,我写过GlobalFilter,处理逻辑大概是:
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().toString(); // 白名单放行,例如 /auth/login、/auth/captcha if (whiteList.contains(path)) { return chain.filter(exchange); } String token = resolveToken(request); if (StringUtils.isEmpty(token) || !jwtUtil.validateToken(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 解析出用户ID和权限,放入header ServerHttpRequest mutated = request.mutate() .header("X-User-Id", jwtUtil.getUserId(token)) .header("X-User-Roles", String.join(",", jwtUtil.getRoles(token))) .build(); return chain.filter(exchange.mutate().request(mutated).build()); }业务服务从header里取用户身份,而不是自己去解析Token,有两个好处:一是解析逻辑收敛在网关,下游服务不需要每处都做验签;二是以后更换鉴权方案不用改所有业务服务。
很多面试题会问“为什么不直接在业务服务里解析JWT”,回答要点在于“关注点分离”。安全策略应该统一收口,否则每个服务各自实现,很可能有的校验有的不校验。而且网关层做鉴权,还可以顺带做限流、审计日志、防重放攻击,这些都是安全链路的组成部分。
密钥管理是更进阶的话题。千万不要把对称密钥写在application.yml里,也不要提交到代码仓库。我见过同事把jwt.secret写在项目配置里,结果一次代码仓库泄露,整个线上Token都可以被伪造,只能强制让所有用户重新登录。规范做法是:对称密钥或私钥放到配置中心或KMS里,部署环境用环境变量注入,定期轮换;微服务之间内部调用,可以用mTLS或签名头做服务身份认证,而不是裸奔的内网调用。
行级权限也是热搜里的高频词。很多人知道RBAC(角色权限),但行级权限问的是“同一个部门的两个人,能看到的订单范围为什么不同”。这涉及数据权限模型:在SQL层根据当前用户所属部门和数据归属做过滤。MyBatis里可以用拦截器改写SQL,在框架层面搞定多租户和数据隔离。这是微服务安全很重要的维度:除了“你能不能访问这个接口”,还要管“你能看到哪几行数据”。
3.3 微服务场景下的数据一致性:安全之外的硬骨头
在微服务里,安全和高可用经常一起被考察。比如下单和扣库存不在同一个服务,怎么保证两边不会一个成功一个失败?这就是数据一致性问题,也是“java怎么保证数据一致性”热搜背后的核心。
最朴素的做法是强一致性事务。本地多表可以用@Transactional,但跨服务就无能为力。分布式事务里常聊的2PC能解决部分场景,但性能差、协调者本身又成单点。现在业界更倾向于最终一致性方案。
我实际项目里用过两种:本地消息表和Seata的AT模式。本地消息表的思路是:在本地事务里先写业务数据,同时写一条“待发送消息”记录,事务提交后,用定时任务把消息发到MQ,下游消费成功后回调确认,失败则重试。这个方案实现简单、没有额外中间件依赖,但需要业务方保证消费者的幂等性。
Seata AT模式则是框架帮助处理,思路是对业务SQL做undo_log记录,事务提交时TM协调各分支,分支通过TC统一管理。它牺牲了部分性能,但写代码的方式接近本地事务,团队接受度高。面试时不用把细节背得特别全,但至少要能画出TM、TC、RM三个角色的关系,以及分布式事务的提交/回滚流程。要让面试官觉得你真的用过,而不是只会名词。
4. 面试中那些“看起来简单”的连环追问
面试里有一种很磨人的问法:先问一个简单知识点,然后一步步往底层戳,直到你答不上来。这种“连环追问”最能暴露基础是否扎实,我把常见几组拿出来拆一下。
4.1 AQS与并发工具:从 synchronized 到 Lock 的原理层
“Java里保证线程安全有哪些方式?”初级选手会说synchronized、Lock、volatile。面试官通常会接一句:“ReentrantLock的内部原理是什么?”这就到了AQS。
AQS全称AbstractQueuedSynchronizer,核心就是“状态变量state + CLH变体队列”。每个ReentrantLock都有一个state,0表示未锁定,大于0表示被持有且重入次数。获取锁时用CAS去改state,成功就拿到锁;失败则把当前线程包装成Node放进等待队列,然后LockSupport.park阻塞。释放锁时减去state,唤醒队列头部等待线程。
很多人只记住AQS的类名,但回答时如果能说清“公平锁和非公平锁实现差异”会更出彩。非公平锁在lock时先直接尝试一次CAS,抢不到才进队列;公平锁则严格检查队列里有没有前驱节点,有就先排队。这个差异正好解释“为什么非公平锁吞吐量更高,但可能出现线程饥饿”。
另外可以顺便准备“synchronized和ReentrantLock的区别”:synchronized是JVM层面的监视器锁,ReentrantLock是API层面;后者支持超时、可中断、多条件变量。假如继续被问到底层,可以提synchronized在JDK 6之后有偏向锁、轻量级锁、重量级锁的升级过程,以及偏向锁后来被逐渐废弃的趋势。
如果面试官继续往深问“volatile为什么不能保证原子性”,可以用这个例子:“一个long型字段在32位JVM上可能被分两次写入”,volatile只保证可见性和一定的有序性,不保证复合操作的原子性。把这几个并发关键字放在一起对比,能体现知识密度。
4.2 对象拷贝、Lombok 与编译器报错:一道奇怪的实操题
第二个高频追问是:“Java里对象拷贝是深拷贝还是浅拷贝?”“@Data注解生成的equals为什么可能出问题?”这类题看起来基础,其实考察的是你有没有被坑过。
先说浅拷贝和深拷贝。默认的Object.clone()是浅拷贝,对象里的引用字段会指向同一个对象。如果对象里只有基本类型和String,浅拷贝问题不大;只要有List、Map、自定义对象,浅拷贝就会埋雷。
深拷贝的常用姿势有三种:重写clone方法并手动复制引用字段;通过序列化和反序列化(需要实现Serializable);使用第三方库如Apache Commons Lang的SerializationUtils。序列化方式的坑是性能差,而且类里如果有不可序列化字段会报错。我的实用建议是:优先设计成不可变对象,实在需要拷贝就用统一的序列化工具封装,别在业务代码里到处重写clone。
Lombok的问题更贴近日常。@Data会生成equals/hashCode,如果有继承关系,且父类和子类都生成equals,很容易出现“子类对象和父类对象equals成立,但hashCode不一致”的问题;而且Lombok生成的equals没有考虑父类字段。另一个经典报错是Lombok和JDK/编译器的兼容性问题:升级JDK后编译直接爆红,“You aren't using a compiler supported by lombok, so lombok will not work”。这种问题通常是因为IDE内置的编译器版本太新,或者Lombok版本太旧。解决办法很直接:升级lombok到支持该JDK的版本,或者在maven-compiler-plugin里设置fork=true并指定可用的编译路径。如果面试里能讲出这个实际报错场景,印象分会直接拉满。
4.3 数据一致性与分布式锁:业务安全的另一面
这组追问喜欢从具体场景切入:“两个服务同时扣一笔余额,怎么保证不超扣?”答案里必然要有分布式锁。
我自己的经验是,分布式锁要解决三个问题:互斥、防死锁、防误删。Redis实现锁时,用命令SET key value NX EX 10,只有key存在时才设置成功,实现互斥;key设置过期时间,避免客户端挂掉后锁永远不释放;释放锁时要注意“先比较value再删除”,否则可能删掉别人刚获取的锁。很多资深面试官会追问“为什么不用SETNX+EXPIRE两条命令”,因为两步操作做不到原子性,中间宕机锁就会永久存在。
再往上进阶,项目里一般直接使用Redisson的RLock,它通过watchdog自动续期,避免业务执行时间超过锁过期时间导致锁提前释放。但即便有了Redisson,也不能把分布式锁当万能的:锁粗粒度太大会拖垮性能,锁粒度太小又有并发安全问题。比如扣库存,不能锁整个商品,最好锁一个库存扣减单元,这个平衡只能结合业务场景谈。
5. 手写与调优题:考官真正想看的底层能力
一线岗位的现场面试,总会留几分钟让候选人手写代码。很多候选人以为就是考算法,其实考官更想通过写代码看你的编码习惯、边界处理意识和基础知识。
5.1 排序算法:从冒泡到快排,手写时的隐藏考点
“手写一个冒泡排序”几乎是最常见的送分题,但能全对的人不多。我见过有人把内层循环边界写成j < arr.length直接越界,有人忘了加交换标志写出了最坏情况的纯冒泡。
正确写法应该这样:
public static void bubbleSort(int[] arr) { if (arr == null || arr.length < 2) { return; } boolean swapped; for (int i = 0; i < arr.length - 1; i++) { swapped = false; for (int j = 0; j < arr.length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; swapped = true; } } if (!swapped) { break; } } }检查点主要包括:是否判空、内层循环边界是否逐步缩小、是否有提前退出。再往后,面试官会让你写快排或归并。快排的核心是partition函数,但很多人会忽略随机选取pivot的重要性。如果每次固定取最后一个元素,遇到接近有序的数组,复杂度会退化成O(n^2)。我在面试中会主动说“这里我会用随机pivot来避免最坏情况”,然后简单解释稳定性问题:快排不稳定,归并稳定,Arrays.sort()对对象用TimSort、对基本类型用双轴快排,因为稳定性对对象排序很重要。这些都是加分的细节。
5.2 深度拷贝与行级权限:贴近业务的代码设计
有些手写题不是算法题,而是让你做“类设计”。比如:“设计一个用户订单查询接口,要求超级管理员能看全部,普通用户只能看自己的,同部门组长能看本部门的。”这就是行级权限的代码实现。
我的设计思路是:查询接口接收当前用户上下文,里面包含userId、roleId、departmentId和权限标识。在SQL层,根据权限标识动态拼接过滤条件:admin不加where条件;user加where user_id = ?;department加where department_id = ?。如果用的是MyBatis,可以把过滤条件封装成DataPermission注解加拦截器,由拦截器统一在SQL末尾追加条件。
面试官如果继续追问“怎么防止日志打印把用户列表全量查出来”,我会回答:在持久层之前先做权限校验,返回值做最小化裁剪,日志里只输出用户ID而不是全对象。这种贴近业务的回答,比单纯手写一个函数更能体现经验。
至于深度拷贝,手写时可以用类似下面的方式反复确认边界:
public static <T extends Serializable> T deepCopy(T obj) { try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(obj); oos.flush(); ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray())); return (T) ois.readObject(); } catch (Exception e) { throw new RuntimeException("deep copy failed", e); } }重点不是这段代码多优雅,而是能不能说清楚“为什么这个方案要求Serializable”“transient字段会怎样”“性能开销在哪里”。面试官想听的往往是这些取舍。
5.3 Java基础冷知识:DNS获取、静态链接与容器
热词里有几个比较冷门的基础问题,比如“java获取dns”“java是静态链接的”“java容器”。如果出现在面试里,往往是用来考察“你是不是天天只CRUD,不关心底层”。
先说获取DNS。Java里可以用InetAddress.getByName(domain)获取IP,但这个操作背后用的是操作系统或JVM配置的DNS解析,每次调用都可能触发网络请求,性能很差。线上高并发时不要频繁裸调InetAddress,要做缓存,或者用支持缓存的解析库,比如dnsjava。在微服务里,服务发现、注册中心、外部域名访问都依赖DNS,缓存策略直接关系到接口延迟。
至于“Java是静态链接的”?答案是Java默认是动态链接的,类文件在运行时由类加载器按需加载。这带来灵活性和动态更新能力,但也意味着部署环境必须有一致的依赖。打包时用Shade插件可以把依赖打成一个fat jar,实现“当前目录下有Java环境就能直接java -jar跑起来”,这在容器化部署里非常实用。
“java容器”这个词有歧义:可能指Servlet容器如Tomcat,也可能指JVM在Docker容器里运行。面试时可以先反问面试官指哪种,再分别作答。Tomcat是Web应用服务器,可以理解为“运行Servlet/JSP的宿主环境”;而容器化部署通常讨论的是“JVM能否感知到Docker容器的CPU和内存限制”。JDK 10之后默认开启UseContainerSupport,JVM会自动识别容器资源限制,不会因为宿主机内存大就把堆设得过大导致容器被杀。这个问题我遇到过,答不上来会很尴尬。
6. 实战复盘:一次模拟面试的完整问答与拆解
理论讲了那么多,可能还有人不知道怎么落地。这一章我直接把一次模拟面试的完整对话还原出来,加上点评。先说清楚:这不是背答案,而是给你参考“听到问题后怎么组织思路”。
6.1 从自我介绍到项目追问:别让项目变成流水账
我问过不少候选人,自我介绍都是“我做了三年Java,用过Spring Boot和微服务,负责过某某系统”。这种介绍面试官基本记不住。比较好的方式是:先一句话定位,比如“我主要做交易系统后端,核心场景是高并发下单和支付回调一致性”,再抛一个亮点,比如“其中一个主要工作是解决分布式锁和幂等,避免用户重复支付”,把节奏引向自己擅长的领域。
模拟问答场景:
面试官:你项目里怎么做登录认证?
候选人:用户登录后,网关签发JWT,后续请求在网关层统一解析,把用户ID传给下游。需要撤销的Token会放到Redis黑名单。
面试官:那用户Token泄露了怎么办?
一个合格的回答方向是:第一是缩短access token有效期;第二是refresh token设置设备指纹和绑定信息,检测到异地设备就强制重新登录;第三是提供主动注销接口,把Token加入黑名单,并让客户端清除本地存储。如果能再说一句“不要用localStorage,要用HttpOnly Cookie降低XSS窃取风险”,会非常加分。
6.2 安全架构问题的追问:为什么要这么设计
在一次模拟面试里,我专门连环追问了安全设计,这里摘出几个有代表性的问题。
问题一:网关做了JWT解析,业务服务怎么信任这个身份?
标准回答是:网关在解析之后,把用户身份写进Header,同时用内部签名头(比如HMAC)对Header内容做签名,业务服务校验签名后才使用Header信息。否则,任何客户端都可以直接伪造X-User-ID。
问题二:如何防止恶意用户重复调用接口?
先分场景。如果是正常操作按钮连点,前端要置灰,后端要做幂等;如果是脚本刷接口,网关层要做限流,比如Spring Cloud Gateway里基于Redis的RequestRateLimiter按用户维度限流;如果涉及资金操作,还要做转账金额风控。把场景和方案对应起来讲,才不会显得背题。
问题三:服务A调用服务B,B怎么知道A是谁?
这已经进入服务间认证。一般方案是内部服务都处在可信环境里,通过mTLS保证连接安全,再配合签名的内部Token标识调用者。被问到“为什么不用JWT做服务间认证”时,可以回答:JWT容易泄露,服务间调用频率高,验签成本和密钥分发成本都不如专用方案可控。
6.3 面试后的复盘清单
模拟面试结束后,我通常会让候选人填一个复盘清单,这里直接分享出来:
- 哪些问题当场答得顺畅?是真理解,还是碰巧背过?
- 哪些问题答一半卡住了?卡住原因是概念不清、场景没接触过,还是没把知识串起来?
- 哪些问题面试官说完答案后,自己仍然没底?把关键词记下来,回去翻源码。
- 有没有在口述中暴露出“用过但没搞懂”的技术?比如用了Redisson却说不出自动续期原理,这是需要立刻补的。
这样复盘一次,比盲目刷十套题有效。
7. 我的面试准备心得与踩坑记录
最后聊点实在的。这篇既然叫“实战”,我不能只给考点,不给方法。
7.1 刷题之外的三个关键动作
第一是读官方文档和源码。至少要读Spring Boot和Spring Security的关键文档,不用全背,但要能说出大概流程。遇到不懂的类,按快捷键跳进源码里看一眼注释,是最省力的记忆方式。
第二是每学一个概念,必须写一个最小可运行Demo验证。比如AQS原理,光看文章印象不深,自己写一个多线程累加的场景,配合断点看等待队列的变化,记忆会非常牢固。技术点不怕深,怕的就是“自以为懂了”。
第三是整理自己的面试数据库。我不太推荐直接背别人的面试题集,而是每次被问倒之后,把问题、当时的回答、正确思路、参考源码位置记成一张表格,时间久了就是一本属于自己的八股文手册。热词里的“java八股文”就是这个意思,关键是怎么把八股变成自己的知识。
7.2 一些真实踩过的坑
讲两个真实故事。
第一个是版本幻觉。我之前项目里用的是Spring Boot 2.3.x,面试时聊Cache、WebSocket配置都按老版本回答,偏偏面试官问的是新版2.6.x的变化,结果路径配置、自动配置文件加载方式全变了,直接说错。Spring Boot版本迭代很快,大版本之间很多机制都变了,面试前一定要确认自己介绍的经验对应的版本。
第二个是“会用”和“讲清”的差距。我曾经在项目里大量用Redisson,但从来没细想过watchdog为什么能让锁不过期。被追问后才知道,Redisson会在锁被占用但业务没结束时,每三分之一过期时间自动续期。这个机制解决了超时误删问题,也带来一个新问题:如果业务长时间不结束,锁一直不被释放,需要自行设置合理leaseTime。这些细节如果不经过复盘总结,面试时就会变成“我用了但说不出为什么”。
之前还有一次,我自以为很懂Spring Security,结果被问到“多个过滤器链的执行顺序到底是怎样的”,才意识到自己只停留在配置层面。后来把源码里FilterChainProxy和SecurityFilterChain的关系翻了一遍,才算真正明白。这类问题很容易成为面试分水岭,能讲清楚的人确实不多。
越是在考察面越来越宽的行情下,那些亲手踩过坑、复盘过答案的人反而越有优势。这次分享就先到这里。说句实在话:下次面试被问倒之后,别再只搜答案,当场打开IDE把报错复现一遍,这个坑就再也坑不到你了。