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;广泛用于…

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

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

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

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

【 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;发现生…

大数据导论汇总

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

React 组件的通信方式

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

【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(‘…

【算法思想04】二分查找

文章目录 1. 基本思想与实现1.1 基本思想1.2 值m的计算方式1.3 查找失败时的返回值1.4 代码实现1.4.1 循环1.4.2 递归 2. 性能分析2.1 时间复杂度2.2 与顺序查找的效率比较 3. 应用3.1 前提3.2 变体3.2.1 最基本的二分查找3.2.2 寻找左侧边界的二分查找3.2.3 寻找右侧边界的二分…

【brainpan靶场渗透】

文章目录 一、基础信息 二、信息收集 三、反弹shell 四、提权 一、基础信息 Kali IP&#xff1a;192.168.20.146 靶机 IP&#xff1a;192.168.20.155 二、信息收集 似乎开放了9999&#xff0c;10000端口&#xff0c;访问页面没有太多内容&#xff0c;扫描一下目录 dirs…

matlab reshape permute

1.reshape 将向量按照顺序重新构建 矩阵&#xff0c;新矩阵 先排完第一列&#xff0c; 再第二列… 2.permute 将向量 维度变换

comctl32.dll没有被指定在window运行怎么解决?

一、文件丢失问题&#xff1a;comctl32.dll没有被指定在Windows上运行怎么解决&#xff1f; comctl32.dll是Windows操作系统中的一个重要组件&#xff0c;它负责提供用户界面元素&#xff0c;如按钮、对话框和列表视图等。当系统提示“comctl32.dll没有被指定在Windows上运行”…

Qt下使用AES进行字符串加密解密

文章目录 前言一、获取QAESEncryption库二、加密与解密实现三、示例完整代码四、下载链接总结 前言 引用&#xff1a;AES&#xff08;Advanced Encryption Standard&#xff09;是一种对称加密算法&#xff0c;被广泛用于数据加密&#xff0c;提供128、192、256位三种密钥长度&…

docker 安装minio

docker pull minio/minio #启动 mkdir -p /root/minio/config mkdir -p /root/minio/datadocker run -d \--name minio \-p 9002:9000 \-p 9001:9001 \--restartalways \-v /root/minio/data:/data \-v /root/minio/config:/root/.minio \-e "MINIO_ACCESS_KEYminioadmin…