引言

随着网络安全意识的提升和用户对便捷性的追求,传统的密码认证方式正逐渐显现出其局限性。Apple在2022年WWDC上推出的Passkey技术,为这一挑战提供了革命性的解决方案。Passkey作为一种基于WebAuthn标准的强身份验证方法,利用公钥加密确保了极致的安全性,同时简化了用户的登录体验。7月13日 作为全球最大同性交友社区的github开始公测Passkey,标志着其进入无密码身份验证阶段。我们也紧跟步伐,本文将结合Golang与Vue实现一个支持Passkey的登录系统。

为什么选择Passkey?

  1. 安全性:Passkey使用高级加密算法来保护用户的密码。

  2. 易用性:用户界面简洁,易于使用。

  3. 跨平台:支持多种操作系统和设备。

技术概览

  • Passkey:一种无密码认证方式,通过用户的生物识别或设备特定认证(如Touch ID/Face ID),生成一对公私钥,公钥存储于服务器,私钥则安全地保存在用户设备上,实现了零知识证明的安全登录。

  • Golang

  • Vue.js

后端实现(Golang)

使用go-webauthn/webauthn这个包来实现WebAuthn功能,我们将创建一个基本的注册与验证示例。首先,确保安装了这个包:

go get github.com/go-webauthn/webauthn

接下来是go-webauthn/webauthn包的简化示例代码,涵盖用户注册新设备和通过已注册设备验证的过程:

package main

import (
	"context"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/duo-labs/go-webauthn/webauthn"
	"github.com/duo-labs/webauthn/protocol"
)

var web *webauthn.WebAuthn
var credentials map[string]*webauthn.Credential // 假设这是一个全局存储,实际应用中应替换为数据库

func init() {
	// 初始化WebAuthn配置
	rp := protocol.RelyingParty{
		Name: "My App",
		ID:   "localhost",
	}
	config := &webauthn.Config{
		RPID:                rp.ID,
		RPOrigin:            "http://localhost:8080",
		EffectiveDomain:     "localhost",
		AttestationType:     protocol.PreferNoAttestation,
		SupportedExtensions: []protocol.ExtensionID{protocol.HMACSecretExtensionID},
	}
	web = webauthn.New(config)
}

func main() {
	gin.SetMode(gin.ReleaseMode)
	router := gin.Default()

	router.POST("/webauthn/register/begin", beginRegistration)
	router.POST("/webauthn/register/finish", finishRegistration)
	router.POST("/webauthn/authenticate/begin", beginAuthentication)
	router.POST("/webauthn/authenticate/finish", finishAuthentication)

	if err := router.Run(":8080"); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

func beginRegistration(c *gin.Context) {
	user := webauthn.User{
		ID:          []byte("user_unique_id"), // 应该是唯一的用户标识符
		Name:        "John Doe",
		DisplayName: "John",
	}
	regOptions, sessionData, err := web.BeginRegistration(context.Background(), user)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// 应保存sessionData用于后续步骤
	_ = sessionData // 实际应用中需要存储sessionData

	c.JSON(http.StatusOK, gin.H{
		"registrationOptions": regOptions,
	})
}

func finishRegistration(c *gin.Context) {
	var regReq protocol.ParsedRegistrationData
	err := json.NewDecoder(c.Request.Body).Decode(&regReq)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid registration data"})
		return
	}

	// 这里假设sessionData已经从之前步骤获取
	sessionData := []byte{} // 应从数据库或会话中获取
	cred, err := web.FinishRegistration(context.Background(), sessionData, regReq)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// 实际应用中,需要将cred.ID和cred.PublicKey等存入数据库
	credentials[cred.ID.String()] = cred

	c.JSON(http.StatusOK, gin.H{"message": "Registration successful"})
}

func beginAuthentication(c *gin.Context) {
	// 从请求中获取用户ID或用户名
	username := c.PostForm("username")

	// 从数据库中查找对应的credentials
	cred, ok := credentials[username]
	if !ok {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
		return
	}

	authOptions, sessionData, err := web.BeginAuthentication(context.Background(), *cred)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	// 应保存sessionData用于后续步骤
	_ = sessionData // 实际应用中需要存储sessionData

	c.JSON(http.StatusOK, gin.H{
		"authenticationOptions": authOptions,
	})
}

func finishAuthentication(c *gin.Context) {
	var authReq protocol.ParsedAssertionData
	err := json.NewDecoder(c.Request.Body).Decode(&authReq)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid authentication data"})
		return
	}

	// 这里假设sessionData已经从之前步骤获取
	sessionData := []byte{} // 应从数据库或会话中获取
	_, err = web.FinishAuthentication(context.Background(), sessionData, authReq)
	if err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"})
		return
	}

	// 验证成功
	c.JSON(http.StatusOK, gin.H{"message": "Authentication successful"})
}

上述代码使用到了go-webauthn/webauthn这个包,仅为简化示例,实际部署时需考虑安全性、错误处理、会话管理及数据库集成等。

使用HTTPS是WebAuthn要求的安全实践,需要在https下才能使用。

前端Vue实现

首先,确保安装了Vue CLI和创建了一个新的Vue项目。然后,安装@github/webauthn-json库来处理WebAuthn的客户端逻辑:

npm install @github/webauthn-json

简易代码示例:

<template>
  <div id="app">
    <h1>Passkey 示例</h1>
    <button @click="startRegistration" v-if="!isRegistering && !isAuthenticated">注册 Passkey</button>
    <button @click="startAuthentication" v-if="!isRegistering && !isAuthenticated" :disabled="!hasPasskeyRegistered">使用 Passkey 登录</button>
    <div v-if="isRegistering">
      <p>注册过程中...</p>
      <button @click="cancelRegistration">取消</button>
    </div>
    <div v-if="isAuthenticated">
      <p>登录成功!</p>
      <button @click="logout">登出</button>
    </div>
  </div>
</template>

<script>
import WebAuthn from '@github/webauthn-json';

export default {
  name: 'App',
  data() {
    return {
      isRegistering: false,
      isAuthenticated: false,
      hasPasskeyRegistered: false, // 假设此状态由后端告知
    };
  },
  methods: {
    async startRegistration() {
      this.isRegistering = true;
      try {
        const response = await fetch('/api/register/begin', { method: 'POST' });
        if (!response.ok) throw new Error('Failed to start registration');
        const options = await response.json();
        
        const credential = await WebAuthn.create(options);
        const registerResponse = await fetch('/api/register/finish', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(credential),
        });
        
        if (!registerResponse.ok) throw new Error('Failed to finish registration');
        console.log('Passkey registered successfully');
        this.hasPasskeyRegistered = true;
        this.isRegistering = false;
      } catch (error) {
        console.error('Registration error:', error);
        this.isRegistering = false;
      }
    },
    
    async startAuthentication() {
      try {
        const response = await fetch('/api/authenticate/begin', { method: 'POST' });
        if (!response.ok) throw new Error('Failed to start authentication');
        const options = await response.json();

        const assertion = await WebAuthn.get(options);
        const authResponse = await fetch('/api/authenticate/finish', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(assertion),
        });

        if (!authResponse.ok) throw new Error('Failed to finish authentication');
        console.log('Passkey authenticated successfully');
        this.isAuthenticated = true;
      } catch (error) {
        console.error('Authentication error:', error);
      }
    },

    cancelRegistration() {
      this.isRegistering = false;
    },

    logout() {
      this.isAuthenticated = false;
    },
  },
};
</script>

结语

Passkey提供了一个简单而安全的密码管理解决方案。通过Go语言的后端和Vue.js的前端,我们能够快速构建一个高效且用户友好的应用。

尽管示例为简化版,但希望它能作为一把钥匙,开启您对Passkey技术深入研究与实践的大门。在未来的开发旅程中,不断探索、优化,将这项前沿技术融入到更多的应用场景中,共同推动互联网安全领域的进步,为用户创造更加安全、便捷的数字生活体验。