本文转载自微信公众号「魔术程序员k」,作者魔术程序员k。转载本文请联系魔术程序员K公众号。
前言
如今,越来越多的笔记本电脑内置指纹识别,用于快速从锁屏进入桌面。一些客户软件也支持用户身份通过指纹认证。
前几天我在想,既然客户端软件可以调用指纹设备,web终端也应该调用,经过一番折腾,终于实现了这个功能,并应用到我的开源项目中。
本文将与您分享我的实现想法和过程。欢迎感兴趣的开发者阅读本文。
实现思路
提供浏览器Web Authentication API,我们可以用这个API调用用户指纹设备实现用户信息认证。
最终实现效果视频如下:
web实现终端指纹登录
注册指纹
首先,我们需要获得从服务端返回的用户凭证,然后将用户凭证传输到指纹设备,并调整系统的指纹认证。认证通过后,回调函数将返回设备id对于客户端信息,我们需要将这些信息保存在服务端,以便调用指纹设备来验证用户身份,以实现登录。
接下来,我们将总结注册指纹的过程,如下所示:
网站登录成功后,用户使用其他方式返回用户凭证,并将用户凭证保存到本地
检查客户端是否有指纹设备
如果存在,将服务器返回的用户凭证和用户信息传递给指纹注册函数,创建指纹
身份认证成功,回调函数返回设备id与客户端信息,设备id保存到本地
将设备id将客户端信息发送到服务端,并将其存储到指定的用户数据中。
注:注册指纹只能工作 https 连接,或使用 localhost的网站中。
指纹认证
我们网站授权指纹登录后,用户将使用用户凭证和设备id保存在本地,当用户进入我们网站时,会从本地拿到这两条数据,提示它是否需要通过指纹来登录系统,同意之后则将设备id将用户凭证传递给指纹设备,调动系统的指纹认证。认证通过后,调用登录接口获取用户信息。
接下来,我们总结指纹认证的过程,如下所示:
- 从当地获取用户凭证和设备id
- 检查客户端是否有指纹设备
- 如果存在用户凭证和设备,id将指纹认证函数传输进行验证
- 身份认证成功,调用登录接口获取用户信息
注:指纹认证只能用于 https 连接,或是使用 localhost的网站中。
实现过程
在上一章中,我们澄清了指纹登录的具体实现思路。接下来,让我们来看看具体的实现过程和代码。
服务端实现
首先,我们需要在服务端写三个接口:获取TouchID、注册TouchID、指纹登录
获取TouchID
该接口用于判断登录用户是否在本网站上注册了指纹。如果已注册,则返回TouchID方便用户下次登录到客户端。
@ApiOperation(value="获取TouchID",notes="通过用户id获取指纹登录所需的证据")@CrossOrigin()@RequestMapping(value="/getTouchID",method=RequestMethod.POST)publicResultVO<?>getTouchID(@ApiParam(name="传入userId",required=true)@Valid@RequestBodyGetTouchIdDtotouchIdDto,@RequestHeader(value="token")Stringtoken){ *** ONObjectresult=userService.getTouchID(JwtUtil.getUserId(token));if(result.getEnum(ResultEnum.class,"code").getCode()==0){//touchId获取成功returnResultVOUtil.success(result.getString("touchId"));}//返回错误信息returnResultVOUtil.error(result.getEnum(ResultEnum.class,"code").getCode(),result.getEnum(ResultEnum.class,"code").getMessage());}//获取TouchID@Overridepublic *** ONObjectgetTouchID(StringuserId){ *** ONObjectreturnResult=new *** ONObject();//根据当前用户id查询数据库touchIdUseruser=userMapper.getTouchId(userId);StringtouchId=user.getTouchId();if(touchId!=null){//touchId存在returnResult.put("code",ResultEnum.GET_TOUCHID_SUCCESS);returnResult.put("touchId",touchId);returnreturnResult;}//touchId不存在returnResult.put("code",ResultEnum.GET_TOUCHID_ERR);returnreturnResult;}注册TouchID
该接口用于接收客户端指纹设备的返回TouchID将获得的信息与客户端信息保存到数据库的指定用户。
@ApiOperation(value="注册TouchID",notes="保存客户端返回的touchid等信息")@CrossOrigin()@RequestMapping(value="/registeredTouchID",method=RequestMethod.POST)publicResultVO<?>registeredTouchID(@ApiParam(name="传入userId",required=true)@Valid@RequestBodySetTouchIdDtotouchIdDto,@RequestHeader(value="token")Stringtoken){ *** ONObjectresult=userService.registeredTouchID(touchIdDto.getTouchId(),touchIdDto.getClientDataJson(),JwtUtil.getUserId(token));if(result.getEnum(ResultEnum.class,"code").getCode()==0){//touchId获取成功returnResultVOUtil.success(result.getString("data"));}//返回错误信息returnResultVOUtil.error(result.getEnum(ResultEnum.class,"code").getCode(),result.getEnum(ResultEnum.class,"code").getMessage());}//注册TouchID@Overridepublic *** ONObjectregisteredTouchID(StringtouchId,StringclientDataJson,StringuserId){ *** ONObjectresult=new *** ONObject();Userrow=newUser();row.setTouchId(touchId);row.setClientDataJson(clientDataJson);row.setUserId(userId);//根据userId更新touchId与客户端信息intupdateResult=userMapper.updateTouchId(row);if(updateResult>0){result.put("code",ResultEnum.SET_TOUCHED_SUCCESS);result.put("data","touch_id设置成功");returnresult;}result.put("code",ResultEnum.SET_TOUCHED_ERR);returnresult;}指纹登录
该接口接收客户端发送的用户凭证和touchId,然后对数据库中的数据进行并返回用户信息。
@ApiOperation(value="指纹登录",notes="通过touchId用户凭证登录系统")@CrossOrigin()@RequestMapping(value="/touchIdLogin",method=RequestMethod.POST)publicResultVO<?>touchIdLogin(@ApiParam(name="传入TouchID与用户凭证",required=true)@Valid@RequestBodyTouchIDLoginDtotouchIDLogin){ *** ONObjectresult=userService.touchIdLogin(touchIDLogin.getTouchId(),touchIDLogin.getCertificate());returnLoginUtil.getLoginResult(result);}///指纹登录@Overridepublic *** ONObjecttouchIdLogin(StringtouchId,Stringcertificate){ *** ONObjectreturnResult=new *** ONObject();Userrow=newUser();row.setTouchId(touchId);row.setUuid(certificate);Useruser=userMapper.selectUserForTouchId(row);StringuserName=user.getUserName();StringuserId=user.getUserId();///用户名null返回错误信息if(userName==null){///指纹认证失败returnResult.put("code",ResultEnum.TOUCHID_LOGIN_ERR);returnreturnResult;}///指纹认证成功,将用户信息返回客户端//...这里省略代码,根据自己的需要返回用户信息...//returnResult.put("code",ResultEnum.LOGIN_SUCCESS);returnreturnResult;}前端实现
在前端分需要将现有的登录逻辑与指纹认证相结合,我们需要实现指纹注册和指纹注册两个函数。
指纹注册
我们需要接收三个参数:用户名和用户名id、用户凭证,我们需要这三个参数来调用指纹设备来生成指纹,具体的实现代码如下:
touchIDRegistered:asyncfunction(userName:string,userId:string,certificate:string){///检查设备是否支持touchIDconsthasTouchID=awaitPublicKeyCredential.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);///调用指纹设备创建指纹constpublicKeyCredential=awaitnavigator.credentials.create(this.touchIDOptions);if(publicKeyCredential&&"rawId"inpublicKeyCredential){//将rowId转为base64constrawId=publicKeyCredential["rawId"];consttouchId=this.arrayBufferToBase64(rawId);constresponse=publicKeyCredential["response"];//获取客户端信息constclientData *** ON=this.arrayBufferToString(response["clientData *** ON"]);//调用注册TouchID接口this.$api.touchIdLogingAPI.registeredTouchID({touchId:touchId,clientDataJson:clientData *** ON}).then((res:responseDataType<string>)=>{if(res.code===0){//保存touchId用于指纹登录localStorage.setItem("touchId",touchId);return;}alert(res.msg);});}}}在上述函数中,在创建指纹时,使用了一个对象,它必须传递,其定义和每个参数的解释如下:
consttouchIDOptions={publicKey:{rp:{name:"chat-system"},//网站信息user:{name:"",//用户名id:"",//用户id(ArrayBuffer)displayName:""//用户名},pubKeyCredParams:[{type:"public-key",alg:-7//接受算法}],challenge:"",//凭证(touchIDOptions)authenticatorSelection:{authenticatorAttachment:"platform"}}}由于touchIDOptions需要一些参数ArrayBuffer类型,我们数据库保存的数据是base64因此,我们需要实现格式base64与ArrayBuffer实现代码如下:
base64ToArrayBuffer:function(base64:string){constbinaryString=window.atob(base64);constlen=binaryString.length;constbytes=newUint8Array(len);for(leti=0;i<len;i ){bytes[i]=binaryString.charCodeAt(i);}returnbytes.buffer;},arrayBufferToBase64:function(buffer:ArrayBuffer){letbinary="";constbytes=newUint8Array(buffer);constlen=bytes.byteLength;for(leti=0;i<len;i ){binary =String.fromCharCode(bytes[i]);}returnwindow.btoa(binary);}通过指纹认证后,客户端信息将在回调函数中返回。数据类型是ArrayBuffer,数据库所需的格式是string因此,我们需要实现类型ArrayBuffer转string实现代码如下:
arrayBufferToString:function(buffer:ArrayBuffer){letbinary="";constbytes=newUint8Array(buffer);constlen=bytes.byteLength;for(leti=0;i<len;i ){binary =String.fromCharCode(bytes[i]);}returnbinary;}注:用户凭证不能包括 _ 和 **-**否则,这两个字符base64ToArrayBuffer函数将无法成功转换。
指纹登录
该函数接收两个参数:用户凭证、设备id,通过这两个参数调动客户端指纹设备验证身份,具体实现代码如下:
touchIDLogin:asyncfunction(certificate:string,touchId:string){///检查设备是否支持touchIDconsthasTouchID=awaitPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();if(hasTouchID){///更新登录凭证this.touchIDLoginOptions.publicKey.challenge=this.base64ToArrayBuffer(certificate);//更新touchIDthis.touchIDLoginOptions.publicKey.allowCredentials[0].id=this.base64ToArrayBuffer(touchId);///开始验证指纹awaitnavigator.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);constcertificate=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){//...成功授权登录,省略其他代码.../////保存指纹登录用户凭证constcertificate=res.data.certificate;localStorage.setItem("certificate",certificate);///检查设备是否支持touchIDconsthasTouchID=awaitPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();if(hasTouchID){//...省略其他代码...////获取TouchID,测试用户是否授权本网站指纹登录this.$api.touchIdLogingAPI.getTouchID({userId:userId}).then(async(res:responseDataType)=>{if(res.code!==0){//touchId不存在,询问用户是否注册touchIdawaitthis.touchIDRegistered(username,userId,certificate);}//保存touchidlocalStorage.setItem("touchId",res.data);//跳转消息组件awaitthis.$router.push({name:"message"});});return;}///设备不支持touchID,直接跳转消息组件awaitthis.$router.push({name:"message"});return;}///登录失败,切回登录界面this.isLoginStatus=loginStatusEnum.NOT_LOGGED_IN;alert(res.msg);});}最终效果如下:
每次第三方平台授权登录时,都会检测当前用户是否授权本网站。如果它被授权,它将被授权Touch ID通过指纹直接登录保存到本地。
用指纹登录
加载登录页面时1s之后,我们从用户当地取出用户凭证和Touch ID,如有,则提示用户是否需要通过指纹登录系统,具体代码如下:
mounted(){consttouchId=localStorage.getItem("touchId");constcertificate=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