1. 面试背景与项目需求
那是一个普通的周三下午,我收到某互联网公司的全栈开发岗位面试邀请。职位描述里赫然写着"Java+Vue3技术栈",这正是我最近两年主攻的方向。作为有5年全栈经验的开发者,我本以为这会是一次常规的技术考察,没想到面试官抛出的实战题让我至今记忆犹新。
题目要求:在90分钟内,基于Spring Boot和Vue3实现一个带权限管理的待办事项系统。需要包含以下核心功能:
- 后端:JWT认证、RBAC权限模型、任务CRUD接口
- 前端:Vue3组合式API、动态路由、Element Plus组件库集成
- 联调:Axios请求封装、跨域处理、状态管理
这个看似简单的需求,实际上暗藏玄机。面试官特意强调要观察"从架构设计到具体实现的完整思维过程",而非仅仅完成功能。这让我意识到,现代全栈面试正在从八股文背诵转向真实场景的工程能力考察。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型背后的思考
2.1 为什么选择Spring Boot + Vue3组合
在当下的全栈开发领域,Java生态与前端框架的组合有多种选择。我最终确定Spring Boot + Vue3的方案,主要基于以下考量:
后端选型理由:
- 企业级需求匹配:Spring Security对JWT和RBAC的原生支持远超Node.js生态
- 性能基准:Java在处理复杂权限逻辑时,类型系统带来的稳定性优势明显
- 面试场景适配:相比Python/Django,Java更能体现全栈工程师的严谨性
前端选型考量:
- 组合式API优势:相比Vue2的Options API,更利于逻辑复用(面试加分项)
- TypeScript友好:与Java后端形成静态类型双保险
- 现代工程化:Vite构建速度是Webpack的10倍以上,适合限时开发
2.2 版本控制与依赖管理
实际开发中,我使用了以下具体版本(面试时被追问过):
bash复制# 后端
spring-boot-starter-parent: 3.1.0
jjwt-api: 0.11.5
# 前端
vue: 3.3.0
element-plus: 2.3.0
pinia: 2.1.0
这里有个关键细节:Spring Boot 3.x默认使用Jakarta EE 9+,与旧版javax包不兼容。我在初始化项目时差点踩坑,幸亏注意到控制台警告并及时调整了import语句。
3. 后端核心实现与坑点
3.1 JWT认证的完整实现链
认证模块是面试官重点考察的部分。我的实现包含以下关键步骤:
- SecurityConfig配置类:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.sessionManagement(sess -> sess.sessionCreationPolicy(STATELESS));
return http.build();
}
}
- JWT工具类中的关键方法:
java复制public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 30))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
踩坑实录:最初没设置JWT过期时间,被面试官当场指出安全隐患。后来补充了30分钟过期的逻辑,并解释了Refresh Token的优化方向。
3.2 RBAC权限模型的落地
权限控制部分采用了经典的五表结构:
sys_user用户表sys_role角色表sys_menu权限菜单表sys_user_role用户角色关联sys_role_menu角色权限关联
在数据查询时,我使用了JOIN+GROUP_CONCAT技巧一次性获取用户所有权限:
sql复制SELECT u.id, u.username,
GROUP_CONCAT(DISTINCT r.code) AS roles,
GROUP_CONCAT(DISTINCT m.perms) AS perms
FROM sys_user u
LEFT JOIN sys_user_role ur ON u.id = ur.user_id
LEFT JOIN sys_role r ON ur.role_id = r.id
LEFT JOIN sys_role_menu rm ON r.id = rm.role_id
LEFT JOIN sys_menu m ON rm.menu_id = m.id
WHERE u.username = ?
GROUP BY u.id
面试官特别提示:实际生产环境要考虑缓存权限数据,避免每次请求都查库。我补充说明了用Redis缓存权限信息的方案。
4. 前端架构设计与技巧
4.1 动态路由的智能处理
前端项目采用基于角色权限的动态路由方案,核心逻辑在permission.js中:
javascript复制// 过滤异步路由
function filterAsyncRoutes(routes, roles) {
return routes.filter(route => {
if (route.meta?.roles) {
return roles.some(role => route.meta.roles.includes(role))
} else {
return true
}
})
}
// 动态添加路由
router.beforeEach(async (to, from, next) => {
if (store.getters.routes.length === 0) {
const { roles } = await store.dispatch('user/getInfo')
const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
accessRoutes.forEach(route => router.addRoute(route))
next({ ...to, replace: true })
} else {
next()
}
})
性能优化点:在真实项目中,应该使用路由懒加载拆分代码块。我在面试中展示了如何用defineAsyncComponent实现:
javascript复制const UserManage = defineAsyncComponent(() => import('@/views/system/user'))
4.2 Pinia状态管理实践
相比Vuex,我选择Pinia作为状态管理工具,主要因为它:
- 更简单的API设计
- 完美的TypeScript支持
- 组合式API风格统一
用户模块的store典型实现:
typescript复制export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const roles = ref<string[]>([])
const login = async (params: LoginParams) => {
const { data } = await api.login(params)
token.value = data.token
roles.value = data.roles
}
return { token, roles, login }
})
类型安全技巧:我为所有API接口定义了TS类型:
typescript复制interface LoginParams {
username: string
password: string
}
interface UserInfo {
roles: string[]
perms: string[]
}
5. 联调过程中的关键问题
5.1 跨域解决方案对比
开发时遇到经典的CORS问题,我向面试官展示了三种解决方案:
- 后端配置(生产推荐):
java复制@Bean
CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
- 前端代理(开发环境):
javascript复制// vite.config.js
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
- Nginx配置(部署方案):
nginx复制location /api {
proxy_pass http://backend;
add_header 'Access-Control-Allow-Origin' '*';
}
5.2 Axios的工程化封装
我实现了具备以下特性的请求封装:
- 自动携带Token
- 统一错误处理
- 请求取消
- 响应数据类型转换
核心代码片段:
typescript复制const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 10000
})
// 请求拦截
service.interceptors.request.use(config => {
if (store.token) {
config.headers.Authorization = `Bearer ${store.token}`
}
return config
})
// 响应拦截
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
showError(res.message)
return Promise.reject(new Error(res.message))
}
return res.data
},
error => {
if (error.response.status === 401) {
router.push('/login')
}
return Promise.reject(error)
}
)
6. 面试官的深度追问
在演示完基础功能后,面试官提出了几个进阶问题:
-
如何防止JWT被篡改?
- 答:除了签名验证,还应将JWT存储到HttpOnly的Cookie中,并设置SameSite属性。对于敏感操作需要二次验证。
-
Vue3的响应式原理与React有何不同?
- 答:Vue3使用Proxy实现细粒度响应,React依赖虚拟DOM diff。Vue的依赖收集是自动的,React需要手动声明依赖。
-
RBAC与ABAC的区别?
- 答:RBAC基于角色静态分配,ABAC(属性基访问控制)可以动态评估用户属性、环境因素等。我们系统后期可以引入ABAC增强灵活性。
-
如果任务量增加10倍,架构如何调整?
- 答:前端采用虚拟滚动优化长列表,后端引入Elasticsearch实现高效检索,数据库考虑读写分离。
7. 复盘与经验总结
这次面试让我对全栈开发有了新的认识:
-
技术深度比广度更重要:虽然题目涉及面广,但面试官更关注每个技术点的实现质量而非简单堆砌功能。
-
架构思维是核心竞争力:从数据库设计到前端状态管理,需要建立完整的系统观。我展示了如何用DDD思想划分模块边界,获得加分。
-
性能与安全缺一不可:在快速开发的同时,要时刻考虑并发问题和安全防护。我主动提到的XSS防御方案(前端DOMPurify + 后端Jackson转义)让面试官印象深刻。
-
TypeScript是必备技能:严格类型定义不仅减少运行时错误,更能体现工程师的严谨性。我的前后端类型共享方案(通过openapi-generator)得到了特别认可。
最后给准备全栈面试的同学建议:多关注主流开源项目的实现方式,比如Spring Security的过滤器链设计、Vue Router的导航守卫机制等。真正的技术实力,往往藏在细节之中。
