前言

在管理后台系统中,传统的账号密码登录方式存在着密码泄露、暴力破解等安全隐患。随着 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

二、系统架构

在这里插入图片描述

整个系统的数据流向非常清晰:

  1. 浏览器摄像头 —— 通过 navigator.mediaDevices.getUserMedia() 获取视频流,利用 Canvas 截取帧并转为 Base64
  2. Vue3 前端 —— FaceCapture.vue 组件负责摄像头交互,login/index.vue 编排两步登录流程
  3. Spring Boot 后端 —— 两步端点:/api/admin/login(密码验证)和 /api/admin/login/face(人脸验证/注册)
  4. 百度云人脸 API —— 执行 1:1 人脸搜索和活体检测,人脸特征数据完全存储在云端
  5. 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 }),
  // ... 其他接口
}

九、写在最后

通过本文的实现,我们将管理后台的登录安全从单一密码验证提升到了密码 + 生物特征的双因素认证。整个方案的核心亮点:

  1. 安全性 —— 预授权 Token 机制将两步认证强制绑定,防止绕过密码直接人脸登录
  2. 用户体验 —— Apple Face ID 风格的取景框动画,3-2-1 倒计时自动采集,无需手动拍照
  3. 架构清晰 —— 前端 Vue3 组件化、后端 Spring Boot 分层架构、百度云 API 封装,职责分明
  4. 首次友好 —— 未注册人脸的管理员首次登录时自动引导注册,无需进入设置页面单独操作

如有任何问题或建议,欢迎在评论区交流讨论 🚀

关键词: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

更多推荐