建站个人网站,广东网站建设企业,专门做产品推广ppt的网站,一个公司做多个网站1、webauthn介绍
官网#xff1a;https://webauthn.io/
1.1、什么是webauthn#xff1f; webauthn即Web Authentication#xff0c;是一个符合W3C标准的Web认证规范。它通过公私钥加密技术#xff0c;实现无密码认证#xff0c;用户仅需通过pin码、指纹、面部识别、usb …1、webauthn介绍
官网https://webauthn.io/
1.1、什么是webauthn webauthn即Web Authentication是一个符合W3C标准的Web认证规范。它通过公私钥加密技术实现无密码认证用户仅需通过pin码、指纹、面部识别、usb key等方式即可实现整套注册登录流程。使用webauthnweb网站的整个认证流程将得到极大简化同时相比传统的密码认证webauthn的安全性更高。
1.2、主流web认证方式和wenauthn对比 目前使用最多的认证方式有三种分别是账号密码登录、短信邮箱验证登录、第三方登录。
1.2.1、账号密码登录 最古老的认证方式缺点一大堆大部分的密码都是弱密码或者是与本人姓名、生日等有关的密码大部分人的常用密码就那么几个不管登录什么应用都是同一个密码偶尔换个密码很快就忘了账号也会忘掉。
1.2.2、短信邮箱验证登录 相比账号密码安全一些。有时候等不到验证码或者很慢。用户的手机号被泄露收到大量垃圾短信接到大量广告电话不胜其烦。现在配合运营商可以不需要验证码实现手机号一键登录了但是前提是必须是正在使用流量的手机号才行比如卡1开了5g数据连接卡2关闭了数据连接则手机号一键登录只支持卡1不支持卡2。
1.2.3、第三方登录 用qq微信登录完还会强行要求你绑定手机号比较繁琐。
1.2.4、webauthn登录
优点
①、不需要密码但需要用户自己输入一个账号可以是手机号邮箱等。
②、通过设备自带的认证方式登录比如PIN、指纹、面部识别、usb key。
③、极简登录流程用户体验好。
④、安全性高服务器不存储任何密码。
缺点
①、只能是web应用才可以。
②、换设备登录问题。 2、webauthn原理 在介绍原理之前先简单介绍一下公钥、私钥是什么东西防止有人不清楚。明确以下三点就能读懂接下来的内容
①、公钥和私钥是成对生成的。
②、公钥可以随便公开私钥不可以泄露。
③、用公钥加密的数据只有用私钥才能解密反之用私钥加密的数据也只有用公钥才能解密公钥私钥属于非对称加密技术。 webauthn 的原理并不复杂它的核心是基于公钥的加密技术。在 webauthn 中用户的身份认证是通过公钥和私钥来实现的。这很像我们平常使用配置了公私钥的 SSH 登录服务器的过程只不过 webauthn 是在浏览器中实现的。 webauthn 使用非对称加密技术其中密钥对包括公钥和私钥。公钥用于注册和验证身份私钥只在用户的设备中保留用于对挑战进行签名从而实现了更加安全的认证方式。WebAuthn支持多种生物识别技术提供了更方便的认证体验避免了传统密码认证带来的一些安全风险。此外WebAuthn还可以防止钓鱼攻击和恶意软件攻击因为它不需要用户记住或输入密码也不需要提供个人信息或电话号码等。
2.1、组成部分 webauthn 由以下三个组成部分组成 ①. 用户代理User Agent用户代理是指浏览器或者其他支持 WebAuthn 的客户端它负责与用户进行交互收集用户的身份认证信息并将其发送给服务器。 ②. 身份验证器Authenticator身份验证器是指用于生成公钥和私钥的设备如手机、USB 密钥或生物识别器。Windows Hello 和 macOS 的 Touch ID 也都是常见的身份验证器。 ③. Relying PartyRelying Party 是指需要进行身份认证的网站或应用程序它负责生成挑战Challenge并将其发送给用户代理然后验证用户代理发送的签名结果。
上述三者在两个不同的用例注册和认证中协同工作如下图所示。图中的各个实体之间的所有通信都由用户代理通常是Web浏览器处理。 2.2、架构实现 webauthn 用公钥证书代替了密码完成用户的注册和身份认证登录。它更像是现有身份认证的增强或补充。为了保证通信数据安全一般基于HTTPSTLS通信。在这个过程中有4个模块。
Server服务端
它可以被认为一个依赖方Relying Party它会存储用户的公钥并负责用户的注册、认证。
JavaScriptJs脚本
调用浏览器API与Server进行通信发起注册或认证过程。
Browser浏览器
需要包含WebAuthn的Credential Management API提供给js调用还需要实现与认证模块进行通信由浏览器统一封装硬件设备的交互。
Authenticator认证模块
它能够创建、存储、检索身份凭证。它一般是个硬件设备智能卡、USBNFC等也可能已经集成到了你的操作系统比如Windows HelloMacOS的Touch ID等。 3、webauthn流程
首先明确在webauthn认证流程中有三个参与者
①、后端服务server
②、浏览器
③、用户设备中的认证器比如Windows HelloMacOS的Touch ID他负责生成公私钥签名。
3.1、注册流程 ①、用户输入username理解为账号可以是手机号、邮箱等前端发起注册请求把username传到后端。
②、后端拿到username后先验证该username是否被注册然后生成挑战然后将username、挑战缓存起来可以用session或redis然后返回给前端挑战、用户信息、依赖方信息。
1挑战一个随机的ByteArray转成的base64字符串。
2用户信息包含idnamedisplayNameid由后端随机生成出于安全考量这应尽可能不与任何用户信息相关联如不要包含用户名、用户邮箱等name即usernamedisplayName为展示名可以随便。3依赖方信息包含后端指定的签名算法、认证方式、所在域名等信息。
③、浏览器请求认证器生成公钥私钥请求认证器的具体方式如下
navigator.credentials.create(credentialCreateOptions).then(publicKeyCredential {console.log(publicKeyCredential)
})其中参数credentialCreateOptions比较复杂他包含这些内容
{publicKey: {challenge, //挑战rp: { //依赖方信息id,name},user: { //用户信息id,name,displayName},pubKeyCredParams: [ //算法列表{type: public-key,alg}],authenticatorSelection: { //指定的认证器类型可选authenticatorAttachment,userVerification},excludeCredentials: [ //用于标识要排除的凭证{id,transports: [],type: public-key}],timeout //超时时间}
}参数说明如下 challenge: Uint8Array转换为 Uint8Array 的挑战长度至少为 16建议为 32 rp: Object依赖方信息其中有一项为必须 ○ rp.id: String可选依赖方 ID必须为当前域名或为当前域名的子集的域名不是子域名。如域名为 test.123.example.com则依赖方 ID 可以是 test.123.example.com, 123.example.com 或 example.com。不指定则默认使用当前域名 ○ rp.name: String依赖方名称用于方便用户辨认 user: Object用户信息其中有三项为必须 ○ user.id: Uint8Array转换为 Uint8Array 的字符串。出于安全考量这应尽可能不与任何用户信息相关联如不要包含用户名、用户邮箱等 ○ user.name: String登录用户名 ○ user.dispalyName: String用于显示的用户名称显示与否的具体行为取决于浏览器 pubKeyCredParams: Array一个算法列表指明依赖方接受哪些签名算法。列表的每一项都是一个对象拥有两个属性 ○ pubKeyCredParams[].type: String值只能为 “public-key” ○ pubKeyCredParams[].alg: Number一个负整数用于标明算法。具体算法对应的数字可以在 COSE 找到 authenticatorSelection: Object可选用于过滤正确的认证器这里介绍常用的一个参数 ○ authenticatorSelection.authenticatorAttachment: String可选指定要求的认证器类型。如果没有满足要求的认证器认证可能会失败。该参数可以为 null表示接受所有类型的认证器或是以下两个值之一 ■ platform表示仅接受平台内置的、无法移除的认证器如手机的指纹识别设备 ■ cross-platform表示仅接受外部认证器如 USB Key ○ authenticatorSelection.userVerification: String可选指定认证器是否需要验证“用户为本人 (User Verified, UV)”否则只须“用户在场 (User Present, UP)”。具体验证过程取决于认证器不同认证器的认证方法不同也有认证器不支持用户验证而对验证结果的处理情况则取决于依赖方。该参数可以为以下三个值之一 ■ required依赖方要求用户验证 ■ preferred默认依赖方希望有用户验证但也接受用户在场的结果 ■ discouraged依赖方不关心用户验证。对于 iOS/iPad OS 13必须设置为此值否则验证将失败 excludeCredentials: Array可选用于标识要排除的凭证可以避免同一个用户多次注册同一个认证器。如果用户试图注册相同的认证器用户代理会抛出 InvalidStateError 错误。数组中的每一项都是一个公钥凭证对象包含以下属性 ○ excludeCredentials[].type: String值只能为 “public-key” ○ excludeCredentials[].id: Uint8Array要排除的凭证 ID ○ excludeCredentials[].transports: Array可选用于指定该凭证所需的认证器与用户代理的通信方式可以包含以下的一或多个字符串 ■ usb可以通过 USB 连接的认证器 ■ nfc可以通过 NFC 连接的认证器 ■ ble可以通过蓝牙连接的认证器 ■ internal平台内置的、无法移除的认证器 timeout: Number可选方法超时时间的毫秒数超时后将强制终止 create() 并抛出错误。若不设置将使用用户代理的默认值若太大或太小则使用最接近的用户代理默认值范围中的值。推荐值为 5000-120000 返回值PublicKeyCredential 包含以下字段
{rawId: ArrayBuffer(32) {},response: AuthenticatorAttestationResponse {attestationObject: ArrayBuffer(390) {},clientDataJSON: ArrayBuffer(121) {}},id: VByF2w2hDXkVsevQFZdbOJdyCTGOrI1-sVEzOzsNnY0,type: public-key
}id: StringBase64URL 编码的凭证 ID rawId: ArrayBufferArrayBuffer 的原始凭证 ID type: String一定是 “public-key” response: ObjectAuthenticatorAttestationResponse 对象是 PublicKeyCredential 的主要部分包含以下两个内容 ○ response.clientDataJSON: ArrayBuffer客户端数据包含 origin即凭证请求来源、挑战等信息 ○ response.attestationObject: ArrayBufferCBOR 编码的认证器数据包含凭证公钥、凭证 ID、签名如果有、签名计数等信息
④、用户授权生成密钥对通过PIN、指纹、面部识别等方式
⑤、前端讲上一步认证器返回的结果传到后端进行注册验证
⑥、后端首先校验前端传过来的挑战是否和自己缓存中的一致然后利用公钥解密签名解密后的签名应该和挑战一致最后保存公钥和认证信息整个注册流程完成。
3.2、登录流程 ①、浏览器向服务器发送登陆请求携带username
②、服务器向浏览器发送挑战并缓存挑战
③、浏览器向认证器发送挑战、依赖方信息和客户端信息请求对挑战签名
④、认证器请求用户授权动作随后通过依赖方信息找到对应私钥并使用私钥签名挑战即断言交给浏览器
⑤、浏览器将签名后的挑战发送给服务器
⑥、服务器用之前存储的公钥验证挑战是否与发送的一致一致则验证成功返回token
其中第三步浏览器向认证器请求签名的实现方式如下
navigator.credentials.get(credentialGetOptions).then(publicKeyCredential {console.log(publicKeyCredential)
})参数credentialGetOptions包含以下字段
{publicKey: {challenge,rpId,userVerification,allowCredentials: [{id,transports: [],type: public-key}],timeout}
}challenge: Uint8Array转换为 Uint8Array 的挑战长度至少为 16建议为 32rpID: String可选依赖方 ID需要和注册认证器时的一致。规则和上述的 rp.id 一致不指定默认使用当前域名userVerification: String和上文一样只是需要注意它这次不在 authenticatorSelection 中了allowCredentials: Array可选用于标识允许的凭证 ID使用户代理找到正确的认证器。只有符合这个列表中凭证 ID 的凭证才能被成功返回。数组中的每一项都是对象包含以下属性 ○ allowCredentials[].type: String值只能为 “public-key” ○ allowCredentials[].id: Uint8Array允许的凭证 ID ○ allowCredentials[].transports: Array可选用于指定该凭证所需的认证器与用户代理的通信方式可以包含以下的一或多个字符串 ■ usb可以通过 USB 连接的认证器 ■ nfc可以通过 NFC 连接的认证器 ■ ble可以通过蓝牙连接的认证器 ■ internal平台内置的、无法移除的认证器
timeout: Number可选方法超时时间的毫秒数和上面的一样推荐值为 5000-120000 返回值publicKeyCredential包含以下字段
{rawId: ArrayBuffer(32) {},response: AuthenticatorAssertionResponse {authenticatorData: ArrayBuffer(37) {},signature: ArrayBuffer(256) {},userHandle: ArrayBuffer(64) {},clientDataJSON: ArrayBuffer(118) {}}id: VByF2w2hDXkVsevQFZdbOJdyCTGOrI1-sVEzOzsNnY0type: public-key
}id: StringBase64URL 编码的凭证 ID rawId: ArrayBufferArrayBuffer 的原始凭证 ID type: String一定是 “public-key” response: Object对于验证流程认证会返回 AuthenticatorAssertionResponse 而不是 AuthenticatorAttestationResponse 对象这个对象包含以下 4 个属性 ○ response.authenticatorData: ArrayBuffer认证器信息包含认证状态、签名计数等 ○ response.signature: ArrayBuffer被认证器签名的 authenticatorData clientDataHashclientDataJSON 的 SHA-256 hash ○ response.userHandle: ArrayBuffercreate() 创建凭证时的用户 ID user.id。许多 U2F 设备不支持这一特性这一项将会是 null ○ response.clientDataJSON: ArrayBuffer客户端数据包含 origin即凭证请求来源、挑战等信息
3.3、演示视频 3.4、webauthn的几个缺点
1、多设备登录问题
由于私钥存储在设备中换设备登录会比较麻烦有两种办法
1支持多设备绑定即一个用户可以对应多个公钥这需要通过业务开发才能支持
2设备与设备之间打通就是说当前账号是在我手机上注册的如果我想在电脑上登录那么电脑和手机互联电脑把相关数据变成二维码手机扫码完成签名发送到电脑从而完成认证。上面视频中扫码演示的便是这个功能但是只在mac与ios中测试成功了在windows与安卓的组合中并未成功具体原因不清楚。
2、文档不全生态不够好
国内关于webauthn的中文文档十分少webauthn方法的相关框架也不多使用webauthn技术的网站也不多压根没见到生态不够好。
3、不能作为唯一的认证方式
webauthn仅限于浏览器中使用且考虑到设备更换、设备丢失等可能还是需要额外绑定手机、邮箱等作为账号找回的手段。也可以把webauthn作为二次认证的方式。
哪些浏览器支持webauthn
Google Chrome 67 或更高版本Microsoft Edge 85 或更高版本Safari 14 或更高版本
参考文章
https://flyhigher.top/develop/2160.html 4、代码示例
4.1、前端代码
前端先写个username输入框一个登录按钮一个注册按钮代码如下
div stylemargin: 0 auto;width: 300px;text-align: center h4 styledisplay: blockWebauthn Test/h4input typetext idusername placeholderuserName classform-control/div styledisplay: flex;justify-content: space-between;margin-top: 10px;button classbtn-primary idbtn-log styleflex: 1Login/buttonbutton classbtn-danger idbtn-reg styleflex: 1Register/button/div
/divscript typemodule//webauthn.js是对后端api接口的封装不具体展示了import {register, registerauth, login, finishLogin} from ../static/js/webauthn.js//为注册按钮绑定时间$(#btn-reg).click(function () {let val $(#username).val();if (!val) {return;}register({userName: val, displayName: val}, res {if (res.success) {let credentialCreateJson JSON.parse(res.result)let credentialCreateOptions {publicKey: {...credentialCreateJson.publicKey,challenge: base64urlToUint8array(credentialCreateJson.publicKey.challenge),user: {...credentialCreateJson.publicKey.user,id: base64urlToUint8array(credentialCreateJson.publicKey.user.id),},excludeCredentials: credentialCreateJson.publicKey.excludeCredentials.map(credential ({...credential,id: base64urlToUint8array(credential.id),})),extensions: credentialCreateJson.publicKey.extensions,}}console.log(credentialCreateOptions:)console.log(credentialCreateOptions)navigator.credentials.create(credentialCreateOptions).then(publicKeyCredential {console.log(publicKeyCredential)console.log(publicKeyCredential)return {type: publicKeyCredential.type,id: publicKeyCredential.id,response: {attestationObject: uint8arrayToBase64url(publicKeyCredential.response.attestationObject),clientDataJSON: uint8arrayToBase64url(publicKeyCredential.response.clientDataJSON),transports: publicKeyCredential.response.getTransports publicKeyCredential.response.getTransports() || [],},clientExtensionResults: publicKeyCredential.getClientExtensionResults(),}}).then(encodedResult {// const form document.getElementById(form);// const formData new FormData(form);console.log(encodedResult)console.log(encodedResult)// formData.append(credential, JSON.stringify(encodedResult));let param {username: val, credential: JSON.stringify(encodedResult)};registerauth(param, function (res) {console.log(res)if (res.success) {alert(注册成功)} else {alert(res.errorDesc)}})})}})});//为登录按钮绑定事件$(#btn-log).click(function () {let val $(#username).val();if (!val) {return;}login({username: val}, function (res) {if (!res.success) {alert(res.errorDesc)}let credentialGetJson JSON.parse(res.result);let credentialGetOptions {publicKey: {...credentialGetJson.publicKey,allowCredentials: credentialGetJson.publicKey.allowCredentials credentialGetJson.publicKey.allowCredentials.map(credential ({...credential,id: base64urlToUint8array(credential.id),})),challenge: base64urlToUint8array(credentialGetJson.publicKey.challenge),extensions: credentialGetJson.publicKey.extensions,},};console.log(credentialGetOptions);navigator.credentials.get(credentialGetOptions).then(publicKeyCredential {let encodedResult {type: publicKeyCredential.type,id: publicKeyCredential.id,response: {authenticatorData: uint8arrayToBase64url(publicKeyCredential.response.authenticatorData),clientDataJSON: uint8arrayToBase64url(publicKeyCredential.response.clientDataJSON),signature: uint8arrayToBase64url(publicKeyCredential.response.signature),userHandle: publicKeyCredential.response.userHandle uint8arrayToBase64url(publicKeyCredential.response.userHandle),},clientExtensionResults: publicKeyCredential.getClientExtensionResults(),}console.log(encodedResult);return encodedResult;}).then(encodedResult {let params {username: val,credential: JSON.stringify(encodedResult)}finishLogin(params, function (res) {if (res.success) {alert(登陆成功)} else {alert(res.errorDesc)}})})})})
/scriptscriptfunction base64urlToUint8array(base64Bytes) {const padding .substring(0, (4 - (base64Bytes.length % 4)) % 4);return base64js.toByteArray((base64Bytes padding).replace(/\//g, _).replace(/\/g, -));}function uint8arrayToBase64url(bytes) {if (bytes instanceof Uint8Array) {return base64js.fromByteArray(bytes).replace(/\/g, -).replace(/\//g, _).replace(//g, );} else {return uint8arrayToBase64url(new Uint8Array(bytes));}}class WebAuthServerError extends Error {constructor(foo bar, ...params) {super(...params)this.name ServerErrorthis.foo foothis.date new Date()}}function throwError(response) {throw new WebAuthServerError(Error from client, response.body);}function checkStatus(response) {if (response.status ! 200) {throwError(response);} else {return response;}}function initialCheckStatus(response) {checkStatus(response);return response.json();}function followRedirect(response) {if (response.status 200) {window.location.href response.url;} else {throwError(response);}}function displayError(error) {const errorElem document.getElementById(errors);errorElem.innerHTML error;console.error(error);}/script4.2、后端代码
后端有许多验证框架本次以 webauthn-server 为例。
使用框架实现先引入依赖
dependencygroupIdcom.yubico/groupIdartifactIdwebauthn-server-core/artifactIdversion1.12.1/versionscopecompile/scope
/dependency实现CredentialRepository接口
package com.zjh.znwz.service;import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;import com.zjh.common.entity.WebauthUser;
import com.zjh.znwz.dao.WebauthUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;import lombok.Getter;Repository
Getter
public class RegistrationService implements CredentialRepository {Autowiredprivate WebauthUserMapper webauthUserMapper;Overridepublic SetPublicKeyCredentialDescriptor getCredentialIdsForUsername(String username) {ListWebauthUser webauthUsers webauthUserMapper.selectByUsername(username);return webauthUsers.stream().map(webauthUser -PublicKeyCredentialDescriptor.builder().id(ByteArray.fromBase64(webauthUser.getCredentialId())).build()).collect(Collectors.toSet());}Overridepublic OptionalByteArray getUserHandleForUsername(String username) {ListWebauthUser webauthUsers webauthUserMapper.selectByUsername(username);return Optional.of(ByteArray.fromBase64(webauthUsers.get(0).getHandle()));}Overridepublic OptionalString getUsernameForUserHandle(ByteArray userHandle) {ListWebauthUser webauthUsers webauthUserMapper.selectByHandle(userHandle.getBase64());return Optional.of(webauthUsers.get(0).getUsername());}Overridepublic OptionalRegisteredCredential lookup(ByteArray credentialId, ByteArray userHandle) {WebauthUser webauthUsers webauthUserMapper.selectOneByCredentialIdAndHandle(credentialId.getBase64(), userHandle.getBase64());OptionalWebauthUser auth Optional.of(webauthUsers);return auth.map(credential -RegisteredCredential.builder().credentialId(ByteArray.fromBase64(credential.getCredentialId())).userHandle(ByteArray.fromBase64(credential.getHandle())).publicKeyCose(ByteArray.fromBase64(credential.getPublicKey()))
// .signatureCount(credential.getCount()).build());}Overridepublic SetRegisteredCredential lookupAll(ByteArray credentialId) {ListWebauthUser auth webauthUserMapper.selectByCredentialId(new String(credentialId.getBytes()));return auth.stream().map(credential -RegisteredCredential.builder().credentialId(ByteArray.fromBase64(credential.getCredentialId())).userHandle(ByteArray.fromBase64(credential.getHandle())).publicKeyCose(ByteArray.fromBase64(credential.getPublicKey()))
// .signatureCount(credential.getCount()).build()).collect(Collectors.toSet());}
}实例化RelyingParty类放入容器中
package com.zjh.znwz.config;import com.yubico.webauthn.RelyingParty;
import com.yubico.webauthn.data.RelyingPartyIdentity;
import com.zjh.znwz.service.RegistrationService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.HashSet;/*** author 1* date 2023-02-01 11:32**/
Configuration
public class RelyingPartyConfig {//Value(${webauthn.host})private String host http://localhost:9093;// Value(${webauthn.id})private String webauthnId localhost;Beanpublic RelyingParty relyingParty(RegistrationService regisrationRepository) {RelyingPartyIdentity rpIdentity RelyingPartyIdentity.builder().id(webauthnId).name(webauthntest).build();HashSetString set new HashSet();set.add(host);return RelyingParty.builder().identity(rpIdentity).credentialRepository(regisrationRepository).origins(set).build();}
}controller代码
package com.zjh.znwz.controller;import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.yubico.webauthn.*;
import com.yubico.webauthn.data.*;
import com.yubico.webauthn.exception.AssertionFailedException;
import com.yubico.webauthn.exception.RegistrationFailedException;
import com.zjh.common.entity.WebauthUser;
import com.zjh.common.page.Result;
import com.zjh.znwz.dao.WebauthUserMapper;
import com.zjh.znwz.utils.RedisUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.server.ResponseStatusException;import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;/*** author 1* date 2023-01-31 14:56**/
Controller
RequestMapping(/webauthn)
public class TestWebauthnController {private final WebauthUserMapper webauthUserMapper;private final RelyingParty relyingParty;private final RedisUtil redisUtil;public TestWebauthnController(WebauthUserMapper webauthUserMapper, RelyingParty relyingParty, RedisUtil redisUtil) {this.webauthUserMapper webauthUserMapper;this.relyingParty relyingParty;this.redisUtil redisUtil;}PostMapping(/register)ResponseBodypublic Result register(RequestBody MapString, String map, HttpSession session) throws JsonProcessingException {String userName map.get(userName);String displayName map.get(displayName);if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(displayName)) {throw new RuntimeException();}//验证userName是否已被注册ListWebauthUser webauthUsers webauthUserMapper.selectByUsername(userName);if (!CollectionUtils.isEmpty(webauthUsers)) {throw new RuntimeException(userName已被注册);}byte[] bytes new byte[32];new SecureRandom().nextBytes(bytes);UserIdentity userIdentity UserIdentity.builder().name(userName).displayName(displayName).id(new ByteArray(bytes)).build();System.out.println(userIdentity.getId().getBase64());StartRegistrationOptions registrationOptions StartRegistrationOptions.builder().user(userIdentity).build();PublicKeyCredentialCreationOptions registration relyingParty.startRegistration(registrationOptions);redisUtil.set(register- userName, registration.toJson(), 300);try {String s registration.toCredentialsCreateJson();System.out.println(s);return Result.success(s);} catch (JsonProcessingException e) {throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, Error processing JSON., e);}}PostMapping(/registerauth)ResponseBodypublic Result registerauth(RequestBody MapString, String map, HttpSession session) {String username map.get(username);String credential map.get(credential);try {Object o redisUtil.get(register- username);if (o null) {throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, Cached request expired. Try to register again!);}PublicKeyCredentialCreationOptions requestOptions PublicKeyCredentialCreationOptions.fromJson(o.toString());if (requestOptions ! null) {PublicKeyCredentialAuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs pkc PublicKeyCredential.parseRegistrationResponseJson(credential);FinishRegistrationOptions options FinishRegistrationOptions.builder().request(requestOptions).response(pkc).build();RegistrationResult result relyingParty.finishRegistration(options);String credentialId result.getKeyId().getId().getBase64();String publicKey result.getPublicKeyCose().getBase64();WebauthUser webauthUser new WebauthUser();webauthUser.setCredentialId(credentialId);webauthUser.setDisplayName(username);webauthUser.setPublicKey(publicKey);webauthUser.setHandle(requestOptions.getUser().getId().getBase64());webauthUser.setUsername(username);System.out.println(JSON.toJSONString(webauthUser));webauthUserMapper.insert(webauthUser);return Result.success();} else {throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, Cached request expired. Try to register again!);}} catch (RegistrationFailedException e) {throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, Registration failed., e);} catch (IOException e) {throw new ResponseStatusException(HttpStatus.BAD_REQUEST, Failed to save credenital, please try again!, e);}}PostMapping(/login)ResponseBodypublic Result login(RequestBody MapString, String map, HttpSession session) {String username map.get(username);ListWebauthUser webauthUsers webauthUserMapper.selectByUsername(username);if (CollectionUtils.isEmpty(webauthUsers)) {throw new RuntimeException(用户名不存在);}AssertionRequest request relyingParty.startAssertion(StartAssertionOptions.builder().username(username).build());try {redisUtil.set(login- username, request.toJson());return Result.success(request.toCredentialsGetJson());} catch (JsonProcessingException e) {throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());}}PostMapping(/finishLogin)ResponseBodypublic Result finishLogin(RequestBody MapString, String map, HttpSession session) {String username map.get(username);String credential map.get(credential);try {PublicKeyCredentialAuthenticatorAssertionResponse, ClientAssertionExtensionOutputs pkc;pkc PublicKeyCredential.parseAssertionResponseJson(credential);Object o redisUtil.get(login- username);AssertionRequest request AssertionRequest.fromJson(o.toString());AssertionResult result relyingParty.finishAssertion(FinishAssertionOptions.builder().request(request).response(pkc).build());if (result.isSuccess()) {return Result.success();} else {return Result.fail(登陆失败);}} catch (IOException e) {throw new RuntimeException(Authentication failed, e);} catch (AssertionFailedException e) {throw new RuntimeException(Authentication failed, e);}}}4.3、案例演示
前端案例项目
后端案例项目