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(Web Authentication)是由W3C(World Wide Web Consortium)开发的Web认证标准,旨在提供更安全、更便捷的用户认证体验‌‌。其原理涉及公钥加密、非对称加密和生物识别等技术。

     webauthn 使用非对称加密技术,其中密钥对包括公钥和私钥。公钥用于注册和验证身份,私钥只在用户的设备中保留,用于对挑战进行签名,从而实现了更加安全的认证方式。WebAuthn支持多种生物识别技术,提供了更方便的认证体验,避免了传统密码认证带来的一些安全风险‌。此外,WebAuthn还可以防止钓鱼攻击和恶意软件攻击,因为它不需要用户记住或输入密码,也不需要提供个人信息或电话号码等‌。

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 = foo
            this.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 {

    @Autowired
    private WebauthUserMapper webauthUserMapper;

    @Override
    public 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());
    }

    @Override
    public Optional<ByteArray> getUserHandleForUsername(String username) {
        List<WebauthUser> webauthUsers = webauthUserMapper.selectByUsername(username);
        return Optional.of(ByteArray.fromBase64(webauthUsers.get(0).getHandle()));
    }

    @Override
    public Optional<String> getUsernameForUserHandle(ByteArray userHandle) {
        List<WebauthUser> webauthUsers = webauthUserMapper.selectByHandle(userHandle.getBase64());
        return Optional.of(webauthUsers.get(0).getUsername());
    }

    @Override
    public 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()
        );
    }

    @Override
    public 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";

    @Bean
    public 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")
    @ResponseBody
    public 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")
    @ResponseBody
    public 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")
    @ResponseBody
    public 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")
    @ResponseBody
    public 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);
        }
    }

}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

iOS AccentColor 和 Color Set

AccentColor 和 Color Set 都是 Xcode 中用于颜色管理的功能&#xff0c;它们适用于不同的开发场景和需求。以下是它们的区别和应用场景分析&#xff1a; 1. AccentColor&#xff08;强调色&#xff09; 1.1 概念&#xff1a; • AccentColor 是在 Xcode 12 中引入的&#xf…

TiDB 的MPP架构概述

MPP架构介绍&#xff1a; 如图&#xff0c;TiDB Server 作为协调者&#xff0c;首先 TiDB Server 会把每个TiFlash 拥有的region 会在TiFlash上做交换&#xff0c;让表连接在一个TiFlash上。另外 TiFlash会作为计算节点&#xff0c;每个TiFlash都负责数据交换&#xff0c;表连接…

git回退指定版本/复制提交id

1.使用“git reset --hard 目标版本号”命令将版本回退2.使用“git push -f”提交更改 因为我们回退后的本地库HEAD指向的版本比远程库的要旧&#xff0c;此时如果用“git push”会报错。 改为使用 git push -f 即可完成回退后的提交。

本地部署 LLaMA-Factory

本地部署 LLaMA-Factory 1. 本地部署 LLaMA-Factory2. 下载模型3. 微调模型3-1. 下载数据集3-2. 配置参数3-3. 启动微调3-4. 模型评估3-5. 模型对话 1. 本地部署 LLaMA-Factory 下载代码&#xff0c; git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Facto…

BLE core 内容整理解释

本文内容比较杂散&#xff0c;只是做记录使用&#xff0c;后续会整理的有条理些 link layer 基本介绍 **Link Layer Control&#xff08;链路层控制&#xff09;**是蓝牙低功耗&#xff08;BLE&#xff09;协议栈的核心部分&#xff0c;负责实现设备间可靠、安全、低功耗的数…

HEIC 是什么图片格式?如何把 iPhone 中的 HEIC 转为 JPG?

在 iPhone 拍摄照片时&#xff0c;默认的图片格式为 HEIC。虽然 HEIC 格式具有高压缩比、高画质等优点&#xff0c;但在某些设备或软件上可能存在兼容性问题。因此&#xff0c;将 HEIC 格式转换为更为通用的 JPG 格式就显得很有必要。本教程将介绍如何使用简鹿格式工厂&#xf…

黑马商城项目—服务注册、服务发现

服务注册 我们把item-service注册到Nacos&#xff0c;步骤如下&#xff1a; 1.引入依赖 在item-service的pom.xml中添加依赖&#xff1a; 2.配置Nacos 在item-service的application.yml中添加nacos地址配置: 3.配置服务实例 为了测试一个服务多个实例的情况&#xff0c;我…

如何卸载和升级 Angular-CLI ?

Angular-CLI 是开发人员使用 Angular 的必备工具。然而&#xff0c;随着频繁的更新和新版本的出现&#xff0c;了解如何有效地卸载和升级 Angular-CLI 对开发人员来说至关重要。本指南提供了一个全面的、循序渐进的方法来帮助您顺利过渡到最新版本。 必备条件 确保您的系统上…

有道云笔记批量导出

前言 最近使用有道云笔记遇到打开过慢&#xff0c;导致笔记丢失&#xff0c;需要会员才能找回之前笔记问题。 决定改用思源&#xff0c;程序中的格式比较难于通过复制保留&#xff0c;即使导出成word 或者pdf&#xff0c;需要一个专门工具导出成Markdown格式&#xff0c;批量…

设计模式与游戏完美开发(2)

更多内容可以浏览本人博客&#xff1a;https://azureblog.cn/ &#x1f60a; 该文章主体内容来自《设计模式与游戏完美开发》—蔡升达 第二篇 基础系统 第四章 游戏主要类——外观模式&#xff08;Facade&#xff09; 一、游戏子功能的整合 一个游戏程序常常由内部数个不同的…

学习C++:变量

变量&#xff1a; 作用&#xff1a;给一段指定的内存空间起名&#xff0c;方便操作这段内容 &#xff08;变量存在的意义&#xff1a;方便我们管理内存空间&#xff09; 语法&#xff1a;数据类型 变量名 初始值&#xff1b; 实例&#xff1a;

electron-vite_18 设置系统音量loudness报错

loudness是一款控制系统音量输出的一款 Node.js 库&#xff1b;但是在electron-vite中直接使用编译的时候会报错&#xff1b;这个时候需要单独处理&#xff1b; 错误分析 error Error: spawn E:\xxx\out\main\adjust_get_current_system_volume_vista_plus.exe 查看编译后项目…

Chrome被360导航篡改了怎么改回来?

一、Chrome被360导航篡改了怎么改回来&#xff1f; 查看是否被360主页锁定&#xff0c;地址栏输入chrome://version&#xff0c;看命令行end后面&#xff08;蓝色部分&#xff09;&#xff0c;是否有https://hao.360.com/?srclm&lsn31c42a959f 修改步骤 第一步&#xff1a…

微信小程序-基于Vant Weapp UI 组件库的Area 省市区选择

Area 省市区选择&#xff0c;省市区选择组件通常与 弹出层 组件配合使用。 areaList 格式 areaList 为对象结构&#xff0c;包含 province_list、city_list、county_list 三个 key。 每项以地区码作为 key&#xff0c;省市区名字作为 value。地区码为 6 位数字&#xff0c;前两…

如何用gpt来分析链接里面的内容(比如分析论文链接)和分析包含多个文件中的一块代码

如何用gpt来分析链接里面的内容&#xff0c;方法如下 这里使用gpt4里面有一个网路的功能 点击搜索框下面这个地球的形状即可启动搜索网页模式 然后即可提出问题在搜索框里&#xff1a;发现正确识别和分析了链接里面的内容 链接如下&#xff1a;https://arxiv.org/pdf/2009.1…

GitLab的卸载与重装

目录 一、GitLab的卸载 二、 GitLab的安装与配置 1. 创建安装目录 2. 安装 3. 使用 3.1 初始化 3.2 创建空白项目 ​编辑 3.3 配置SSH 3.3.1 配置公钥 ​编辑 3.3.2 配置私钥 3.4 配置本地git库 一、GitLab的卸载 1. 停止gitlab sudo gitlab-ctl stop 2. 卸载…

中文学习系统:成本效益分析与系统优化

2.1 SSM框架介绍 本课题程序开发使用到的框架技术&#xff0c;英文名称缩写是SSM&#xff0c;在JavaWeb开发中使用的流行框架有SSH、SSM、SpringMVC等&#xff0c;作为一个课题程序采用SSH框架也可以&#xff0c;SSM框架也可以&#xff0c;SpringMVC也可以。SSH框架是属于重量级…

牛客网刷题 ——C语言初阶——BC112小乐乐求和

1.牛客网刷题 ——C语言初阶 牛客网&#xff1a;BC112小乐乐求和 小乐乐最近接触了求和符号Σ&#xff0c;他想计算的结果。但是小乐乐很笨&#xff0c;请你帮助他解答。 输入描述: 输入一个正整数n (1 ≤ n ≤ 109) 输出描述: 输出一个值&#xff0c;为求和结果。 示例1 输…

计算机操作系统与安全复习笔记

1 绪论 操作系统目标: 方便性; 有效性; 可扩充性; 开放性. 作用: 用户与计算机硬件系统之间的接口; 计算机资源的管理者; 实现了对计算机资源的抽象; 计算机工作流程的组织者. 多道程序设计: 内存中同时存放若干个作业, 使其共享系统资源且同时运行; 单处理机环境下宏观上并行…

数据结构(哈希表(下)方法讲解)

前言&#xff1a; 在前一部分中&#xff0c;我们探讨了哈希表的基本原理、设计思想、优势与挑战&#xff0c;并了解了它在实际项目中的应用场景。哈希表作为一种高效的数据结构&#xff0c;在查找、插入和删除等操作上具有显著优势&#xff0c;但要真正掌握它的使用&#xff0…