การลงทะเบียนพาสคีย์ฝั่งเซิร์ฟเวอร์

ภาพรวม

ภาพรวมระดับสูงของขั้นตอนสำคัญที่เกี่ยวข้องกับการลงทะเบียนพาสคีย์มีดังนี้

ขั้นตอนการลงทะเบียนพาสคีย์

  • กำหนดตัวเลือกเพื่อสร้างพาสคีย์ ส่งข้อมูลดังกล่าวไปยังไคลเอ็นต์เพื่อให้คุณส่งต่อไปยังการเรียกใช้การสร้างพาสคีย์ ซึ่งก็คือการเรียกใช้ WebAuthn API navigator.credentials.create บนเว็บ และ credentialManager.createCredential บน Android หลังจากผู้ใช้ยืนยันการสร้างพาสคีย์ ระบบจะแก้ไขการเรียกใช้การสร้างพาสคีย์และแสดงผลข้อมูลเข้าสู่ระบบ PublicKeyCredential
  • ยืนยันข้อมูลเข้าสู่ระบบและจัดเก็บไว้ในเซิร์ฟเวอร์

ส่วนต่อไปนี้จะเจาะลึกรายละเอียดของแต่ละขั้นตอน

สร้างตัวเลือกการสร้างข้อมูลเข้าสู่ระบบ

ขั้นตอนแรกที่คุณต้องทำบนเซิร์ฟเวอร์คือการสร้างออบเจ็กต์ PublicKeyCredentialCreationOptions

โดยใช้ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO โดยปกติแล้ว ไลบรารีจะเสนอฟังก์ชันยูทิลิตีที่สร้างตัวเลือกเหล่านี้ให้คุณได้ เช่น SimpleWebAuthn มี generateRegistrationOptions

PublicKeyCredentialCreationOptions ควรมีทุกอย่างที่จำเป็นสำหรับการสร้างพาสคีย์ ซึ่งได้แก่ ข้อมูลเกี่ยวกับผู้ใช้ ข้อมูลเกี่ยวกับ RP และการกำหนดค่าสำหรับพร็อพเพอร์ตี้ของข้อมูลเข้าสู่ระบบที่คุณกำลังสร้าง เมื่อกำหนดค่าทั้งหมดนี้แล้ว ให้ส่งค่าเหล่านี้ตามที่จำเป็นไปยังฟังก์ชันในไลบรารีฝั่งเซิร์ฟเวอร์ FIDO ซึ่งมีหน้าที่สร้างออบเจ็กต์ PublicKeyCredentialCreationOptions

ฟิลด์บางฟิลด์ของ PublicKeyCredentialCreationOptions อาจเป็นค่าคงที่ ส่วนอื่นๆ ควรกำหนดแบบไดนามิกบนเซิร์ฟเวอร์ ดังนี้

  • rpId: หากต้องการป้อนข้อมูลรหัส RP บนเซิร์ฟเวอร์ ให้ใช้ฟังก์ชันหรือตัวแปรฝั่งเซิร์ฟเวอร์ที่ระบุชื่อโฮสต์ของเว็บแอปพลิเคชัน เช่น example.com
  • user.name และ user.displayName: หากต้องการป้อนข้อมูลในช่องเหล่านี้ ให้ใช้ข้อมูลเซสชันของผู้ใช้ที่ลงชื่อเข้าใช้ (หรือข้อมูลบัญชีผู้ใช้ใหม่ หากผู้ใช้สร้างพาสคีย์เมื่อลงชื่อสมัครใช้) user.name มักจะเป็นอีเมลและไม่ซ้ำกันสำหรับ RP user.displayName คือชื่อที่เรียกง่าย โปรดทราบว่าบางแพลตฟอร์มอาจไม่ใช้ displayName
  • user.id: สตริงแบบสุ่มที่ไม่ซ้ำกันซึ่งสร้างขึ้นเมื่อสร้างบัญชี ซึ่งควรเป็นแบบถาวร ไม่เหมือนกับชื่อผู้ใช้ที่อาจแก้ไขได้ รหัสผู้ใช้จะระบุบัญชี แต่ไม่ควรมีข้อมูลส่วนบุคคลที่ระบุตัวบุคคลนั้นได้ (PII) คุณน่าจะมีรหัสผู้ใช้อยู่แล้วในระบบ แต่หากจำเป็น ให้สร้างรหัสผู้ใช้สำหรับพาสคีย์โดยเฉพาะเพื่อไม่ให้มี PII
  • excludeCredentials: รายการรหัสของข้อมูลเข้าสู่ระบบที่มีอยู่เพื่อป้องกันการสร้างพาสคีย์ซ้ำจากผู้ให้บริการพาสคีย์ หากต้องการป้อนข้อมูลในช่องนี้ ให้ค้นหาข้อมูลเข้าสู่ระบบที่มีอยู่สำหรับผู้ใช้รายนี้ในฐานข้อมูล ดูรายละเอียดได้ที่หัวข้อป้องกันการสร้างพาสคีย์ใหม่หากมีพาสคีย์อยู่แล้ว
  • challenge: สำหรับการลงทะเบียนข้อมูลเข้าสู่ระบบ การตรวจสอบจะไม่เกี่ยวข้อง เว้นแต่คุณจะใช้การรับรอง ซึ่งเป็นเทคนิคขั้นสูงในการยืนยันตัวตนของผู้ให้บริการพาสคีย์และข้อมูลที่ส่งออกมา อย่างไรก็ตาม แม้ว่าคุณจะไม่ได้ใช้การรับรอง แต่คำถามก็ยังคงเป็นช่องที่ต้องกรอก ดูวิธีการสร้างคำถามที่ปลอดภัยสำหรับการตรวจสอบสิทธิ์ได้ในการตรวจสอบสิทธิ์พาสคีย์ฝั่งเซิร์ฟเวอร์

การเข้ารหัสและการถอดรหัส

PublicKeyCredentialCreationOptions ที่เซิร์ฟเวอร์ส่งมา
PublicKeyCredentialCreationOptions ที่เซิร์ฟเวอร์ส่งมา ช่องไบนารี เช่น challenge และ user.id จะได้รับการเข้ารหัสฝั่งเซิร์ฟเวอร์เป็น base64URL เพื่อให้สามารถส่งตัวเลือกเป็น JSON ผ่าน HTTPS ได้

ออบเจ็กต์ WebAuthn PublicKeyCredentialCreationOptions มาตรฐานมีฟิลด์ที่เป็น ArrayBuffer ซึ่งไม่สามารถแปลงเป็นอนุกรมโดยตรงด้วย JSON.stringify() หากต้องการส่งตัวเลือกเหล่านี้ผ่าน HTTPS คุณต้องเข้ารหัสช่องไบนารีเป็นสตริง base64URL ดังนี้

  • ในเซิร์ฟเวอร์ ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO (เช่น SimpleWebAuthn) จะสร้างออบเจ็กต์ตัวเลือกที่ทำให้เป็นอนุกรม JSON ได้ (เช่น PublicKeyCredentialCreationOptionsJSON) โดยอัตโนมัติด้วยช่องที่เข้ารหัส Base64URL
  • ในฝั่งไคลเอ็นต์ เบราว์เซอร์ที่ทันสมัยรองรับ PublicKeyCredential.parseCreationOptionsFromJSON() เพื่อแปลงตัวเลือก JSON เป็น PublicKeyCredentialCreationOptions โดยตรง เมื่อใช้ไลบรารีฝั่งไคลเอ็นต์ เช่น @simplewebauthn/browser ฟังก์ชันอย่าง startRegistration({ optionsJSON: options }) จะจัดการการแปลงนี้โดยอัตโนมัติ

โค้ดตัวอย่าง: สร้างตัวเลือกการสร้างข้อมูลเข้าสู่ระบบ

เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่าง ในขั้นตอนนี้ เราจะส่งต่อการสร้างตัวเลือกข้อมูลเข้าสู่ระบบคีย์สาธารณะไปยังฟังก์ชัน generateRegistrationOptions

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: cred.id,
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = await generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: isoUint8Array.fromUTF8String(user.id),
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        residentKey: 'required',
        userVerification: 'preferred',
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

จัดเก็บคีย์สาธารณะ

PublicKeyCredentialCreationOptions ที่เซิร์ฟเวอร์ส่งมา
navigator.credentials.create แสดงผลออบเจ็กต์ PublicKeyCredential

เมื่อ navigator.credentials.create แก้ไขปัญหาในฝั่งไคลเอ็นต์ได้สำเร็จ หมายความว่าระบบสร้างพาสคีย์เรียบร้อยแล้ว ระบบจะแสดงผลออบเจ็กต์ PublicKeyCredential

ออบเจ็กต์ PublicKeyCredential มีออบเจ็กต์ AuthenticatorAttestationResponse ซึ่งแสดงถึงการตอบกลับของผู้ให้บริการพาสคีย์ต่อคำสั่งของไคลเอ็นต์ในการสร้างพาสคีย์ ซึ่งมีข้อมูลเกี่ยวกับข้อมูลเข้าสู่ระบบใหม่ที่คุณต้องใช้ในฐานะ RP เพื่อตรวจสอบสิทธิ์ผู้ใช้ในภายหลัง ดูข้อมูลเพิ่มเติมเกี่ยวกับ AuthenticatorAttestationResponse ได้ในภาคผนวก: AuthenticatorAttestationResponse

ส่งออบเจ็กต์ PublicKeyCredential ไปยังเซิร์ฟเวอร์ เมื่อได้รับแล้ว ให้ยืนยัน

ส่งต่อขั้นตอนการยืนยันนี้ไปยังไลบรารีฝั่งเซิร์ฟเวอร์ FIDO โดยปกติแล้วไลบรารีจะมีฟังก์ชันยูทิลิตีเพื่อวัตถุประสงค์นี้ เช่น SimpleWebAuthn มี verifyRegistrationResponse ดูสิ่งที่เกิดขึ้นเบื้องหลังได้ในภาคผนวก: การยืนยันการตอบกลับการลงทะเบียน

เมื่อยืนยันสำเร็จแล้ว ให้จัดเก็บข้อมูลเข้าสู่ระบบไว้ในฐานข้อมูลเพื่อให้ผู้ใช้ตรวจสอบสิทธิ์ด้วยพาสคีย์ที่เชื่อมโยงกับข้อมูลเข้าสู่ระบบดังกล่าวได้ในภายหลัง

ใช้ตารางเฉพาะสำหรับข้อมูลเข้าสู่ระบบคีย์สาธารณะที่เชื่อมโยงกับพาสคีย์ ผู้ใช้มีรหัสผ่านได้เพียงรหัสเดียว แต่มีพาสคีย์ได้หลายรายการ เช่น พาสคีย์ที่ซิงค์ผ่าน Apple iCloud Keychain และพาสคีย์ที่ซิงค์ผ่านเครื่องมือจัดการรหัสผ่านบน Google

ตัวอย่างสคีมาที่คุณใช้จัดเก็บข้อมูลเข้าสู่ระบบได้มีดังนี้

สคีมาฐานข้อมูลสำหรับพาสคีย์

  • ตารางผู้ใช้
    • user_id: รหัสผู้ใช้หลัก รหัสแบบสุ่มที่ไม่ซ้ำกันและถาวรสำหรับผู้ใช้ ใช้คีย์นี้เป็นคีย์หลักสำหรับตารางผู้ใช้
    • username ชื่อผู้ใช้ที่ผู้ใช้กำหนด ซึ่งอาจแก้ไขได้
    • passkey_user_id: รหัสผู้ใช้ที่ไม่มี PII เฉพาะสำหรับพาสคีย์ ซึ่งแสดงด้วย user.id ในตัวเลือกการลงทะเบียน เมื่อผู้ใช้พยายามตรวจสอบสิทธิ์ในภายหลัง ตัวตรวจสอบสิทธิ์จะทำให้ passkey_user_id นี้พร้อมใช้งานในการตอบกลับการตรวจสอบสิทธิ์ใน userHandle เราขอแนะนำว่าอย่าตั้งค่า passkey_user_id เป็นคีย์หลัก คีย์หลักมักจะกลายเป็น PII โดยพฤตินัยในระบบ เนื่องจากมีการใช้งานอย่างแพร่หลาย
  • ตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
    • id: รหัสข้อมูลเข้าสู่ระบบ ใช้คอลัมน์นี้เป็นคีย์หลักสำหรับตารางข้อมูลเข้าสู่ระบบคีย์สาธารณะ
    • public_key: คีย์สาธารณะของข้อมูลเข้าสู่ระบบ
    • passkey_user_id: ใช้เป็นคีย์นอกเพื่อสร้างลิงก์กับตารางผู้ใช้
    • backed_up: ระบบจะสำรองข้อมูลพาสคีย์หากผู้ให้บริการพาสคีย์ซิงค์ข้อมูลพาสคีย์ การจัดเก็บสถานะการสำรองข้อมูลจะมีประโยชน์หากคุณต้องการพิจารณาเลิกใช้รหัสผ่านในอนาคตสำหรับผู้ใช้ที่มีพาสคีย์ backed_up คุณสามารถตรวจสอบว่ามีการสำรองข้อมูลพาสคีย์หรือไม่โดยดูที่แฟล็ก BE ใน authenticatorData หรือใช้ฟีเจอร์ไลบรารีฝั่งเซิร์ฟเวอร์ FIDO ซึ่งโดยปกติแล้วจะพร้อมใช้งานเพื่อให้คุณเข้าถึงข้อมูลนี้ได้ง่ายๆ การจัดเก็บการมีสิทธิ์สำรองข้อมูลอาจเป็นประโยชน์ในการตอบคำถามของผู้ใช้ที่อาจเกิดขึ้น
    • name: (ไม่บังคับ) ชื่อที่แสดงสำหรับข้อมูลเข้าสู่ระบบเพื่อให้ผู้ใช้ตั้งชื่อที่กำหนดเองสำหรับข้อมูลเข้าสู่ระบบได้
    • transports: อาร์เรย์ของการรับส่ง การจัดเก็บการส่งข้อมูลจะเป็นประโยชน์ต่อประสบการณ์ของผู้ใช้ในการตรวจสอบสิทธิ์ เมื่อมีการรับส่งข้อมูล เบราว์เซอร์จะทำงานตามนั้นและแสดง UI ที่ตรงกับการรับส่งข้อมูลที่ผู้ให้บริการพาสคีย์ใช้ในการสื่อสารกับไคลเอ็นต์ โดยเฉพาะอย่างยิ่งสำหรับกรณีการใช้งานการตรวจสอบสิทธิ์ซ้ำที่ allowCredentials ไม่ว่างเปล่า

ข้อมูลอื่นๆ ที่อาจเป็นประโยชน์ในการจัดเก็บเพื่อวัตถุประสงค์ด้านประสบการณ์ของผู้ใช้ ได้แก่ รายการต่างๆ เช่น ผู้ให้บริการพาสคีย์ เวลาที่สร้างข้อมูลเข้าสู่ระบบ และเวลาที่ใช้ครั้งล่าสุด อ่านเพิ่มเติมได้ในการออกแบบอินเทอร์เฟซผู้ใช้ของพาสคีย์

โค้ดตัวอย่าง: จัดเก็บข้อมูลเข้าสู่ระบบ

เราใช้ไลบรารี SimpleWebAuthn ในตัวอย่าง ในขั้นตอนนี้ เราจะส่งต่อการยืนยันการตอบกลับการลงทะเบียนไปยังฟังก์ชัน verifyRegistrationResponse

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const {
      aaguid,
      credential,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '00000000-0000-0000-0000-000000000000' ?
        req.useragent?.platform : aaguids[aaguid]?.name;

    const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: credential.id,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: credential.transports || response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

ภาคผนวก: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse มีออบเจ็กต์ที่สำคัญ 2 รายการ ได้แก่

  • response.clientDataJSON คือเวอร์ชัน JSON ของข้อมูลไคลเอ็นต์ ซึ่งในเว็บคือข้อมูลที่เบราว์เซอร์เห็น ซึ่งประกอบด้วยต้นทางของ RP, ชาเลนจ์ และ androidPackageName หากไคลเอ็นต์เป็นแอป Android ในฐานะ RP การอ่าน clientDataJSON จะช่วยให้คุณเข้าถึงข้อมูลที่เบราว์เซอร์เห็น ณ เวลาที่มีคำขอ create
  • response.attestationObjectมีข้อมูล 2 ส่วนดังนี้
    • attestationStatement ซึ่งไม่เกี่ยวข้อง เว้นแต่คุณจะใช้การรับรอง
    • authenticatorData คือข้อมูลที่ผู้ให้บริการพาสคีย์เห็น ในฐานะ RP การอ่าน authenticatorData จะช่วยให้คุณเข้าถึงข้อมูลที่ผู้ให้บริการพาสคีย์เห็นและส่งกลับมา ณ เวลาที่มีคำขอ create

authenticatorDataมีข้อมูลสำคัญเกี่ยวกับข้อมูลเข้าสู่ระบบคีย์สาธารณะที่เชื่อมโยงกับพาสคีย์ที่สร้างขึ้นใหม่ ดังนี้

  • ข้อมูลเข้าสู่ระบบคีย์สาธารณะและรหัสข้อมูลเข้าสู่ระบบที่ไม่ซ้ำกันสำหรับข้อมูลเข้าสู่ระบบดังกล่าว
  • รหัส RP ที่เชื่อมโยงกับข้อมูลเข้าสู่ระบบ
  • แฟล็กที่อธิบายสถานะของผู้ใช้เมื่อสร้างพาสคีย์: ผู้ใช้แสดงตัวจริงหรือไม่ และผู้ใช้ได้รับการยืนยันสำเร็จหรือไม่ (ดูข้อมูลเจาะลึกเกี่ยวกับ userVerification)
  • AAGUID คือตัวระบุสำหรับผู้ให้บริการพาสคีย์ เช่น เครื่องมือจัดการรหัสผ่านบน Google คุณสามารถระบุผู้ให้บริการพาสคีย์และแสดงชื่อในหน้าการจัดการพาสคีย์ได้โดยอิงตาม AAGUID (ดูระบุผู้ให้บริการพาสคีย์ด้วย AAGUID)

แม้ว่า authenticatorData จะฝังอยู่ภายใน attestationObject แต่ข้อมูลที่อยู่ในนั้นก็จำเป็นสำหรับการติดตั้งใช้งานพาสคีย์ ไม่ว่าคุณจะใช้การรับรองหรือไม่ก็ตาม authenticatorData ได้รับการเข้ารหัสและมีช่องที่เข้ารหัสในรูปแบบไบนารี โดยปกติแล้ว ไลบรารีฝั่งเซิร์ฟเวอร์จะจัดการการแยกวิเคราะห์และการถอดรหัส หากคุณไม่ได้ใช้ไลบรารีฝั่งเซิร์ฟเวอร์ ให้พิจารณาใช้ประโยชน์จากฝั่งไคลเอ็นต์ getAuthenticatorData() เพื่อประหยัดงานแยกวิเคราะห์และถอดรหัสฝั่งเซิร์ฟเวอร์

ภาคผนวก: การยืนยันการตอบกลับการลงทะเบียน

เบื้องหลังการยืนยันการตอบกลับการลงทะเบียนประกอบด้วยการตรวจสอบต่อไปนี้

  • ตรวจสอบว่ารหัส RP ตรงกับเว็บไซต์ของคุณ
  • ตรวจสอบว่าต้นทางของคำขอเป็นต้นทางที่คาดไว้สำหรับเว็บไซต์ของคุณ (URL ของเว็บไซต์หลัก, แอป Android)
  • หากคุณต้องการการยืนยันตัวตนของผู้ใช้ ให้ตรวจสอบว่าแฟล็กการยืนยันตัวตนของผู้ใช้ authenticatorData.uv เป็น true
  • โดยปกติแล้ว แฟล็กการแสดงตนของผู้ใช้ authenticatorData.up ควรเป็น true แต่หากข้อมูลเข้าสู่ระบบสร้างขึ้นแบบมีเงื่อนไข แฟล็กควรเป็น false
  • ตรวจสอบว่าไคลเอ็นต์สามารถให้คำถามที่คุณให้ไว้ได้ หากคุณไม่ได้ใช้เอกสารรับรอง การตรวจสอบนี้ก็ไม่สำคัญ อย่างไรก็ตาม การใช้การตรวจสอบนี้เป็นแนวทางปฏิบัติแนะนำ ซึ่งจะช่วยให้มั่นใจว่าโค้ดของคุณพร้อมใช้งานหากคุณตัดสินใจใช้เอกสารรับรองในอนาคต
  • ตรวจสอบว่ายังไม่มีการลงทะเบียนรหัสข้อมูลเข้าสู่ระบบสำหรับผู้ใช้รายใด
  • ยืนยันว่าอัลกอริทึมที่ผู้ให้บริการพาสคีย์ใช้สร้างข้อมูลเข้าสู่ระบบเป็นอัลกอริทึมที่คุณระบุไว้ (ในช่อง alg แต่ละช่องของ publicKeyCredentialCreationOptions.pubKeyCredParams ซึ่งโดยปกติจะกำหนดไว้ภายในไลบรารีฝั่งเซิร์ฟเวอร์และคุณมองไม่เห็น) ซึ่งวิธีนี้ช่วยให้มั่นใจว่าผู้ใช้จะลงทะเบียนได้เฉพาะกับอัลกอริทึมที่คุณเลือกอนุญาตเท่านั้น

ดูข้อมูลเพิ่มเติมได้ที่ซอร์สโค้ดสำหรับ verifyRegistrationResponse ของ SimpleWebAuthn หรือเจาะลึกรายการการยืนยันทั้งหมดในข้อกำหนดเฉพาะ

รายการถัดไป

การตรวจสอบสิทธิ์พาสคีย์ฝั่งเซิร์ฟเวอร์