GraphQL - การพิสูจน์ตัวตนไคลเอ็นต์

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

ด่วนจว

ในตัวอย่างนี้เราจะใช้ jQuery เพื่อสร้างแอปพลิเคชันไคลเอนต์ ในการตรวจสอบคำขอเราจะใช้ express-jwt โมดูลทางฝั่งเซิร์ฟเวอร์

โมดูล express-jwt เป็นมิดเดิลแวร์ที่ให้คุณตรวจสอบสิทธิ์คำขอ HTTP โดยใช้โทเค็น JWT JSON Web Token (JWT) เป็นสตริงแบบยาวที่ระบุผู้ใช้ที่ล็อกอิน

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

ภาพประกอบ

เราจะทำตามขั้นตอนที่ชาญฉลาดเพื่อทำความเข้าใจภาพประกอบนี้

การตั้งค่าเซิร์ฟเวอร์

ต่อไปนี้เป็นขั้นตอนในการตั้งค่าเซิร์ฟเวอร์ -

ขั้นตอนที่ 1 - ดาวน์โหลดและติดตั้งการอ้างอิงที่จำเป็นสำหรับโครงการ

สร้างโฟลเดอร์ auth-server-app. เปลี่ยนไดเร็กทอรีของคุณเป็น auth-server-app จากขั้ว ทำตามขั้นตอนที่ 3 ถึง 5 ที่อธิบายไว้ในบทการตั้งค่าสภาพแวดล้อม

ขั้นตอนที่ 2 - สร้างสคีมา

เพิ่ม schema.graphql ไฟล์ในโฟลเดอร์โครงการ  auth-server-app และเพิ่มรหัสต่อไปนี้ -

type Query
{
   greetingWithAuth:String
}

ขั้นตอนที่ 3 - เพิ่มตัวแก้ไข

สร้างไฟล์ resolvers.js ในโฟลเดอร์โครงการและเพิ่มรหัสต่อไปนี้ -

ตัวแก้ไขจะตรวจสอบว่าอ็อบเจ็กต์ผู้ใช้ที่พิสูจน์ตัวตนมีอยู่ในอ็อบเจ็กต์บริบทของ GraphQL หรือไม่ จะเพิ่มข้อยกเว้นหากไม่มีผู้ใช้ที่ผ่านการรับรองความถูกต้อง

const db = require('./db')

const Query = {
   greetingWithAuth:(root,args,context,info) => {

      //check if the context.user is null
      if (!context.user) {
         throw new Error('Unauthorized');
      }
      return "Hello from TutorialsPoint, welcome back : "+context.user.firstName;
   }
}

module.exports = {Query}

ขั้นตอนที่ 4 - สร้างไฟล์ Server.js

มิดเดิลแวร์การตรวจสอบสิทธิ์รับรองความถูกต้องของผู้โทรโดยใช้ JSON Web Token URL สำหรับการพิสูจน์ตัวตนคือ http://localhost:9000/login.

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

หากโทเค็นถูกต้อง req.user จะถูกตั้งค่าด้วยออบเจ็กต์ JSON ที่ถอดรหัสเพื่อใช้โดยมิดเดิลแวร์ในภายหลังสำหรับการอนุญาตและการควบคุมการเข้าถึง

โค้ดต่อไปนี้ใช้สองโมดูล - jsonwebtoken และ express-jwt เพื่อพิสูจน์ตัวตนคำขอ -

  • เมื่อผู้ใช้คลิกที่ไฟล์ greetจะมีการร้องขอเส้นทาง / graphql หากผู้ใช้ไม่ได้รับการรับรองความถูกต้องเขาจะได้รับแจ้งให้ตรวจสอบสิทธิ์ตัวเอง

  • ผู้ใช้จะได้รับแบบฟอร์มที่ยอมรับรหัสอีเมลและรหัสผ่าน ในตัวอย่างของเราเส้นทาง / เข้าสู่ระบบมีหน้าที่ในการพิสูจน์ตัวตนผู้ใช้

  • เส้นทาง / login ตรวจสอบว่าพบรายการที่ตรงกันในฐานข้อมูลหรือไม่สำหรับข้อมูลประจำตัวที่ผู้ใช้ให้มา

  • หากข้อมูลรับรองไม่ถูกต้องข้อยกเว้น HTTP 401 จะถูกส่งกลับไปยังผู้ใช้

  • หากข้อมูลรับรองถูกต้องเซิร์ฟเวอร์จะสร้างโทเค็น โทเค็นนี้ถูกส่งเป็นส่วนหนึ่งของการตอบกลับไปยังผู้ใช้ สิ่งนี้ทำได้โดยฟังก์ชัน jwt.sign

const expressJwt = require('express-jwt');
const jwt = require('jsonwebtoken');

//private key
const jwtSecret = Buffer.from('Zn8Q5tyZ/G1MHltc4F/gTkVJMlrbKiZt', 'base64');

app.post('/login', (req, res) => {
   const {email, password} = req.body;
   
   //check database
   const user = db.students.list().find((user) =>  user.email === email);
   if (!(user && user.password === password)) {
      res.sendStatus(401);
      return;
   }
   
   //generate a token based on private key, token doesn't have an expiry
   const token = jwt.sign({sub: user.id}, jwtSecret);
   res.send({token});
});

สำหรับทุกคำขอฟังก์ชัน app.use () จะถูกเรียกใช้ สิ่งนี้จะเรียกใช้มิดเดิลแวร์ expressJWT มิดเดิลแวร์นี้จะถอดรหัส JSON Web Token รหัสผู้ใช้ที่จัดเก็บในโทเค็นจะถูกเรียกและเก็บเป็นผู้ใช้คุณสมบัติในออบเจ็กต์คำขอ

//decodes the JWT and stores in request object
app.use(expressJwt({
   secret: jwtSecret,
   credentialsRequired: false
}));

เพื่อให้คุณสมบัติผู้ใช้พร้อมใช้งานภายในบริบท GraphQL คุณสมบัตินี้ถูกกำหนดให้กับ context วัตถุดังแสดงด้านล่าง -

//Make req.user available to GraphQL context
app.use('/graphql', graphqlExpress((req) => ({
   schema,
   context: {user: req.user &&apm; db.students.get(req.user.sub)}
})));

สร้าง server.js ในเส้นทางโฟลเดอร์ปัจจุบัน ไฟล์ server.js ที่สมบูรณ์มีดังนี้ -

const bodyParser = require('body-parser');
const cors = require('cors');
const express = require('express');
const expressJwt = require('express-jwt'); //auth
const jwt = require('jsonwebtoken'); //auth
const db = require('./db');

var port = process.env.PORT || 9000
const jwtSecret = Buffer.from('Zn8Q5tyZ/G1MHltc4F/gTkVJMlrbKiZt', 'base64');
const app = express();

const fs = require('fs')
const typeDefs = fs.readFileSync('./schema.graphql',{encoding:'utf-8'})
const resolvers = require('./resolvers')
const {makeExecutableSchema} = require('graphql-tools')

const schema = makeExecutableSchema({typeDefs, resolvers})

app.use(cors(), bodyParser.json(), expressJwt({
   secret: jwtSecret,
   credentialsRequired: false
}));

const  {graphiqlExpress,graphqlExpress} = require('apollo-server-express')

app.use('/graphql', graphqlExpress((req) => ({
   schema,
   context: {user: req.user && db.students.get(req.user.sub)}
})));
app.use('/graphiql',graphiqlExpress({endpointURL:'/graphql'}))

//authenticate students
app.post('/login', (req, res) => {
   const email = req.body.email;
   const password = req.body.password;

   const user = db.students.list().find((user) =>  user.email === email);
   if (!(user && user.password === password)) {
      res.sendStatus(401);
      return;
   }
   const token = jwt.sign({sub: user.id}, jwtSecret);
   res.send({token});
});

app.listen(port, () => console.info(`Server started on port ${port}`));

ขั้นตอนที่ 5 - เรียกใช้แอปพลิเคชัน

ดำเนินการคำสั่ง  npm start ในเทอร์มินัล เซิร์ฟเวอร์จะทำงานบนพอร์ต 9000 ที่นี่เราใช้ GraphiQL เป็นไคลเอนต์เพื่อทดสอบแอปพลิเคชัน

เปิดเบราว์เซอร์และพิมพ์ URL http://localhost:9000/graphiql. พิมพ์คำค้นหาต่อไปนี้ในตัวแก้ไข -

{
   greetingWithAuth
}

ในการตอบกลับด้านล่างเราได้รับข้อผิดพลาดเนื่องจากเราไม่ใช่ผู้ใช้ที่ตรวจสอบสิทธิ์

{
   "data": {
      "greetingWithAuth": null
   },
   "errors": [
      {
         "message": "Unauthorized",
         "locations": [
            {
               "line": 2,
               "column": 3
            }
         ],
         "path": [
            "greetingWithAuth"
         ]
      }
   ]
}

ในส่วนถัดไปให้เราสร้างแอปพลิเคชันไคลเอนต์เพื่อตรวจสอบสิทธิ์

การตั้งค่าไคลเอ็นต์ JQuery

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

เมื่อคุณเข้าสู่ระบบด้วยผู้ใช้ที่มีอยู่ในฐานข้อมูลหน้าจอต่อไปนี้จะปรากฏขึ้น -

ในการเข้าถึง greetingเราต้องเข้าถึง URL ก่อน http://localhost:9000/login เส้นทางดังต่อไปนี้

การตอบกลับจะมีโทเค็นที่สร้างจากเซิร์ฟเวอร์

$.ajax({
   url:"http://localhost:9000/login",
   contentType:"application/json",
   type:"POST",
   data:JSON.stringify({email,password}),
   success:function(response) {
      loginToken = response.token;
      $('#authStatus')
      .html("authenticated successfully")
      .css({"color":"green",'font-weight':'bold'});
      $("#greetingDiv").html('').css({'color':''});
   },
   error:(xhr,err) =>  alert('error')
})

หลังจากล็อกอินสำเร็จเราสามารถเข้าถึงสคีมาgreetingWithAuthตามที่ระบุด้านล่าง ควรมี Authorizationheader สำหรับคำขอที่ตามมาทั้งหมดพร้อมโทเค็นผู้ถือ

{ 
   url: "http://localhost:9000/graphql",
   contentType: "application/json",
   headers: {"Authorization": 'bearer '+loginToken},  type:'POST',
   data: JSON.stringify({
   query:`{greetingWithAuth}`
}

ต่อไปนี้เป็นรหัสสำหรับ index.html -

<!DOCTYPE html>
<html>
   <head>
      <script src = "https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
      <script>
         $(document).ready(function() {
            let loginToken = "";
            $("#btnGreet").click(function() {
                  $.ajax({url: "http://localhost:9000/graphql",
                  contentType: "application/json",
                  headers: {"Authorization": 'bearer '+loginToken},
                  type:'POST',
                  data: JSON.stringify({
                  query:`{greetingWithAuth}` }),
                  success: function(result) {
                  $("#greetingDiv").html("<h1>"+result.data.greetingWithAuth+"</h1>")
                  },
                  error:function(jQxhr,error) {
                     if(jQxhr.status == 401) {
                        $("#greetingDiv").html('please authenticate first!!')
                        .css({"color":"red",'font-weight':'bold'})
                        return;
                     }
                     $("#greetingDiv").html('error').css("color","red");
                  }
               });
            });
            $('#btnAuthenticate').click(function() {
               var email =  $("#txtEmail").val();
               var password =  $("#txtPwd").val();
               if(email && password) {
                  $.ajax({
                     url:"http://localhost:9000/login",
                     contentType:"application/json",
                     type:"POST",
                     data:JSON.stringify({email,password}),
                     success:function(response) {
                        loginToken =  response.token;
                        $('#authStatus')
                        .html("authenticated successfully")
                        .css({"color":"green",'font-weight':'bold'});
                        $("#greetingDiv").html('').css({'color':''});
                     },
                     error:(xhr,err) =>  alert('error')
                  })
               }else alert("email and pwd empty")
            })
         });
      </script>
   </head>
   
   <body>
      <h1> GraphQL Authentication </h1>
      <hr/>
      <section>
         <button id = "btnGreet">Greet</button>
         <br/> <br/>
         <div id = "greetingDiv"></div>
      </section>
      <br/> <br/> <br/>
      <hr/>
      
      <section id = "LoginSection">
         <header>
            <h2>*Login first to  access greeting </h2>
         </header>
         <input type = "text" value = "[email protected]" placeholder = "enter email" id = "txtEmail"/>
         <br/>
         
         <input type = "password" value = "pass123" placeholder = "enter password" id = "txtPwd"/>
         <br/>
         
         <input type = "button" id = "btnAuthenticate"  value = "Login"/>
         <p id = "authStatus"></p>
      </section>
   </body>
</html>