webauthn介绍及应用

1、webauthn介绍

官网:https://webauthn.io/

1.1、什么是webauthn?

        webauthn即Web Authentication,是一个符合W3C标准的Web认证规范。它通过公私钥加密技术,实现无密码认证,用户仅需通过pin码、指纹、面部识别、usb key等方式,即可实现整套注册登录流程。使用webauthn,web网站的整个认证流程将得到极大简化,同时,相比传统的密码认证,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 Party:Relying Party 是指需要进行身份认证的网站或应用程序,它负责生成挑战(Challenge)并将其发送给用户代理,然后验证用户代理发送的签名结果

上述三者在两个不同的用例(注册和认证)中协同工作,如下图所示。图中的各个实体之间的所有通信都由用户代理(通常是Web浏览器)处理。

2.2、架构实现

      webauthn 用公钥证书代替了密码,完成用户的注册和身份认证(登录)。它更像是现有身份认证的增强或补充。为了保证通信数据安全,一般基于HTTPS(TLS)通信。在这个过程中,有4个模块。

Server(服务端)

它可以被认为一个依赖方(Relying Party),它会存储用户的公钥并负责用户的注册认证

JavaScript(Js脚本)

调用浏览器API,与Server进行通信,发起注册或认证过程

Browser(浏览器)

需要包含WebAuthn的Credential Management API提供给js调用,还需要实现与认证模块进行通信,由浏览器统一封装硬件设备的交互。

Authenticator(认证模块)

它能够创建、存储、检索身份凭证。它一般是个硬件设备(智能卡、USB,NFC等),也可能已经集成到了你的操作系统(比如Windows Hello,MacOS的Touch ID等)。

3、webauthn流程

首先明确,在webauthn认证流程中,有三个参与者:

①、后端服务server

②、浏览器

③、用户设备中的认证器:比如Windows Hello,MacOS的Touch ID,他负责生成公私钥,签名。

3.1、注册流程

①、用户输入username(理解为账号,可以是手机号、邮箱等),前端发起注册请求,把username传到后端。

②、后端拿到username后,先验证该username是否被注册;然后生成挑战;然后将username、挑战缓存起来(可以用session或redis);然后返回给前端挑战、用户信息、依赖方信息。

(1)挑战:一个随机的ByteArray转成的base64字符串。

(2)用户信息:包含id,name,displayName;id由后端随机生成(出于安全考量,这应尽可能不与任何用户信息相关联,如不要包含用户名、用户邮箱等),name即username,displayName为展示名,可以随便。(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.comexample.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: String:Base64URL 编码的凭证 ID

  • rawId: ArrayBufferArrayBuffer 的原始凭证 ID

  • type: String:一定是 “public-key”

  • response: Object:AuthenticatorAttestationResponse 对象,是 PublicKeyCredential 的主要部分,包含以下两个内容:

       ○ response.clientDataJSON: ArrayBuffer:客户端数据,包含 origin(即凭证请求来源)、挑战等信息

       ○ response.attestationObject: ArrayBuffer:CBOR 编码的认证器数据,包含凭证公钥、凭证 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,建议为 32
  • rpID: 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-sVEzOzsNnY0"type: "public-key"
}
  • id: String:Base64URL 编码的凭证 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: ArrayBuffer:create() 创建凭证时的用户 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 style="margin: 0 auto;width: 300px;text-align: center" ><h4 style="display: block">Webauthn Test</h4><input type="text" id="username" placeholder="userName" class="form-control"/><div style="display: flex;justify-content: space-between;margin-top: 10px;"><button class="btn-primary" id="btn-log" style="flex: 1">Login</button><button class="btn-danger" id="btn-reg" style="flex: 1">Register</button></div>
</div><script type="module">//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)}})})})})
</script><script>function 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 = 'ServerError'this.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);}</script>

4.2、后端代码

后端有许多验证框架,本次以 webauthn-server 为例。

使用框架实现,先引入依赖:

<dependency><groupId>com.yubico</groupId><artifactId>webauthn-server-core</artifactId><version>1.12.1</version><scope>compile</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 Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(String username) {List<WebauthUser> webauthUsers = webauthUserMapper.selectByUsername(username);return webauthUsers.stream().map(webauthUser ->PublicKeyCredentialDescriptor.builder().id(ByteArray.fromBase64(webauthUser.getCredentialId())).build()).collect(Collectors.toSet());}@Overridepublic Optional<ByteArray> getUserHandleForUsername(String username) {List<WebauthUser> webauthUsers = webauthUserMapper.selectByUsername(username);return Optional.of(ByteArray.fromBase64(webauthUsers.get(0).getHandle()));}@Overridepublic Optional<String> getUsernameForUserHandle(ByteArray userHandle) {List<WebauthUser> webauthUsers = webauthUserMapper.selectByHandle(userHandle.getBase64());return Optional.of(webauthUsers.get(0).getUsername());}@Overridepublic Optional<RegisteredCredential> lookup(ByteArray credentialId, ByteArray userHandle) {WebauthUser webauthUsers = webauthUserMapper.selectOneByCredentialIdAndHandle(credentialId.getBase64(), userHandle.getBase64());Optional<WebauthUser> 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 Set<RegisteredCredential> lookupAll(ByteArray credentialId) {List<WebauthUser> 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();HashSet<String> 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 Map<String, 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是否已被注册List<WebauthUser> 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 Map<String, 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) {PublicKeyCredential<AuthenticatorAttestationResponse, 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 Map<String, String> map, HttpSession session) {String username = map.get("username");List<WebauthUser> 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 Map<String, String> map, HttpSession session) {String username = map.get("username");String credential = map.get("credential");try {PublicKeyCredential<AuthenticatorAssertionResponse, 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、案例演示

前端案例项目

后端案例项目

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/891134.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ElasticSearch如何做性能优化?

大家好&#xff0c;我是锋哥。今天分享关于【ElasticSearch如何做性能优化?】面试题。希望对大家有帮助&#xff1b; ElasticSearch如何做性能优化? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Elasticsearch 是一个开源的分布式搜索引擎&#xff0c;广泛用于…

Python 高级游戏开发:构建一个基于 Pygame 的多人在线战斗游戏

在本篇文章中,我们将介绍如何使用 Python 和 Pygame 库开发一个具有多人在线功能的高级游戏。我们将涵盖如何搭建游戏服务器,如何实现客户端-服务器模型,以及如何处理网络通信和游戏逻辑。这不仅能帮助你提升游戏开发技能,还能加深对 Python 网络编程的理解。 一、准备工作…

瑞芯微全新芯片平台RK3506优势详解,高集成低功耗,为工业而生 触觉智能测评

RK3506是瑞芯微Rockchip在2024年第四季度全新推出的Arm嵌入式芯片平台&#xff0c;三核Cortex-A7单核Cortex-M0多核异构设计&#xff0c;CPU频率达1.5Ghz, M0 MCU为200Mhz。 而RK3506芯片平台下的工业级芯片型号RK3506J&#xff0c;具备-40-85℃的工业宽温性能、发热量小&#…

CentOS7 初始化模板

一、下载Centos7的qcow2镜像 CentOS Cloud Images qemu-img.exe convert -f qcow2 -O vmdk CentOS-7-x86_64-GenericCloud-2211.qcow2 centos7.vmdk 二、更改root密码 注意由于有cloud init的原因&#xff0c;按网上进入救援模式的办法无法修改密码&#xff0c;会卡住不动。…

Docker 安装全攻略:从入门到上手

Docker 安装全攻略&#xff1a;从入门到上手 在当今的软件开发与部署领域&#xff0c;Docker 已经成为了一项不可或缺的关键技术。它能够将应用程序及其依赖项打包成轻量级、可移植的容器&#xff0c;极大地简化了开发、测试和部署的流程。本文将详细讲解在不同操作系统下 Doc…

代码随想录算法训练营第六十天 | 图 | A星算法

Day 60 总结 自己实现中遇到哪些困难今日收获&#xff0c;记录一下自己的学习时间 13:00 - 14:00 BFS 题目&#xff1a;127. 骑士的攻击 给定两个坐标&#xff0c;搜索最短路径 使用 BFS&#xff0c;广度搜索&#xff0c;按层搜索找到最短路径 public class Main {public…

【 Sonarqube】可视化Java项目单元测试覆盖率统计框架搭建

一、项目背景&#xff1a; 一个小公司的朋友反应他们那边Java项目单元测试有&#xff0c;但还没有可视化统计覆盖率数据&#xff0c;没法统计就不能直观的看到单测的覆盖率&#xff0c;Java的覆盖率统计框架还是比较成熟&#xff0c;部署起来也不是很难&#xff0c;下面我们逐…

PTA数据结构编程题7-1最大子列和问题

我参考的B站up的思路 题目 题目链接 给定K个整数组成的序列{ N 1 ​ , N 2 ​ , …, N K ​ }&#xff0c;“连续子列”被定义为{ N i ​ , N i1 ​ , …, N j ​ }&#xff0c;其中 1≤i≤j≤K。“最大子列和”则被定义为所有连续子列元素的和中最大者。例如给定序列{ -2, 1…

jangow-01-1.0.1靶机

靶机 ip&#xff1a;192.168.152.155 把靶机的网络模式调成和攻击机kali一样的网络模式&#xff0c;我的kali是NAT模式, 在系统启动时(长按shift键)直到显示以下界面 ,我们选第二个&#xff0c;按回车。 继续选择第二个&#xff0c;这次按 e 进入编辑页面 接下来&#xff0c;…

在Windows上读写Linux磁盘镜像的一种方法

背景 嵌入式开发中&#xff0c;经常会把系统的Linux磁盘镜像保存到Windows上&#xff0c;以便上传到网盘备份或发送给工厂&#xff0c;但是如果想读取/修改镜像中的某个文件&#xff0c;一般有2种方案&#xff1a; 直接访问 就是用虚拟磁盘软件将镜像文件挂载成磁盘&#xf…

apisix的hmac-auth认证

目录 1、apisix的hmac认证Authorization头信息 2、signature的lua生成源码 3、java生成签证的简单示例 4、postman调用如下 apisix的hmac-auth认证&#xff0c;介绍可以看官方文档 hmac-auth | Apache APISIX -- Cloud-Native API Gateway 照着官方文档&#xff0c;发现生…

某些iphone手机录音获取流stream延迟问题 以及 录音一次第二次不录音问题

一些型号的iphone手机录音获取流stream延迟问题 以及 录音一次第二次不录音问题 延迟问题 navigator.mediaDevices.getUserMedia({ audio: true }) .then((stream) > {console.log(stream) }&#xff09;从开始到获取stream会有将近2s的延迟 导致按下按钮开始录音 会有前…

大数据导论汇总

第一章 作业 2、请阐述把数据变得可用需要经过哪几个步骤. 答&#xff1a;一共要经历三个步骤&#xff1a;数据清洗、数据管理、数据分析。 第一步&#xff1a;数据清洗。使用数据的第一步通常是数据清洗&#xff0c;也就是把数据变成一种可用的状态。这个过程需要借助工具去…

React 组件的通信方式

在 React 应用开发中&#xff0c;组件之间的通信是构建复杂用户界面和交互逻辑的关键。正确地实现组件通信能够让我们的应用更加灵活和易于维护。以下是几种常见的 React组件通信方式。 一、父子组件通信 1. 通过 props 传递数据&#xff08;父组件向子组件传递数据&#xff0…

高级Python游戏开发:基于Pygame的2D平台跳跃游戏

在这篇文章中,我们将开发一个基于 Python 和 Pygame 的2D平台跳跃游戏。这个游戏将包含多个关卡、玩家角色的跳跃控制以及简单的敌人和障碍物。通过这个项目,你将学习如何使用 Pygame 创建更复杂的游戏逻辑,如何处理碰撞检测以及如何设计关卡系统。 一、项目设计概述 在这…

Hugging Face Dataset的 dataset_info.json 文件详解

Hugging Face Dataset的 dataset_info.json 文件详解 什么是 dataset_info.json 文件&#xff1f; 在使用 Hugging Face&#xff08;HF&#xff09;数据集时&#xff0c;dataset_info.json 文件是一个描述数据集及其元数据的重要配置文件。这个文件包含了有关数据集的基本信息…

【JDBC】转账案例

回顾 使用工具类查询表 需求&#xff1a; 查询student表的所有数据&#xff0c;把数据封装到一个集合中 数据准备 #创建表 CREATE TABLE student( sid INT, name VARCHAR(100), age INT, sex VARCHAR(100) ) #插入数据 INSERT INTO student VALUES(1,张三,18,女),(2…

dede-cms关于shell漏洞

一.文件式管理器 1.新建文件 新建一个php文件&#xff0c;内容写个php脚本语言 访问&#xff0c;可以运行 2.文件上传 上传一个php文件&#xff0c;内容同样写一个php代码 访问&#xff0c;运行成功 二.模块-广告管理 来到模块-广告管理——>增加一个新广告 在这里试一下…

k-Means聚类算法 HNUST【数据分析技术】(2025)

1.理论知识 K-means算法&#xff0c;又称为k均值算法。K-means算法中的k表示的是聚类为k个簇&#xff0c;means代表取每一个聚类中数据值的均值作为该簇的中心&#xff0c;或者称为质心&#xff0c;即用每一个的类的质心对该簇进行描述。K-Means算法接受参数K&#xff1b;然后将…

Opencv之对图片的处理和运算

Opencv实现对图片的处理和修改 目录 Opencv实现对图片的处理和修改灰度图读取灰度图转换灰度图 RBG图单通道图方法一方法二 单通道图显色合并单通道图 图片截取图片打码图片组合缩放格式1格式2 图像运算图像ma[m:n,x:y]b[m1:n1,x1:y1] add加权运算 灰度图 读取灰度图 imread(‘…