前言

现在越来越多的笔记本电脑内置了指纹识别,用于快速从锁屏进入桌面,一些客户端的软件也支持通过指纹来认证用户身份。

前几天我在想,既然客户端软件能调用指纹设备,web端应该也可以调用,经过一番折腾后,终于实现了这个功能,并应用在了我的开源项目中。

本文就跟大家分享下我的实现思路以及过程,欢迎各位感兴趣的开发者阅读本文。

实现思路

浏览器提供了Web Authentication API, 我们可以利用这套API来调用用户的指纹设备来实现用户信息认证,有关WebAuthn的更多内容请移步:WebAuthn在线体验、WebAuthn使用文档

最终的实现效果视频,请移步:web端指纹登录

注册指纹

首先,我们需要拿到服务端返回的用户凭证,随后将用户凭证传给指纹设备,调起系统的指纹认证,认证通过后,回调函数会返回设备id与客户端信息,我们需要将这些信息保存在服务端,用于后面调用指纹设备来验证用户身份,从而实现登录。

接下来,我们总结下注册指纹的过程,如下所示:

  • 用户使用其他方式在网站登录成功后,服务端返回用户凭证,将用户凭证保存到本地
  • 检测客户端是否存在指纹设备
  • 如果存在,将服务端返回的用户凭证与用户信息传递给指纹注册函数来创建指纹
  • 身份认证成功,回调函数返回设备id与客户端信息,将设备id保存到本地
  • 将设备id与客户端信息发送至服务端,将其存储到指定用户数据中。

⚠️注意:注册指纹只能工作在使用 https 连接,或是使用 localhost的网站中。

指纹认证

用户在我们网站授权指纹登录后,会将用户凭证与设备id保存在本地,当用户进入我们网站时,会从本地拿到这两条数据,提示它是否需要通过指纹来登录系统,同意之后则将设备id与用户凭证传给指纹设备,调起系统的指纹认证,认证通过后,调用登录接口,获取用户信息。

接下来,我们总结下指纹认证的过程,如下所示:

  • 从本地获取用户凭证与设备id
  • 检测客户端是否存在指纹设备
  • 如果存在,将用户凭证与设备id传给指纹认证函数进行校验
  • 身份认证成功,调用登录接口获取用户信息

⚠️注意:指纹认证只能工作在使用 https 连接,或是使用 localhost的网站中。

实现过程

上一个章节,我们捋清了指纹登录的具体实现思路,接下来我们来看下具体的实现过程与代码。

服务端实现

首先,我们需要在服务端写3个接口:获取TouchID、注册TouchID、指纹登录

获取TouchID

这个接口用于判断登录用户是否已经在本网站注册了指纹,如果已经注册则返回TouchID到客户端,方便用户下次登录。

  • controller层代码如下
    @ApiOperation(value = "获取TouchID", notes = "通过用户id获取指纹登录所需凭据")@CrossOrigin()@RequestMapping(value = "/getTouchID", method = RequestMethod.POST)public ResultVO<?> getTouchID(@ApiParam(name = "传入userId", required = true) @Valid @RequestBody GetTouchIdDto touchIdDto, @RequestHeader(value = "token") String token) {JSONObject result = userService.getTouchID(JwtUtil.getUserId(token));if (result.getEnum(ResultEnum.class, "code").getCode() == 0) {// touchId获取成功return ResultVOUtil.success(result.getString("touchId"));}// 返回错误信息return ResultVOUtil.error(result.getEnum(ResultEnum.class, "code").getCode(), result.getEnum(ResultEnum.class, "code").getMessage());}
  • 接口具体实现代码如下
    // 获取TouchID@Overridepublic JSONObject getTouchID(String userId) {JSONObject returnResult = new JSONObject();// 根据当前用户id从数据库查询touchIdUser user = userMapper.getTouchId(userId);String touchId = user.getTouchId();if (touchId != null) {// touchId存在returnResult.put("code", ResultEnum.GET_TOUCHID_SUCCESS);returnResult.put("touchId", touchId);return returnResult;}// touchId不存在returnResult.put("code", ResultEnum.GET_TOUCHID_ERR);return returnResult;}

注册TouchID

这个接口用于接收客户端指纹设备返回的TouchID与客户端信息,将获取到的信息保存到数据库的指定用户。

  • controller层代码如下
    @ApiOperation(value = "注册TouchID", notes = "保存客户端返回的touchid等信息")@CrossOrigin()@RequestMapping(value = "/registeredTouchID", method = RequestMethod.POST)public ResultVO<?> registeredTouchID(@ApiParam(name = "传入userId", required = true) @Valid @RequestBody SetTouchIdDto touchIdDto, @RequestHeader(value = "token") String token) {JSONObject result = userService.registeredTouchID(touchIdDto.getTouchId(), touchIdDto.getClientDataJson(), JwtUtil.getUserId(token));if (result.getEnum(ResultEnum.class, "code").getCode() == 0) {// touchId获取成功return ResultVOUtil.success(result.getString("data"));}// 返回错误信息return ResultVOUtil.error(result.getEnum(ResultEnum.class, "code").getCode(), result.getEnum(ResultEnum.class, "code").getMessage());}
  • 接口具体实现代码如下
    // 注册TouchID@Overridepublic JSONObject registeredTouchID(String touchId, String clientDataJson, String userId) {JSONObject result = new JSONObject();User row = new User();row.setTouchId(touchId);row.setClientDataJson(clientDataJson);row.setUserId(userId);// 根据userId更新touchId与客户端信息int updateResult = userMapper.updateTouchId(row);if (updateResult>0) {result.put("code", ResultEnum.SET_TOUCHED_SUCCESS);result.put("data", "touch_id设置成功");return result;}result.put("code", ResultEnum.SET_TOUCHED_ERR);return result;}

指纹登录

这个接口接收客户端发送的用户凭证与touchId,随后将其和数据库中的数据进行校验,返回用户信息。

  • controller层代码如下
    @ApiOperation(value = "指纹登录", notes = "通过touchId与用户凭证登录系统")@CrossOrigin()@RequestMapping(value = "/touchIdLogin", method = RequestMethod.POST)public ResultVO<?> touchIdLogin(@ApiParam(name = "传入Touch ID与用户凭证", required = true) @Valid @RequestBody TouchIDLoginDto touchIDLogin) {JSONObject result = userService.touchIdLogin(touchIDLogin.getTouchId(), touchIDLogin.getCertificate());return LoginUtil.getLoginResult(result);}
  • 接口具体实现代码如下
    // 指纹登录@Overridepublic JSONObject touchIdLogin(String touchId, String certificate) {JSONObject returnResult = new JSONObject();User row = new User();row.setTouchId(touchId);row.setUuid(certificate);User user = userMapper.selectUserForTouchId(row);String userName = user.getUserName();String userId = user.getUserId();// 用户名为null则返回错误信息if (userName == null) {// 指纹认证失败returnResult.put("code", ResultEnum.TOUCHID_LOGIN_ERR);return returnResult;}// 指纹认证成功,返回用户信息至客户端// ... 此处代码省略,根据自己的需要返回用户信息即可 ...//returnResult.put("code", ResultEnum.LOGIN_SUCCESS);return returnResult;}

前端实现

前端部分,需要将现有的登录逻辑和指纹认证相结合,我们需要实现两个函数:指纹注册、指纹登录。

指纹注册

这个函数我们需要接收3个参数:用户名、用户id、用户凭证,我们需要这三个参数来调用指纹设备来生成指纹,具体的实现代码如下:

    touchIDRegistered: async function(userName: string,userId: string,certificate: string) {// 校验设备是否支持touchIDconst hasTouchID = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();if (hasTouchID &&window.confirm("检测到您的设备支持指纹登录,是否启用?")) {// 更新注册凭证this.touchIDOptions.publicKey.challenge = this.base64ToArrayBuffer(certificate);// 更新用户名、用户idthis.touchIDOptions.publicKey.user.name = userName;this.touchIDOptions.publicKey.user.displayName = userName;this.touchIDOptions.publicKey.user.id = this.base64ToArrayBuffer(userId);// 调用指纹设备,创建指纹const publicKeyCredential = await navigator.credentials.create(this.touchIDOptions);if (publicKeyCredential && "rawId" in publicKeyCredential) {// 将rowId转为base64const rawId = publicKeyCredential["rawId"];const touchId = this.arrayBufferToBase64(rawId);const response = publicKeyCredential["response"];// 获取客户端信息const clientDataJSON = this.arrayBufferToString(response["clientDataJSON"]);// 调用注册TouchID接口this.$api.touchIdLogingAPI.registeredTouchID({touchId: touchId,clientDataJson: clientDataJSON}).then((res: responseDataType<string>) => {if (res.code === 0) {// 保存touchId用于指纹登录localStorage.setItem("touchId", touchId);return;}alert(res.msg);});}}}

上面函数中在创建指纹时,用到了一个对象,它是创建指纹必须要传的,它的定义以及每个参数的解释如下所示:

const touchIDOptions = {publicKey: {rp: { name: "chat-system" }, // 网站信息user: {name: "", // 用户名id: "", // 用户id(ArrayBuffer)displayName: "" // 用户名},pubKeyCredParams: [{ type: "public-key", alg: -7 },{type: "public-key",alg: -35},{ type: "public-key", alg: -36 },{ type: "public-key", alg: -257 },{type: "public-key",alg: -258},{ type: "public-key", alg: -259 },{ type: "public-key", alg: -37 },{type: "public-key",alg: -38},{ type: "public-key", alg: -39 },{ type: "public-key", alg: -8 }], // 接受的加密算法challenge: "", // 凭证(ArrayBuffer)timeout: 60000, // 授权超时时间authenticatorSelection: {authenticatorAttachment: "platform"} // 只接受指纹登录或windows hello登录}
}

由于touchIDOptions中,有的参数需要ArrayBuffer类型,我们数据库保存的数据是base64格式的,因此我们需要实现base64与ArrayBuffer之间相互转换的函数,实现代码如下:

    base64ToArrayBuffer: function(base64: string) {const binaryString = window.atob(base64);const len = binaryString.length;const bytes = new Uint8Array(len);for (let i = 0; i < len; i++) {bytes[i] = binaryString.charCodeAt(i);}return bytes.buffer;},arrayBufferToBase64: function(buffer: ArrayBuffer) {let binary = "";const bytes = new Uint8Array(buffer);const len = bytes.byteLength;for (let i = 0; i < len; i++) {binary += String.fromCharCode(bytes[i]);}return window.btoa(binary);}

指纹认证通过后,会在回调函数中返回客户端信息,数据类型是ArrayBuffer,数据库需要的格式是string类型,因此我们需要实现ArrayBuffer转string的函数,实现代码如下:

    arrayBufferToString: function(buffer: ArrayBuffer) {let binary = "";const bytes = new Uint8Array(buffer);const len = bytes.byteLength;for (let i = 0; i < len; i++) {binary += String.fromCharCode(bytes[i]);}return binary;}

注意⚠️:用户凭证中不能包含 _ 和 **-**这两个字符,否则base64ToArrayBuffer函数将无法成功转换。

指纹登录

这个函数接收2个参数:用户凭证、设备id,我们会通过这两个参数来调起客户端的指纹设备来验证身份,具体的实现代码如下:

touchIDLogin: async function(certificate: string, touchId: string) {// 校验设备是否支持touchIDconst hasTouchID = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();if (hasTouchID) {// 更新登录凭证this.touchIDLoginOptions.publicKey.challenge = this.base64ToArrayBuffer(certificate);// 更新touchIDthis.touchIDLoginOptions.publicKey.allowCredentials[0].id = this.base64ToArrayBuffer(touchId);// 开始校验指纹await navigator.credentials.get(this.touchIDLoginOptions);// 调用指纹登录接口this.$api.touchIdLogingAPI.touchIdLogin({touchId: touchId,certificate: certificate}).then((res: responseDataType) => {if (res.code == 0) {// 存储当前用户信息localStorage.setItem("token", res.data.token);localStorage.setItem("refreshToken", res.data.refreshToken);localStorage.setItem("profilePicture", res.data.avatarSrc);localStorage.setItem("userID", res.data.userID);localStorage.setItem("username", res.data.username);const certificate = res.data.certificate;localStorage.setItem("certificate", certificate);// 跳转消息组件this.$router.push({name: "message"});return;}// 切回登录界面this.isLoginStatus = loginStatusEnum.NOT_LOGGED_IN;alert(res.msg);});}}

注意⚠️:注册新的指纹后,旧的Touch id就会失效,只能通过新的Touch ID来登录,否则系统无法调起指纹设备,会报错:认证出了点问题。

整合现有登录逻辑

完成上述步骤后,我们已经实现了整个指纹的注册、登录的逻辑,接下来我们来看看如何将其与现有登录进行整合。

调用指纹注册

当用户使用用户名、密码或者第三方平台授权登录成功后,我们就调用指纹注册函数,提示用户是否对本网站进行授权,实现代码如下:

 authLogin: function(state: string, code: string, platform: string) {this.$api.authLoginAPI.authorizeLogin({state: state,code: code,platform: platform}).then(async (res: responseDataType) => {if (res.code == 0) {//  ... 授权登录成功,其他代码省略 ... //// 保存用户凭证,用于指纹登录const certificate = res.data.certificate;localStorage.setItem("certificate", certificate);// 校验设备是否支持touchIDconst hasTouchID = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();if (hasTouchID) {//  ... 其他代码省略 ... //// 获取Touch ID,检测用户是否已授权本网站指纹登录this.$api.touchIdLogingAPI.getTouchID({userId: userId}).then(async (res: responseDataType) => {if (res.code !== 0) {// touchId不存在, 询问用户是否注册touchIdawait this.touchIDRegistered(username, userId, certificate);}// 保存touchidlocalStorage.setItem("touchId", res.data);// 跳转消息组件await this.$router.push({name: "message"});});return;}// 设备不支持touchID,直接跳转消息组件await this.$router.push({name: "message"});return;}// 登录失败,切回登录界面this.isLoginStatus = loginStatusEnum.NOT_LOGGED_IN;alert(res.msg);});}

最终的效果如下所示:

每次第三方平台授权登录时都会检测当前用户是否已授权本网站,如果已授权则将Touch ID保存至本地,用于通过指纹直接登录。

调用指纹登录

当登录页面加载完毕1s后,我们从用户本地取出用户凭证与Touch ID,如果存在则提示用户是否需要通过指纹来登录系统,具体代码如下所示:

  mounted() {const touchId = localStorage.getItem("touchId");const certificate = localStorage.getItem("certificate");// 如果touchId存在,则调用指纹登录if (touchId && certificate) {// 提示用户是否需要touchId登录setTimeout(() => {if (window.confirm("您已授权本网站通过指纹登录,是否立即登录?")) {this.touchIDLogin(certificate, touchId);}}, 1000);}}

最终效果如下所示:

项目地址

本文代码的完整地址请移步:Login.vue

  • 在线体验地址:chat-system
  • 项目GitHub地址:chat-system-github

写在最后

最近打算换工作,有没有广州这边的公司可以内推我呀

实现Web端指纹登录相关推荐

  1. 用python写注册登录界面web_用Python实现web端用户登录和注册功能

    这篇文章主要介绍了用Python实现web端用户登录和注册功能的教程,需要的朋友可以参考下 用户管理是绝大部分Web网站都需要解决的问题.用户管理涉及到用户注册和登录. 用户注册相对简单,我们可以先通 ...

  2. python123注册登录_用Python实现web端用户登录和注册功能的教程

    用户管理是绝大部分Web网站都需要解决的问题.用户管理涉及到用户注册和登录. 用户注册相对简单,我们可以先通过API把用户注册这个功能实现了: _RE_MD5 = re.compile(r'^[0-9 ...

  3. Teamcenter Web 端自动登录 (SSO)

    原生的机制 Teamcenter 2007 的web 端, 本身提供了SSO 的机制. 原生的机制使用 Java Applet 的方式获取浏览器端所在的机器登录的用户名,使用这个用户名进行登录, 而无 ...

  4. 网页版人脸登录,Web端人脸登录,人脸识别,基于Springboot+vue2.X版本+mysql

    或许这个才是你真正需要的!感兴趣的同学gitee Star一下哦! 阅读目录 介绍 一,springboot后端项目 1,拉取项目后,导入相关依赖jar包 2,执行sql文件夹下面的mysql脚本 3 ...

  5. 用python写注册登录_用Python实现web端用户登录和注册功能的教程

    用户管理是绝大部分Web网站都需要解决的问题.用户管理涉及到用户注册和登录. 用户注册相对简单,我们可以先通过API把用户注册这个功能实现了: _RE_MD5 = re.compile(r'^[0-9 ...

  6. java模拟网易邮箱登录_求赐教:网易邮箱Web端模拟登录看信的加密参数_ntes_nnid、_ntes_nuid...

    网易邮箱的模拟登录和收信都非常简单,在阅读邮件的时候需要两个参数:_ntes_nnid 和 _ntes_nuid _ntes_nnid=21533f97b25070a31c249f59513ad20c ...

  7. 关于Facebook Web端第三方登录

    1.集成并测试facebook 1.注册开发者账号 2.创建应用 2.代码块 3.测试结果 1.注册开发者账号 登陆facebook开发者平台 (https://developers.facebook ...

  8. android 访问web,android 访问web端与解析json,模拟用户登录

    之前写过一个java web端的登录验证,最后返回一个json字符串. 字符串格式如下:{"appmsg":"账号或密码错误","appcode&qu ...

  9. C#进行Visio二次开发之Web端启动绘图客户端并登录

    有这样的需求,一个系统,包含Web端的后台和Winform的绘图客户端程序,用户需要在Web端能够启动绘图客户端,并且不需要重新登录(因为已经登录了Web端了). 那么在IE的Web端,如何启动Win ...

最新文章

  1. PAT(甲级)2020年春季考试 7-2 The Judger
  2. Android 保存图片到系统及相关问题的解决方案
  3. sql2005-数据库备份方案
  4. 几种快速傅里叶变换(FFT)的C++实现
  5. 键盘事件与JS Filter
  6. 怎样备份和恢复SAV企业版服务器的设置
  7. 安全狗服云手机端上架各大手机应有市场
  8. Eclipse 反编译之 JadClipse
  9. C# socket编程TcpClient与TcpListener UdpClient
  10. GPS卫星定位基本原理
  11. 线上展示3D可视化电子沙盘管理系统
  12. 0CTF2017 WEB WriteUp
  13. 什么是虚拟主机管理系统?
  14. 惠普无线鼠标没有反应
  15. oracle 与plc通信,cim系统(com系统和plc的通讯)
  16. Android——百度语音唤醒
  17. 3.5mm耳机喇叭和麦克接头差异
  18. 【078】比才歌剧《卡门》序曲
  19. 5G毫米波Vs毫米波通信模块
  20. 快速了解位运算符——与()、非(~)、或(|)、异或(^)

热门文章

  1. 开发者周刊:英特尔再爆重大芯片漏洞;微软开源Bing搜索关键算法;Facebook联合创始人呼吁拆分Facebook
  2. 23-25-35岁北漂者的未来该如何规划?~转
  3. B. Multiply by 2, divide by 6(数学) Codeforces Round #653 (Div. 3)
  4. 深度学习框架智能时代的操作系统是什么?
  5. 新开课day21+day22总结
  6. typedef——用法总结
  7. ug曲面建模实例教程计算机,UG曲面造型实例-直接建模
  8. 目标检测评价指标:精确度mAP
  9. 【数据结构与算法】-6.1图的基本概念和术语
  10. 高晓松《晓说》为何这么红?