Vue3 + Spring Boot 实现 Face ID 人脸识别登录 —— 从密码到双因素认证的完整实践
前言
在管理后台系统中,传统的账号密码登录方式存在着密码泄露、暴力破解等安全隐患。随着 WebRTC 和云端 AI 技术的发展,浏览器端人脸识别已经变得非常成熟且易于集成。本文将详细介绍如何基于 Vue 3 + Spring Boot 3 + 百度云人脸识别 API 实现一套完整的 密码 + Face ID 双因素认证系统。
最终效果:管理员输入账号密码后,必须通过摄像头进行人脸验证才能进入系统;首次登录的管理员会自动进入人脸注册流程。
一、技术选型
| 层级 | 技术 | 说明 |
|---|---|---|
| 前端框架 | Vue 3 (Composition API) | <script setup> + Vite 5 |
| UI 组件库 | Element Plus | 表单、对话框、消息提示 |
| 摄像头采集 | getUserMedia API | 浏览器原生 WebRTC,无需插件 |
| 后端框架 | Spring Boot 3.2 | Java 17,内嵌 Tomcat |
| ORM | MyBatis-Plus 3.5 | Lambda 查询,零 XML |
| 人脸识别 | 百度云 Face API V3 | 活体检测 + 1:1 人脸搜索 |
| 认证鉴权 | JWT (JJWT 0.12) | HMAC-SHA256,支持 pre_auth 角色 |
| 密码加密 | BCrypt | Spring Security Crypto |
二、系统架构

整个系统的数据流向非常清晰:
- 浏览器摄像头 —— 通过
navigator.mediaDevices.getUserMedia()获取视频流,利用 Canvas 截取帧并转为 Base64 - Vue3 前端 ——
FaceCapture.vue组件负责摄像头交互,login/index.vue编排两步登录流程 - Spring Boot 后端 —— 两步端点:
/api/admin/login(密码验证)和/api/admin/login/face(人脸验证/注册) - 百度云人脸 API —— 执行 1:1 人脸搜索和活体检测,人脸特征数据完全存储在云端
- MySQL —— 仅存储
face_enabled状态标记和注册时间,不存储人脸数据
安全层设计
- TLS 1.3 全链路加密传输
- BCrypt 对密码做单向哈希
- JWT 双角色设计:
pre_auth(5分钟短效,仅用于人脸验证步骤)+admin(24小时正式 Token) - 百度云 OAuth 2.0 获取 Access Token 后调用人脸 API
三、双因素登录流程

步骤 1:密码验证 → 获取预授权 Token
用户输入账号密码 → 前端 POST /api/admin/login
→ 后端 BCrypt 验证密码
→ 签发 Pre-Auth Token (role: "pre_auth", 有效期 5 分钟)
→ 返回 needRegister 标记(首次登录的管理员需要注册人脸)
关键设计点:密码验证通过后不直接签发正式 JWT,而是签发一个短效的预授权 Token。这个 Token 的 role 是 "pre_auth",仅能用于第二步的人脸验证,无法访问其他 API。
步骤 2:Face ID 人脸验证 / 注册 → 获取正式 JWT
前端弹出摄像头 → FaceCapture 组件倒计时 3-2-1 → 自动采集人脸帧
→ 前端 POST /api/admin/login/face { preAuthToken, imageBase64 }
→ 后端验证 pre_auth token → 查询 faceEnabled 状态
├── 已注册 → 百度云 searchFace() 1:1 比对 → score ≥ 80 通过
└── 未注册 → 百度云 registerFace() 注册人脸 → faceEnabled = 1
→ 签发正式 JWT Token (role: "admin", 有效期 24h)
→ 前端存储 localStorage → 路由守卫放行 → Dashboard
为什么需要预授权 Token?
如果没有预授权机制,攻击者可以绕过密码直接调用人脸验证接口。预授权 Token 将密码验证和人脸验证绑定在一起,确保两步认证的连续性和安全性。
四、前端核心实现
4.1 登录页面 —— 两步流程编排
登录页面 login/index.vue 是整个流程的指挥中心,通过 step 状态变量切换两个步骤:
<template>
<!-- 步骤1:账号密码 -->
<template v-if="step === 1">
<el-form :model="form" :rules="rules">
<el-input v-model="form.username" placeholder="管理员账号" />
<el-input v-model="form.password" type="password" placeholder="密码" />
<el-button :loading="loading" @click="handleLogin">登 录</el-button>
</el-form>
</template>
<!-- 步骤2:Face ID 验证 -->
<template v-else>
<FaceCapture
ref="captureRef"
:mode="needRegister ? 'register' : 'login'"
@image-ready="onImageReady"
@cancel="step = 1"
/>
<el-button @click="step = 1">返回密码登录</el-button>
</template>
</template>
密码验证通过后,自动切换到 Face ID 步骤:
const handleLogin = async () => {
const res = await adminApi.login(form)
if (res.code === 200) {
preAuthToken.value = res.data.token // 预授权 token
needRegister.value = res.data.needRegister // 是否需要注册
step.value = 2 // 切换到 Face ID 步骤
}
}
const onImageReady = async (imageBase64) => {
captureRef.value?.showVerifying()
const res = await adminApi.loginFace({ preAuthToken: preAuthToken.value, imageBase64 })
if (res.code === 200) {
localStorage.setItem('token', res.data.token) // 存储正式 JWT
router.push('/dashboard')
} else {
captureRef.value?.showFail(res.message) // 显示失败动画
}
}
4.2 FaceCapture 组件 —— Face ID 风格取景框

FaceCapture.vue 是一个 377 行的独立组件,模拟了 Apple Face ID 的视觉效果:
- 圆形取景框 + 深色背景 + 绿色强调色 (
#10A37F) - 旋转环动画 —— 外圈 SVG 圆环持续旋转
- 人脸轮廓 —— SVG Path 绘制面部引导线
- 扫描线动画 —— 渐变光束上下扫描
- 3-2-1 倒计时 —— 环形进度条 + 大数字
- 成功/失败动画 —— 绿色对勾/红色抖动 X
组件的状态机设计:
loading → ready → scanner → countdown → captured → verifying → (success | failed)
// 摄像头初始化
async function init() {
stream = await startCamera() // getUserMedia
videoRef.value.srcObject = stream
status.value = 'ready' // 等待用户点击"开始采集"
}
// 自动采集流程
function startScanning() {
status.value = 'scanner' // 扫描动画
setTimeout(() => startCountdown(), 1500)
}
function startCountdown() {
status.value = 'countdown'
countdown.value = 3
timer = setInterval(() => {
countdown.value--
if (countdown.value <= 0) capture() // 倒计时结束,自动拍照
}, 1000)
}
function capture() {
const base64 = captureFrame(videoRef.value) // Canvas 截帧
status.value = 'captured'
emit('image-ready', base64) // 通知父组件发送请求
}
4.3 摄像头工具模块
utils/faceRecognition.js 提供了三个核心函数:
// 打开摄像头(前置摄像头,分辨率 1280x720)
export async function startCamera(facingMode = 'user') {
return navigator.mediaDevices.getUserMedia({
video: { facingMode, width: { ideal: 1280 }, height: { ideal: 720 } },
audio: false
})
}
// 从 video 元素截取帧 → 镜像翻转 → Base64 JPEG
export function captureFrame(video) {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const ctx = canvas.getContext('2d')
ctx.translate(canvas.width, 0)
ctx.scale(-1, 1) // 镜像翻转(自拍模式)
ctx.drawImage(video, 0, 0)
return canvas.toDataURL('image/jpeg', 0.92).split(',')[1] // 纯 Base64
}
// 停止所有摄像头轨道
export function stopCamera(stream) {
stream?.getTracks().forEach(t => t.stop())
}
五、后端核心实现
5.1 JWT 双角色设计
@Component
public class JwtUtil {
// 正式管理 Token —— 密码 + 人脸都通过后签发
public String generateToken(Long adminId, String username) {
return Jwts.builder()
.subject(String.valueOf(adminId))
.claim("username", username)
.claim("role", "admin") // ← 正式管理员角色
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 86_400_000)) // 24h
.signWith(secretKey)
.compact();
}
// 预授权 Token —— 密码验证通过后签发,仅用于人脸验证步骤
public String generatePreAuthToken(Long adminId, String username) {
return Jwts.builder()
.subject(String.valueOf(adminId))
.claim("username", username)
.claim("role", "pre_auth") // ← 预授权角色
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 300_000)) // 5min
.signWith(secretKey)
.compact();
}
}
5.2 两步登录服务实现
@Service
public class AdminServiceImpl implements AdminService {
// 第一步:密码验证 → 预授权 Token
public Result<AdminLoginVO> login(LoginDTO dto) {
Admin admin = adminMapper.selectOne(
new LambdaQueryWrapper<Admin>()
.eq(Admin::getUsername, dto.getUsername()));
if (admin == null || !BCrypt.checkpw(dto.getPassword(), admin.getPassword()))
return Result.error("账号或密码错误");
if (admin.getStatus() == 0)
return Result.error("该账号已被禁用");
// 签发预授权 Token(非正式 JWT)
String preAuthToken = jwtUtil.generatePreAuthToken(admin.getId(), admin.getUsername());
boolean needRegister = admin.getFaceEnabled() == null || admin.getFaceEnabled() != 1;
AdminLoginVO vo = new AdminLoginVO();
vo.setToken(preAuthToken);
vo.setNeedRegister(needRegister); // 前端据此选择验证/注册模式
return Result.success(vo);
}
// 第二步:人脸验证/注册 → 正式 JWT
public Result<AdminLoginVO> loginWithFace(LoginFaceDTO dto) {
// 验证预授权 Token
if (!jwtUtil.validateToken(dto.getPreAuthToken()))
return Result.error("预授权token已过期,请重新登录");
String role = jwtUtil.getRoleFromToken(dto.getPreAuthToken());
if (!"pre_auth".equals(role))
return Result.error("无效的预授权token");
Long adminId = jwtUtil.getAdminIdFromToken(dto.getPreAuthToken());
Admin admin = adminMapper.selectById(adminId);
boolean needRegister = admin.getFaceEnabled() == null || admin.getFaceEnabled() != 1;
if (needRegister) {
// 首次使用:注册人脸
baiduFaceService.registerFace(admin.getUsername(), dto.getImageBase64());
admin.setFaceEnabled(1);
admin.setFaceRegisteredAt(LocalDateTime.now());
adminMapper.updateById(admin);
} else {
// 已注册:1:1 人脸比对
BaiduFaceService.MatchResult result =
baiduFaceService.searchFace(admin.getUsername(), dto.getImageBase64());
if (result == null)
return Result.error("人脸验证失败,请确保是本人且光线充足");
if (result.score() < 80)
return Result.error("人脸相似度不足(" + result.score() + "分),请重试");
}
// 签发正式 JWT
String token = jwtUtil.generateToken(admin.getId(), admin.getUsername());
// ...
return Result.success(token);
}
}
5.3 百度云人脸 API 集成
@Service
public class BaiduFaceService {
@Value("${baidu.face.api-key}")
private String apiKey;
@Value("${baidu.face.secret-key}")
private String secretKey;
@Value("${baidu.face.group-id}")
private String groupId;
// 获取 OAuth 2.0 Access Token(带缓存)
private String getAccessToken() { /* client_credentials 流程 */ }
// 注册人脸到百度云用户组
public String registerFace(String userId, String imageBase64) {
// POST https://aip.baidubce.com/rest/2.0/face/v3/faceset/user/add
// 参数: group_id, user_id, image, image_type=BASE64,
// quality_control=NORMAL, liveness_control=NORMAL
}
// 1:1 人脸搜索(带活体检测)
public MatchResult searchFace(String userId, String imageBase64) {
// POST https://aip.baidubce.com/rest/2.0/face/v3/search
// 参数: group_id_list, user_id, image, image_type=BASE64,
// match_threshold=80, liveness_control=NORMAL
}
public record MatchResult(String userId, double score) {}
}
5.4 拦截器白名单配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/admin/login", // 第一步:密码验证
"/api/admin/login/face", // 第二步:人脸验证
"/api/ai-chat/**",
"/api/ai-conversation/member/**"
);
}
}
六、API 接口速查
| 端点 | Method | 认证 | 说明 |
|---|---|---|---|
/api/admin/login |
POST | 无 | 密码验证 → 返回 Pre-Auth Token |
/api/admin/login/face |
POST | Pre-Auth | 人脸验证/注册 → 返回正式 JWT |
/api/admin/face/register |
POST | JWT | 登录后单独注册人脸 |
/api/admin/face/status |
GET | JWT | 查询人脸注册状态 |
/api/admin/face |
DELETE | JWT | 删除人脸数据 |
/api/admin/password |
PUT | JWT | 修改管理员密码 |
七、配置文件
在百度智能云上获取App-ID、Api-Key和Secret-key按照以下操作:

之后创建应用:

点击立即创建即可,再回到应用列表界面查看需要调用的字段即可
之后在application.yml 中百度云人脸配置:
baidu:
face:
app-id: "你的AppID"
api-key: "你的APIKey"
secret-key: "你的SecretKey"
group-id: "admin_group" # 人脸用户组
match-threshold: 80 # 匹配阈值 (0-100)
liveness: "NORMAL" # 活体检测等级
数据库迁移脚本(migration_face_login.sql):
ALTER TABLE `admin`
ADD COLUMN `face_enabled` TINYINT NOT NULL DEFAULT 0,
ADD COLUMN `face_registered_at` DATETIME DEFAULT NULL;
注意:人脸数据存储在百度云端,本地数据库只记录启用状态和时间戳。
八、前端 API 层
// admin/src/api/index.js
export const adminApi = {
login: (data) => request({ url: '/admin/login', method: 'post', data }),
loginFace: (data) => request({ url: '/admin/login/face', method: 'post', data }),
// ... 其他接口
}
九、写在最后
通过本文的实现,我们将管理后台的登录安全从单一密码验证提升到了密码 + 生物特征的双因素认证。整个方案的核心亮点:
- 安全性 —— 预授权 Token 机制将两步认证强制绑定,防止绕过密码直接人脸登录
- 用户体验 —— Apple Face ID 风格的取景框动画,3-2-1 倒计时自动采集,无需手动拍照
- 架构清晰 —— 前端 Vue3 组件化、后端 Spring Boot 分层架构、百度云 API 封装,职责分明
- 首次友好 —— 未注册人脸的管理员首次登录时自动引导注册,无需进入设置页面单独操作
如有任何问题或建议,欢迎在评论区交流讨论 🚀
关键词:Vue3,Spring Boot,人脸识别,Face ID,百度云 API,JWT,双因素认证,CSDN
技术栈:Vue 3 + Vite 5 + Element Plus + Spring Boot 3.2 + MyBatis-Plus + MySQL 8 + 百度云 Face API V3
更多推荐

所有评论(0)